//! Factorization methods for multivariate polynomials
//! that implement [Factorize].
use std::{
borrow::Cow,
cmp::Reverse,
collections::{BinaryHeap, hash_map::Entry},
ops::RangeInclusive,
};
use ahash::{HashMap, HashSet, HashSetExt};
use rand::{Rng, SeedableRng, rng, rngs::StdRng};
use tracing::debug;
use crate::{
GLOBAL_SETTINGS,
combinatorics::CombinationIterator,
domains::{
EuclideanDomain, Field, InternalOrdering, Ring, RingOps, SampleableRing, Set,
algebraic::{AlgebraicExtension, GaloisField},
finite_field::{
FiniteField, FiniteFieldCore, FiniteFieldElement, FiniteFieldWorkspace,
PrimeIteratorU64, ToFiniteField, Zp,
},
integer::{Integer, IntegerRing, Z, gcd_unsigned},
rational::{Q, RationalField},
},
kernels::{DensePolynomialMulRequest, GeometricSequenceStepRequest},
tensors::matrix::Vector,
};
use super::{
LexOrder, PositiveExponent,
gcd::{MAX_RNG_PREFACTOR, POW_CACHE_SIZE, PolynomialGCD},
polynomial::MultivariatePolynomial,
};
const SPARSE_MDP_SAMPLE_BASE_ATTEMPTS: usize = 1;
// Maximum average number of input terms per bivariate degree-box position for
// selecting the bivariate-start integer factorization algorithm.
const INTEGER_FACTOR_BIVARIATE_SPARSE_BOX_DENSITY_THRESHOLD: f64 = 5.0;
// Minimum term count for selecting a lower-degree main variable for sparse
// univariate-start integer factorization.
const INTEGER_FACTOR_SPARSE_UNIVARIATE_MIN_TERMS: usize = 256;
// Minimum bivariate-box score for selecting univariate start with a margin
// above the automatic method boundary.
const INTEGER_FACTOR_SPARSE_UNIVARIATE_MIN_BOX_DENSITY: f64 =
2.0 * INTEGER_FACTOR_BIVARIATE_SPARSE_BOX_DENSITY_THRESHOLD;
// Maximum leading-layer size relative to the smallest layer in the input.
const INTEGER_FACTOR_SPARSE_UNIVARIATE_MAX_LCOEFF_RATIO: usize = 2;
// Minimum reduction in main-variable degree required by the reordered image.
const INTEGER_FACTOR_SPARSE_UNIVARIATE_MIN_DEGREE_RATIO: usize = 4;
// Number of failed univariate lifting attempts before automatic integer
// factorization retries the polynomial with the bivariate-start algorithm.
const INTEGER_FACTOR_UNIVARIATE_AUTO_RETRIES: usize = 3;
// Number of failed bivariate lifting attempts before automatic integer
// factorization retries the polynomial with the univariate-start algorithm.
const INTEGER_FACTOR_BIVARIATE_AUTO_RETRIES: usize = 3;
// Number of small deterministic coordinate blocks tried before random sampling.
const WANG_PRIME_SAMPLE_ATTEMPTS: usize = 3;
// Number of cyclic rotations of the fixed bivariate Wang prime coordinates.
const BIVARIATE_WANG_PRIME_SAMPLE_ROTATIONS: usize = 3;
// Lower boundary for the selected bivariate-box score used by one-image Wang
// leading-coefficient reconstruction; the score must be strictly above it.
const INTEGER_FACTOR_BIVARIATE_WANG_MIN_BOX_DENSITY: f64 = 4.0;
// Maximum number of active variables in a bivariate-start image whose monomial
// leading coefficient is reconstructed from distinct-prime evaluations.
const INTEGER_FACTOR_BIVARIATE_WANG_MAX_VARIABLES: usize = 5;
// Maximum sum of degrees in coordinates evaluated by a deterministic
// bivariate Wang sample.
const INTEGER_FACTOR_BIVARIATE_WANG_MAX_EVALUATED_TOTAL_DEGREE: u32 = 96;
// Largest degree factored in a one-shot univariate image used only to certify
// that a post-separable multivariate component is irreducible.
const INTEGER_FACTOR_IRREDUCIBILITY_SCOUT_MAX_DEGREE: usize = 64;
// Maximum number of retained coefficient cells across the target and factor images
// used by one evaluated Hensel stage.
const MAX_EVALUATED_HENSEL_IMAGE_CELLS: usize = 1 << 22;
// Maximum number of terms grouped into geometric sequences for one polynomial image.
const MAX_EVALUATED_HENSEL_GROUPED_TERMS: usize = 1 << 20;
// Minimum number of base-prime digits for using composite-modulus quadratic Hensel corrections.
const MIN_QUADRATIC_HENSEL_DIGITS: usize = 64;
// Minimum retained modular factor count for spending another distinct-degree
// factorization on a wide prime after the small-prime search.
const MIN_DENSE_U64_PRIME_FACTOR_COUNT: usize = 10;
// Minimum factor-bound height for reducing a two-factor bivariate lift with a wide base prime.
const BIVARIATE_WIDE_PRIME_MIN_BOUND_BITS: u64 = 256;
// Largest main-variable degree for which the wide-prime convolution bound stays in u64.
const BIVARIATE_WIDE_PRIME_MAX_DEGREE: u32 = 64;
// Start of the prime range that supplies about 26 bits per p-adic correction digit.
const BIVARIATE_WIDE_PRIME_START: u64 = 65_000_000;
// Number of wide candidates tried before resuming the complete small-prime search.
const BIVARIATE_WIDE_PRIME_ATTEMPTS: usize = 8;
// Maximum number of term advances across all geometric samples in one image rebuild.
const MAX_EVALUATED_HENSEL_TERM_STEPS: usize = 1 << 24;
// Maximum number of dense coefficient rows retained for either lifted factor.
const MAX_EVALUATED_HENSEL_Y_ROWS: usize = 1 << 16;
// Largest univariate finite-field image retained in dense storage while
// screening integer-factorization primes.
const MAX_DENSE_ZP_DDF_COEFFICIENTS: usize = 256;
// Sparse inputs below this density keep using the generic polynomial path.
const MAX_DENSE_ZP_DDF_SPARSITY_RATIO: usize = 4;
// Number of ordinary DDF steps completed before considering a cached
// high-characteristic Frobenius map.
const DENSE_ZP_FROBENIUS_CLASSICAL_PREFIX: usize = 4;
// Bound the cumulative and simultaneously pending products generated by the
// packed sparse polynomial square-root path.
const MAX_SPARSE_SQUARE_ROOT_PAIR_PRODUCTS: usize = 1 << 22;
const MAX_SPARSE_SQUARE_ROOT_PENDING_PRODUCTS: usize = 1 << 20;
const MIN_PACKED_SPARSE_SQUARE_TERMS: usize = 64;
const MAX_PACKED_SPARSE_SQUARE_PAIRS: usize = 1 << 20;
// Minimum input size for trying a certified quadratic split before the general
// separability and square-free passes.
const MIN_EARLY_QUADRATIC_FACTOR_TERMS: usize = 256;
// Number of high-support coefficient views checked for a factor that omits a
// variable before an exact quadratic discriminant is constructed.
const EARLY_SEPARABLE_CONTENT_PAIR_PROBES: usize = 2;
// Minimum estimated discriminant products per input term before coefficient
// content is cheaper to scout than constructing the exact discriminant.
const EARLY_SEPARABLE_CONTENT_MIN_PRODUCT_RATIO: usize = 32;
// Prime used by the univariate finite-field images that screen early
// discriminants and coefficient-layer GCDs. It is 3 modulo 4, so a square
// root of a quadratic residue needs one exponentiation.
const EARLY_FACTOR_PROBE_PRIME: u32 = 65_519;
/// Distinct-degree blocks together with their exact number of irreducible factors.
struct DistinctDegreeFactorization<P> {
blocks: Vec<(usize, P)>,
factor_count: usize,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum DenseZpAccumulationMode {
DirectMontgomeryReduction,
NativeRemainder,
WideRemainder,
}
/// Reusable buffers for dense products and reciprocal reduction over `Zp`.
struct DenseZpMulModWorkspace {
u64_accumulators: Vec<u64>,
u128_accumulators: Vec<u128>,
reverse_dividend: Vec<FiniteFieldElement<u32>>,
reverse_quotient: Vec<FiniteFieldElement<u32>>,
low_product: Vec<FiniteFieldElement<u32>>,
}
impl DenseZpMulModWorkspace {
fn new(maximum_degree: usize) -> Self {
Self {
u64_accumulators: Vec::with_capacity(2 * maximum_degree - 1),
u128_accumulators: Vec::new(),
reverse_dividend: Vec::with_capacity(maximum_degree),
reverse_quotient: Vec::with_capacity(maximum_degree),
low_product: Vec::with_capacity(maximum_degree),
}
}
}
/// A cached Frobenius map for repeated `p`th powers modulo one monic polynomial.
///
/// Column `i` stores `x^(i*p)` modulo the construction modulus. Applying the
/// map computes `sum_i a_i*x^(i*p)`, which equals `a(x)^p` over `Zp`. DDF may
/// subsequently reduce the result modulo a factor of the construction modulus.
struct DenseZpFrobeniusContext {
columns: Vec<Vec<FiniteFieldElement<u32>>>,
u64_accumulators: Vec<u64>,
u128_accumulators: Vec<u128>,
}
impl DenseZpFrobeniusContext {
/// Build all columns from `x^p` using modular products against the fixed
/// construction modulus.
fn new<E: PositiveExponent>(
field: &Zp,
modulus: &[FiniteFieldElement<u32>],
reverse_modulus_inverse: &[FiniteFieldElement<u32>],
x_to_p: &[FiniteFieldElement<u32>],
workspace: &mut DenseZpMulModWorkspace,
) -> Self {
#[cfg(test)]
DENSE_ZP_FROBENIUS_MAP_BUILDS.with(|builds| builds.set(builds.get() + 1));
let modulus_degree = modulus.len() - 1;
debug_assert!(modulus_degree >= 2);
debug_assert!(x_to_p.len() <= modulus_degree);
let mut columns = Vec::with_capacity(modulus_degree);
columns.push(vec![field.one()]);
columns.push(x_to_p.to_vec());
let mut product = Vec::with_capacity(2 * modulus_degree - 1);
while columns.len() < modulus_degree {
DenseZpDistinctDegreeContext::<E>::multiply_mod_into(
field,
modulus,
reverse_modulus_inverse,
columns.last().unwrap(),
x_to_p,
&mut product,
workspace,
);
columns.push(std::mem::take(&mut product));
}
Self {
columns,
u64_accumulators: Vec::with_capacity(modulus_degree),
u128_accumulators: Vec::new(),
}
}
/// Apply the cached linear map to a dense residue in ascending coefficient
/// order. The result is reduced modulo the construction modulus.
fn apply<E: PositiveExponent>(
&mut self,
field: &Zp,
value: &[FiniteFieldElement<u32>],
output: &mut Vec<FiniteFieldElement<u32>>,
) {
debug_assert!(value.len() <= self.columns.len());
let output_len = self.columns.len();
output.resize(output_len, field.zero());
output.fill(field.zero());
let mode =
DenseZpDistinctDegreeContext::<E>::accumulation_mode_for_terms(field, value.len());
match mode {
DenseZpAccumulationMode::DirectMontgomeryReduction
| DenseZpAccumulationMode::NativeRemainder => {
self.u64_accumulators.resize(output_len, 0);
self.u64_accumulators.fill(0);
for (scalar, column) in value.iter().zip(&self.columns) {
if field.is_zero(scalar) {
continue;
}
for (accumulator, coefficient) in self.u64_accumulators.iter_mut().zip(column) {
*accumulator +=
u64::from(*scalar.inner()) * u64::from(*coefficient.inner());
}
}
let reduce_directly =
matches!(mode, DenseZpAccumulationMode::DirectMontgomeryReduction);
for (coefficient, &accumulator) in output.iter_mut().zip(&self.u64_accumulators) {
*coefficient = DenseZpDistinctDegreeContext::<E>::reduce_u64_accumulator(
field,
accumulator,
reduce_directly,
);
}
}
DenseZpAccumulationMode::WideRemainder => {
self.u128_accumulators.resize(output_len, 0);
self.u128_accumulators.fill(0);
for (scalar, column) in value.iter().zip(&self.columns) {
if field.is_zero(scalar) {
continue;
}
for (accumulator, coefficient) in self.u128_accumulators.iter_mut().zip(column)
{
*accumulator +=
u128::from(*scalar.inner()) * u128::from(*coefficient.inner());
}
}
for (coefficient, &accumulator) in output.iter_mut().zip(&self.u128_accumulators) {
*coefficient = DenseZpDistinctDegreeContext::<E>::reduce_u128_accumulator(
field,
accumulator,
);
}
}
}
DenseZpDistinctDegreeContext::<E>::trim(field, output);
}
}
/// Dense arithmetic for distinct-degree factorization over a 32-bit prime field.
///
/// The integer factorizer uses this context while screening univariate modular
/// images. It retains the current monic divisor, its reversed reciprocal, and
/// multiplication workspaces while repeatedly computing Frobenius powers. A
/// sparse polynomial is reconstructed only for the GCD that identifies the
/// next distinct-degree block.
struct DenseZpDistinctDegreeContext<'a, E: PositiveExponent> {
field: Zp,
variable: usize,
template: &'a MultivariatePolynomial<Zp, E, LexOrder>,
modulus: Vec<FiniteFieldElement<u32>>,
reverse_modulus_inverse: Vec<FiniteFieldElement<u32>>,
power: Vec<FiniteFieldElement<u32>>,
multiplication_output: Vec<FiniteFieldElement<u32>>,
multiplication_workspace: DenseZpMulModWorkspace,
}
impl<'a, E: PositiveExponent> DenseZpDistinctDegreeContext<'a, E> {
/// Construct a bounded dense workspace for a monic univariate polynomial.
///
/// Large or genuinely sparse inputs return `None` so their caller can use
/// the generic polynomial representation.
fn new(
polynomial: &'a MultivariatePolynomial<Zp, E, LexOrder>,
variable: usize,
) -> Option<Self> {
if polynomial.is_zero()
|| variable >= polynomial.nvars()
|| polynomial.exponents_iter().any(|exponents| {
exponents
.iter()
.enumerate()
.any(|(index, exponent)| index != variable && !exponent.is_zero())
})
{
return None;
}
let degree = polynomial.degree(variable).to_u32() as usize;
let coefficient_count = degree.checked_add(1)?;
if degree == 0
|| coefficient_count > MAX_DENSE_ZP_DDF_COEFFICIENTS
|| coefficient_count
> polynomial
.nterms()
.saturating_mul(MAX_DENSE_ZP_DDF_SPARSITY_RATIO)
{
return None;
}
let field = polynomial.ring().clone();
if !field.is_one(&polynomial.lcoeff()) {
return None;
}
let modulus = Self::dense_coefficients(polynomial, variable, &field);
debug_assert_eq!(modulus.len(), coefficient_count);
debug_assert!(
modulus
.last()
.is_some_and(|coefficient| field.is_one(coefficient))
);
let mut context = Self {
field,
variable,
template: polynomial,
modulus,
reverse_modulus_inverse: Vec::with_capacity(degree),
power: Vec::with_capacity(degree),
multiplication_output: Vec::with_capacity(2 * degree - 1),
multiplication_workspace: DenseZpMulModWorkspace::new(degree),
};
context.refresh_reverse_modulus_inverse();
Some(context)
}
/// Convert a univariate polynomial to ascending degree-indexed
/// coefficients, retaining zero cells between nonzero terms.
fn dense_coefficients(
polynomial: &MultivariatePolynomial<Zp, E, LexOrder>,
variable: usize,
field: &Zp,
) -> Vec<FiniteFieldElement<u32>> {
let degree = polynomial.degree(variable).to_u32() as usize;
let mut coefficients = vec![field.zero(); degree + 1];
for term in polynomial {
debug_assert!(
term.exponents
.iter()
.enumerate()
.all(|(index, exponent)| { index == variable || exponent.is_zero() })
);
coefficients[term.exponents[variable].to_u32() as usize] = *term.coefficient;
}
coefficients
}
/// Replace the divisor used by subsequent modular products after a
/// distinct-degree block has been removed.
fn set_modulus(&mut self, polynomial: &MultivariatePolynomial<Zp, E, LexOrder>) {
#[cfg(test)]
DENSE_ZP_DDF_MODULUS_UPDATES.with(|updates| updates.set(updates.get() + 1));
let coefficient_count = polynomial.degree(self.variable).to_u32() as usize + 1;
self.modulus.resize(coefficient_count, self.field.zero());
self.modulus.fill(self.field.zero());
for term in polynomial {
self.modulus[term.exponents[self.variable].to_u32() as usize] = *term.coefficient;
}
debug_assert!(
self.modulus
.last()
.is_some_and(|coefficient| self.field.is_one(coefficient))
);
self.refresh_reverse_modulus_inverse();
}
/// Cache the reciprocal of the reversed monic modulus through the degree
/// needed to recover a quotient from a product of reduced residues.
fn refresh_reverse_modulus_inverse(&mut self) {
let modulus_degree = self.modulus.len() - 1;
self.reverse_modulus_inverse
.resize(modulus_degree, self.field.zero());
self.reverse_modulus_inverse[0] = self.field.one();
for degree in 1..modulus_degree {
let (previous, coefficient) = self.reverse_modulus_inverse.split_at_mut(degree);
let mut value = self.field.zero();
for reverse_degree in 1..=degree {
self.field.sub_mul_assign(
&mut value,
&self.modulus[modulus_degree - reverse_degree],
&previous[degree - reverse_degree],
);
}
coefficient[0] = value;
}
}
/// Remove trailing zero cells from a dense coefficient vector.
fn trim(field: &Zp, coefficients: &mut Vec<FiniteFieldElement<u32>>) {
while coefficients
.last()
.is_some_and(|coefficient| field.is_zero(coefficient))
{
coefficients.pop();
}
}
/// Reduce an owned dense polynomial modulo a cached monic divisor.
fn remainder_monic(
field: &Zp,
dividend: &mut Vec<FiniteFieldElement<u32>>,
divisor: &[FiniteFieldElement<u32>],
) {
debug_assert!(divisor.len() >= 2);
debug_assert!(
divisor
.last()
.is_some_and(|coefficient| field.is_one(coefficient))
);
if dividend.len() >= divisor.len() {
let divisor_degree = divisor.len() - 1;
for degree in (divisor_degree..dividend.len()).rev() {
let pivot = std::mem::replace(&mut dividend[degree], field.zero());
if field.is_zero(&pivot) {
continue;
}
let shift = degree - divisor_degree;
for (coefficient, divisor_coefficient) in dividend[shift..degree]
.iter_mut()
.zip(&divisor[..divisor_degree])
{
field.sub_mul_assign(coefficient, divisor_coefficient, &pivot);
}
}
dividend.truncate(divisor_degree);
}
Self::trim(field, dividend);
}
/// Select an exact accumulator and reduction for a truncated convolution.
fn accumulation_mode(
field: &Zp,
left_len: usize,
right_len: usize,
output_len: usize,
) -> DenseZpAccumulationMode {
let collision_count = left_len.min(right_len).min(output_len);
Self::accumulation_mode_for_terms(field, collision_count)
}
/// Select an exact accumulator for a known number of raw Montgomery
/// products contributing to one output coefficient.
fn accumulation_mode_for_terms(field: &Zp, product_count: usize) -> DenseZpAccumulationMode {
let maximum_product = u128::from(field.get_prime() - 1).pow(2);
let maximum_coefficient = maximum_product
.checked_mul(product_count as u128)
.expect("dense finite-field convolution bound overflow");
if maximum_coefficient > u64::MAX as u128 {
DenseZpAccumulationMode::WideRemainder
} else if maximum_coefficient < u128::from(field.get_prime()) << u32::BITS {
DenseZpAccumulationMode::DirectMontgomeryReduction
} else {
DenseZpAccumulationMode::NativeRemainder
}
}
/// Convert an exact `u64` sum of raw Montgomery products to one field
/// coefficient.
#[inline]
fn reduce_u64_accumulator(
field: &Zp,
accumulator: u64,
reduce_directly: bool,
) -> FiniteFieldElement<u32> {
if reduce_directly {
debug_assert!(u128::from(accumulator) < u128::from(field.get_prime()) << u32::BITS);
field.reduce_montgomery_product_sum(accumulator)
} else {
let residue = (accumulator % u64::from(field.get_prime())) as u32;
field.mul(
FiniteFieldElement::from_inner(residue),
FiniteFieldElement::from_inner(1),
)
}
}
/// Convert an exact `u128` sum of raw Montgomery products to one field
/// coefficient.
#[inline]
fn reduce_u128_accumulator(field: &Zp, accumulator: u128) -> FiniteFieldElement<u32> {
let residue = (accumulator % u128::from(field.get_prime())) as u32;
field.mul(
FiniteFieldElement::from_inner(residue),
FiniteFieldElement::from_inner(1),
)
}
/// Compute the requested low coefficients of a dense product, reducing
/// each exact convolution sum once into Montgomery representation.
fn multiply_low_into(
field: &Zp,
left: &[FiniteFieldElement<u32>],
right: &[FiniteFieldElement<u32>],
output_len: usize,
output: &mut Vec<FiniteFieldElement<u32>>,
u64_accumulators: &mut Vec<u64>,
u128_accumulators: &mut Vec<u128>,
) {
if left.is_empty() || right.is_empty() || output_len == 0 {
output.clear();
return;
}
debug_assert!(output_len <= left.len() + right.len() - 1);
output.resize(output_len, field.zero());
let mode = Self::accumulation_mode(field, left.len(), right.len(), output_len);
match mode {
DenseZpAccumulationMode::DirectMontgomeryReduction
| DenseZpAccumulationMode::NativeRemainder => {
u64_accumulators.resize(output_len, 0);
u64_accumulators.fill(0);
for (left_degree, left_coefficient) in left.iter().enumerate() {
if left_degree >= output_len {
break;
}
let retained_right_len = right.len().min(output_len - left_degree);
for (right_degree, right_coefficient) in
right[..retained_right_len].iter().enumerate()
{
let product = u64::from(*left_coefficient.inner())
* u64::from(*right_coefficient.inner());
// `right_degree` is truncated so this index is below `output_len`.
unsafe {
*u64_accumulators.get_unchecked_mut(left_degree + right_degree) +=
product;
}
}
}
let reduce_directly =
matches!(mode, DenseZpAccumulationMode::DirectMontgomeryReduction);
for (coefficient, &accumulator) in output.iter_mut().zip(u64_accumulators.iter()) {
*coefficient =
Self::reduce_u64_accumulator(field, accumulator, reduce_directly);
}
}
DenseZpAccumulationMode::WideRemainder => {
u128_accumulators.resize(output_len, 0);
u128_accumulators.fill(0);
for (left_degree, left_coefficient) in left.iter().enumerate() {
if left_degree >= output_len {
break;
}
let retained_right_len = right.len().min(output_len - left_degree);
for (right_degree, right_coefficient) in
right[..retained_right_len].iter().enumerate()
{
let product = u128::from(*left_coefficient.inner())
* u128::from(*right_coefficient.inner());
// `right_degree` is truncated so this index is below `output_len`.
unsafe {
*u128_accumulators.get_unchecked_mut(left_degree + right_degree) +=
product;
}
}
}
for (coefficient, &accumulator) in output.iter_mut().zip(u128_accumulators.iter()) {
*coefficient = Self::reduce_u128_accumulator(field, accumulator);
}
}
}
}
/// Compute the requested low coefficients of a dense square, accumulating
/// each off-diagonal product twice.
fn square_low_into(
field: &Zp,
value: &[FiniteFieldElement<u32>],
output_len: usize,
output: &mut Vec<FiniteFieldElement<u32>>,
u64_accumulators: &mut Vec<u64>,
u128_accumulators: &mut Vec<u128>,
) {
if value.is_empty() || output_len == 0 {
output.clear();
return;
}
debug_assert!(output_len <= 2 * value.len() - 1);
output.resize(output_len, field.zero());
let mode = Self::accumulation_mode(field, value.len(), value.len(), output_len);
match mode {
DenseZpAccumulationMode::DirectMontgomeryReduction
| DenseZpAccumulationMode::NativeRemainder => {
u64_accumulators.resize(output_len, 0);
u64_accumulators.fill(0);
for (left_degree, left_coefficient) in value.iter().enumerate() {
if left_degree >= output_len {
break;
}
if 2 * left_degree < output_len {
let square = u64::from(*left_coefficient.inner()).pow(2);
// The diagonal index was checked against `output_len` above.
unsafe {
*u64_accumulators.get_unchecked_mut(2 * left_degree) += square;
}
}
let retained_right_len = value.len().min(output_len - left_degree);
if left_degree + 1 < retained_right_len {
for (right_degree, right_coefficient) in value
[left_degree + 1..retained_right_len]
.iter()
.enumerate()
{
let product = 2
* u64::from(*left_coefficient.inner())
* u64::from(*right_coefficient.inner());
let degree = 2 * left_degree + 1 + right_degree;
// The right slice is truncated so this index is below `output_len`.
unsafe {
*u64_accumulators.get_unchecked_mut(degree) += product;
}
}
}
}
let reduce_directly =
matches!(mode, DenseZpAccumulationMode::DirectMontgomeryReduction);
for (coefficient, &accumulator) in output.iter_mut().zip(u64_accumulators.iter()) {
*coefficient =
Self::reduce_u64_accumulator(field, accumulator, reduce_directly);
}
}
DenseZpAccumulationMode::WideRemainder => {
u128_accumulators.resize(output_len, 0);
u128_accumulators.fill(0);
for (left_degree, left_coefficient) in value.iter().enumerate() {
if left_degree >= output_len {
break;
}
if 2 * left_degree < output_len {
let square = u128::from(*left_coefficient.inner()).pow(2);
// The diagonal index was checked against `output_len` above.
unsafe {
*u128_accumulators.get_unchecked_mut(2 * left_degree) += square;
}
}
let retained_right_len = value.len().min(output_len - left_degree);
if left_degree + 1 < retained_right_len {
for (right_degree, right_coefficient) in value
[left_degree + 1..retained_right_len]
.iter()
.enumerate()
{
let product = 2
* u128::from(*left_coefficient.inner())
* u128::from(*right_coefficient.inner());
let degree = 2 * left_degree + 1 + right_degree;
// The right slice is truncated so this index is below `output_len`.
unsafe {
*u128_accumulators.get_unchecked_mut(degree) += product;
}
}
}
}
for (coefficient, &accumulator) in output.iter_mut().zip(u128_accumulators.iter()) {
*coefficient = Self::reduce_u128_accumulator(field, accumulator);
}
}
}
}
/// Reduce a product of two current residues with the cached reversed
/// modulus reciprocal.
fn reduce_product(
field: &Zp,
modulus: &[FiniteFieldElement<u32>],
reverse_modulus_inverse: &[FiniteFieldElement<u32>],
product: &mut Vec<FiniteFieldElement<u32>>,
workspace: &mut DenseZpMulModWorkspace,
) {
Self::trim(field, product);
let modulus_degree = modulus.len() - 1;
if product.len() <= modulus_degree {
return;
}
debug_assert!(product.len() <= 2 * modulus_degree - 1);
debug_assert_eq!(reverse_modulus_inverse.len(), modulus_degree);
let quotient_len = product.len() - modulus_degree;
workspace
.reverse_dividend
.resize(quotient_len, field.zero());
for (coefficient, product_coefficient) in workspace
.reverse_dividend
.iter_mut()
.zip(product[modulus_degree..].iter().rev())
{
*coefficient = *product_coefficient;
}
Self::multiply_low_into(
field,
&workspace.reverse_dividend,
reverse_modulus_inverse,
quotient_len,
&mut workspace.reverse_quotient,
&mut workspace.u64_accumulators,
&mut workspace.u128_accumulators,
);
workspace.reverse_quotient.reverse();
Self::multiply_low_into(
field,
&modulus[..modulus_degree],
&workspace.reverse_quotient,
modulus_degree,
&mut workspace.low_product,
&mut workspace.u64_accumulators,
&mut workspace.u128_accumulators,
);
for (coefficient, product_coefficient) in product[..modulus_degree]
.iter_mut()
.zip(&workspace.low_product)
{
field.sub_assign(coefficient, product_coefficient);
}
product.truncate(modulus_degree);
Self::trim(field, product);
}
/// Multiply two reduced dense residues and reduce their product modulo the
/// cached monic modulus.
fn multiply_mod_into(
field: &Zp,
modulus: &[FiniteFieldElement<u32>],
reverse_modulus_inverse: &[FiniteFieldElement<u32>],
left: &[FiniteFieldElement<u32>],
right: &[FiniteFieldElement<u32>],
output: &mut Vec<FiniteFieldElement<u32>>,
workspace: &mut DenseZpMulModWorkspace,
) {
if left.is_empty() || right.is_empty() {
output.clear();
return;
}
let output_len = left
.len()
.checked_add(right.len())
.and_then(|length| length.checked_sub(1))
.expect("dense finite-field product length overflow");
debug_assert!(left.len() < modulus.len());
debug_assert!(right.len() < modulus.len());
Self::multiply_low_into(
field,
left,
right,
output_len,
output,
&mut workspace.u64_accumulators,
&mut workspace.u128_accumulators,
);
Self::reduce_product(field, modulus, reverse_modulus_inverse, output, workspace);
}
/// Square a reduced dense residue and reduce the result modulo the cached
/// monic modulus.
fn square_mod_into(
field: &Zp,
modulus: &[FiniteFieldElement<u32>],
reverse_modulus_inverse: &[FiniteFieldElement<u32>],
value: &[FiniteFieldElement<u32>],
output: &mut Vec<FiniteFieldElement<u32>>,
workspace: &mut DenseZpMulModWorkspace,
) {
if value.is_empty() {
output.clear();
return;
}
let output_len = 2 * value.len() - 1;
debug_assert!(value.len() < modulus.len());
Self::square_low_into(
field,
value,
output_len,
output,
&mut workspace.u64_accumulators,
&mut workspace.u128_accumulators,
);
Self::reduce_product(field, modulus, reverse_modulus_inverse, output, workspace);
}
/// Replace `value` by its `p`th power modulo the current divisor.
///
/// Since every `Zp` used here has odd characteristic, the low exponent bit
/// is already represented by `value`; binary powering starts with
/// `value^2` and avoids multiplying a polynomial by one.
fn raise_to_characteristic(&mut self, value: &mut Vec<FiniteFieldElement<u32>>) {
Self::remainder_monic(&self.field, value, &self.modulus);
Self::square_mod_into(
&self.field,
&self.modulus,
&self.reverse_modulus_inverse,
value,
&mut self.power,
&mut self.multiplication_workspace,
);
let mut exponent = self.field.get_prime() >> 1;
while exponent != 0 {
if exponent & 1 != 0 {
Self::multiply_mod_into(
&self.field,
&self.modulus,
&self.reverse_modulus_inverse,
value,
&self.power,
&mut self.multiplication_output,
&mut self.multiplication_workspace,
);
std::mem::swap(value, &mut self.multiplication_output);
}
exponent >>= 1;
if exponent != 0 {
Self::square_mod_into(
&self.field,
&self.modulus,
&self.reverse_modulus_inverse,
&self.power,
&mut self.multiplication_output,
&mut self.multiplication_workspace,
);
std::mem::swap(&mut self.power, &mut self.multiplication_output);
}
}
}
/// Return the number of dense modular products used by binary `p`th
/// powering after the initial value has replaced the exponent's low bit.
fn characteristic_power_product_count(&self) -> usize {
let characteristic = self.field.get_prime();
let square_count = (u32::BITS - characteristic.leading_zeros() - 1) as usize;
let multiply_count = characteristic.count_ones() as usize - 1;
square_count + multiply_count
}
/// Decide whether constructing and applying a Frobenius map costs no more
/// dense modular products than the next classical `p`th-power step.
fn should_cache_frobenius(&self, distinct_degree: usize, residual_degree: usize) -> bool {
if distinct_degree < DENSE_ZP_FROBENIUS_CLASSICAL_PREFIX {
return false;
}
let map_construction_products = residual_degree.saturating_sub(2);
map_construction_products.saturating_add(1) <= self.characteristic_power_product_count()
}
/// Materialize `value - x` for the GCD that extracts the next block.
fn polynomial_minus_x(
&self,
value: &[FiniteFieldElement<u32>],
) -> MultivariatePolynomial<Zp, E, LexOrder> {
let coefficient_count = value.len().max(2);
let mut polynomial = self.template.zero_with_capacity(coefficient_count);
let mut exponents = vec![E::zero(); self.template.nvars()];
for degree in 0..coefficient_count {
let mut coefficient = value
.get(degree)
.copied()
.unwrap_or_else(|| self.field.zero());
if degree == 1 {
self.field.sub_assign(&mut coefficient, &self.field.one());
}
if self.field.is_zero(&coefficient) {
continue;
}
exponents[self.variable] = E::from_u32(degree as u32);
polynomial.append_monomial_back(coefficient, &exponents);
}
polynomial
}
/// Compute all distinct-degree blocks, stopping as soon as an optional
/// factor-count limit is proven to be exceeded.
fn factor(
mut self,
max_factor_count: Option<usize>,
) -> Result<DistinctDegreeFactorization<MultivariatePolynomial<Zp, E, LexOrder>>, usize> {
let mut factors = Vec::new();
let mut factor_count = 0usize;
let mut h = vec![self.field.zero(), self.field.one()];
let mut f = self.template.clone();
let mut distinct_degree = 0usize;
let mut x_to_p = None;
let mut frobenius: Option<DenseZpFrobeniusContext> = None;
if max_factor_count.is_some_and(|limit| limit == 0) {
#[cfg(test)]
{
BOUNDED_DDF_REJECTIONS.with(|rejections| rejections.set(rejections.get() + 1));
LAST_BOUNDED_DDF_REJECTION_DEGREE.with(|degree| degree.set(0));
}
return Err(1);
}
while !f.is_one() {
distinct_degree += 1;
if let Some(context) = &mut frobenius {
context.apply::<E>(&self.field, &h, &mut self.multiplication_output);
std::mem::swap(&mut h, &mut self.multiplication_output);
Self::remainder_monic(&self.field, &mut h, &self.modulus);
} else {
self.raise_to_characteristic(&mut h);
if distinct_degree == 1 {
x_to_p = Some(h.clone());
}
}
let g = f.univariate_gcd(&self.polynomial_minus_x(&h));
let removed_block = !g.is_one();
if removed_block {
f = f.quot_rem_univariate_monic(&g).0;
let block_degree = g.degree(self.variable).to_u32() as usize;
debug_assert_eq!(block_degree % distinct_degree, 0);
factor_count += block_degree / distinct_degree;
factors.push((distinct_degree, g));
}
let factor_count_lower_bound = factor_count + usize::from(!f.is_constant());
if max_factor_count.is_some_and(|limit| factor_count_lower_bound > limit) {
#[cfg(test)]
{
BOUNDED_DDF_REJECTIONS.with(|rejections| rejections.set(rejections.get() + 1));
LAST_BOUNDED_DDF_REJECTION_DEGREE.with(|degree| degree.set(distinct_degree));
}
return Err(factor_count_lower_bound);
}
let residual_degree = f.last_exponents()[self.variable].to_u32() as usize;
if residual_degree < 2 * (distinct_degree + 1) {
if !f.is_constant() {
factor_count += 1;
factors.push((residual_degree, f));
}
break;
}
if removed_block {
self.set_modulus(&f);
}
if frobenius.is_none() && self.should_cache_frobenius(distinct_degree, residual_degree)
{
Self::remainder_monic(&self.field, &mut h, &self.modulus);
let mut generator = x_to_p
.take()
.expect("the first Frobenius power precedes the cached DDF map");
Self::remainder_monic(&self.field, &mut generator, &self.modulus);
frobenius = Some(DenseZpFrobeniusContext::new::<E>(
&self.field,
&self.modulus,
&self.reverse_modulus_inverse,
&generator,
&mut self.multiplication_workspace,
));
}
}
Ok(DistinctDegreeFactorization {
blocks: factors,
factor_count,
})
}
}
/// Dense arithmetic for odd-characteristic Cantor-Zassenhaus equal-degree
/// factorization over `Zp`.
///
/// A split attempt samples one dense residue, checks it for an immediate GCD,
/// and otherwise raises it to `(p^d - 1) / 2` modulo the current block. The
/// modulus reciprocal and all product buffers are retained across the binary
/// powering steps. Polynomials are materialized only for the GCDs that can
/// produce a factor.
struct DenseZpEqualDegreeContext<'a, E: PositiveExponent> {
field: Zp,
variable: usize,
template: &'a MultivariatePolynomial<Zp, E, LexOrder>,
modulus: Vec<FiniteFieldElement<u32>>,
reverse_modulus_inverse: Vec<FiniteFieldElement<u32>>,
exponent_bits: Vec<bool>,
random_residue: Vec<FiniteFieldElement<u32>>,
power: Vec<FiniteFieldElement<u32>>,
result: Vec<FiniteFieldElement<u32>>,
multiplication_output: Vec<FiniteFieldElement<u32>>,
multiplication_workspace: DenseZpMulModWorkspace,
}
impl<'a, E: PositiveExponent> DenseZpEqualDegreeContext<'a, E> {
/// Prepare a bounded dense workspace for an equal-degree block.
///
/// The same density and degree limits as dense distinct-degree
/// factorization keep allocation proportional to the sparse input size.
/// `Zp` represents odd prime fields; characteristic two uses the separate
/// `Z2` type and remains on the generic trace-based implementation.
fn new(
polynomial: &'a MultivariatePolynomial<Zp, E, LexOrder>,
variable: usize,
equal_degree: usize,
) -> Option<Self> {
let DenseZpDistinctDegreeContext {
field,
variable,
template,
modulus,
reverse_modulus_inverse,
power,
multiplication_output,
multiplication_workspace,
} = DenseZpDistinctDegreeContext::new(polynomial, variable)?;
let degree = modulus.len() - 1;
if equal_degree == 0 || degree <= equal_degree || !degree.is_multiple_of(equal_degree) {
return None;
}
let characteristic = field.size().unwrap();
let mut exponent =
(&characteristic.pow(equal_degree as u64) - &Integer::one()) / &Integer::from(2);
let mut exponent_bits = Vec::new();
while !exponent.is_zero() {
exponent_bits.push((&exponent % &Integer::Single(2)).is_one());
exponent /= 2;
}
debug_assert!(!exponent_bits.is_empty());
Some(Self {
field,
variable,
template,
modulus,
reverse_modulus_inverse,
exponent_bits,
random_residue: Vec::with_capacity(degree),
power,
result: Vec::with_capacity(degree),
multiplication_output,
multiplication_workspace,
})
}
/// Convert ascending dense coefficients into a sparse polynomial for a
/// univariate GCD.
fn polynomial(
&self,
coefficients: &[FiniteFieldElement<u32>],
) -> MultivariatePolynomial<Zp, E, LexOrder> {
let mut polynomial = self.template.zero_with_capacity(coefficients.len());
let mut exponents = vec![E::zero(); self.template.nvars()];
for (degree, coefficient) in coefficients.iter().enumerate() {
if self.field.is_zero(coefficient) {
continue;
}
exponents[self.variable] = E::from_u32(degree as u32);
polynomial.append_monomial_back(*coefficient, &exponents);
}
polynomial
}
/// Raise `base` to the cached Cantor-Zassenhaus exponent modulo the block.
///
/// The first set exponent bit copies the corresponding power into the
/// result, avoiding a polynomial multiplication by one.
fn power_to_half_group_order(&mut self, base: &[FiniteFieldElement<u32>]) {
self.power.clear();
self.power.extend_from_slice(base);
self.result.clear();
let mut result_initialized = false;
for (bit_index, bit) in self.exponent_bits.iter().copied().enumerate() {
if bit {
if result_initialized {
DenseZpDistinctDegreeContext::<E>::multiply_mod_into(
&self.field,
&self.modulus,
&self.reverse_modulus_inverse,
&self.result,
&self.power,
&mut self.multiplication_output,
&mut self.multiplication_workspace,
);
std::mem::swap(&mut self.result, &mut self.multiplication_output);
} else {
self.result.extend_from_slice(&self.power);
result_initialized = true;
}
}
if bit_index + 1 < self.exponent_bits.len() {
DenseZpDistinctDegreeContext::<E>::square_mod_into(
&self.field,
&self.modulus,
&self.reverse_modulus_inverse,
&self.power,
&mut self.multiplication_output,
&mut self.multiplication_workspace,
);
std::mem::swap(&mut self.power, &mut self.multiplication_output);
}
}
debug_assert!(result_initialized);
}
/// Find one proper factor of the current equal-degree block.
fn split<R: Rng + ?Sized>(&mut self, rng: &mut R) -> MultivariatePolynomial<Zp, E, LexOrder> {
let degree = self.modulus.len() - 1;
let sampling_range = 0..=i64::from(self.field.get_prime() - 1);
loop {
self.random_residue.clear();
self.random_residue
.extend((0..degree).map(|_| self.field.sample(rng, &sampling_range)));
DenseZpDistinctDegreeContext::<E>::trim(&self.field, &mut self.random_residue);
if self.random_residue.len() <= 1 {
continue;
}
*self.random_residue.last_mut().unwrap() = self.field.one();
let random_polynomial = self.polynomial(&self.random_residue);
let gcd = random_polynomial.univariate_gcd(self.template);
if !gcd.is_one() {
return gcd;
}
let random_residue = std::mem::take(&mut self.random_residue);
self.power_to_half_group_order(&random_residue);
self.random_residue = random_residue;
if self.result.is_empty() {
self.result.push(self.field.zero());
}
self.field
.sub_assign(&mut self.result[0], &self.field.one());
DenseZpDistinctDegreeContext::<E>::trim(&self.field, &mut self.result);
if self.result.len() <= 1 {
continue;
}
let gcd = self.polynomial(&self.result).univariate_gcd(self.template);
if !gcd.is_one() && &gcd != self.template {
return gcd;
}
}
}
/// Split a dense block into its monic irreducible factors of degree `d`.
///
/// Recursive factors that become too sparse for bounded dense storage use
/// the generic polynomial implementation.
fn factor(
polynomial: &MultivariatePolynomial<Zp, E, LexOrder>,
variable: usize,
d: usize,
) -> Option<Vec<MultivariatePolynomial<Zp, E, LexOrder>>> {
let polynomial = polynomial.clone().make_monic();
let degree = polynomial.degree(variable).to_u32() as usize;
if d == 0 || degree < d || !degree.is_multiple_of(d) {
return None;
}
if degree == d {
return Some(vec![polynomial]);
}
let mut rng = rng();
let mut pending = vec![polynomial];
let mut factors = Vec::new();
let mut first_block = true;
while let Some(block) = pending.pop() {
if block.degree(variable).to_u32() as usize == d {
factors.push(block);
continue;
}
let factor =
if let Some(mut context) = DenseZpEqualDegreeContext::new(&block, variable, d) {
#[cfg(test)]
if first_block {
DENSE_ZP_EDF_BLOCKS.with(|blocks| blocks.set(blocks.get() + 1));
}
context.split(&mut rng)
} else if first_block {
return None;
} else {
factors.extend(block.equal_degree_factorization(d));
continue;
};
first_block = false;
let (cofactor, remainder) = block.quot_rem_univariate_monic(&factor);
debug_assert!(remainder.is_zero());
// The stack processes the factor subtree before the cofactor subtree, matching the
// recursive generic algorithm's factor order for downstream Hensel tie-breaking.
pending.push(cofactor);
pending.push(factor);
}
Some(factors)
}
}
/// A suitable finite-field image whose equal-degree factorization has been deferred.
struct ModularIntegerFactorization<E: PositiveExponent> {
field: Zp,
variable: usize,
distinct_degree: DistinctDegreeFactorization<MultivariatePolynomial<Zp, E, LexOrder>>,
}
/// Result of screening a degree-preserving, square-free finite-field image
/// with nonzero leading and constant coefficients.
enum ModularPrimeScreen<E: PositiveExponent> {
Candidate(ModularIntegerFactorization<E>),
FactorLimitExceeded { lower_bound: usize },
}
/// Identifies either an input modular factor or a product built earlier in a
/// univariate Hensel product tree.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
enum UnivariateHenselProductTreeLink {
Leaf(usize),
Internal(usize),
}
/// Either lifted modular leaves at the requested precision or exact factors
/// certified before that precision was reached.
enum UnivariateHenselProductTreeLiftResult<P> {
Lifted(Vec<P>),
Exact(Vec<P>),
}
/// An exact two-part decomposition certified from two lifted leaves and its
/// complementary leaves.
///
/// Each part retains the modular leaves that reduce to it. Once the current
/// modulus covers both local coefficient bounds, those two groups can be
/// recombined independently without lifting to the original polynomial's
/// larger global bound.
struct UnivariateHenselExactPartition<P> {
exact_parts: [P; 2],
leaf_indices: [Vec<usize>; 2],
coefficient_bounds: [Integer; 2],
required_exponent: usize,
}
/// Records one product of two coprime child factors.
///
/// Internal links address earlier entries in the topology's bottom-up node
/// array, and leaf links address the original modular-factor slice.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct UnivariateHenselProductTreeNode {
children: [UnivariateHenselProductTreeLink; 2],
degree: usize,
}
/// Connectivity and degree metadata for simultaneous univariate Hensel
/// lifting.
///
/// Nodes are stored in merge order, so multiplying their children from the
/// start of `nodes` toward the root makes every referenced child available.
#[derive(Clone, Debug, Eq, PartialEq)]
struct UnivariateHenselProductTreeTopology {
leaf_degrees: Vec<usize>,
nodes: Vec<UnivariateHenselProductTreeNode>,
root: Option<UnivariateHenselProductTreeLink>,
}
impl UnivariateHenselProductTreeTopology {
/// Returns the degree represented by a leaf or internal product link.
fn degree(&self, link: UnivariateHenselProductTreeLink) -> usize {
match link {
UnivariateHenselProductTreeLink::Leaf(index) => self.leaf_degrees[index],
UnivariateHenselProductTreeLink::Internal(index) => self.nodes[index].degree,
}
}
/// Selects the pair of leaves whose product is closest in degree to its
/// complement, with input indices resolving equal-degree choices.
fn most_balanced_leaf_pair(&self) -> Option<[usize; 2]> {
let total_degree = self
.leaf_degrees
.iter()
.try_fold(0usize, |total, degree| total.checked_add(*degree))?;
(0..self.leaf_degrees.len())
.flat_map(|left| (left + 1..self.leaf_degrees.len()).map(move |right| [left, right]))
.min_by_key(|[left, right]| {
let pair_degree = self.leaf_degrees[*left]
.checked_add(self.leaf_degrees[*right])
.expect("Hensel factor degrees overflow");
(
pair_degree.abs_diff(total_degree - pair_degree),
*left,
*right,
)
})
}
/// Selects a two-leaf split that is more degree-balanced than the current
/// internal/internal root split.
fn balanced_leaf_pair_improving_root(&self) -> Option<[usize; 2]> {
let root = match self.root? {
UnivariateHenselProductTreeLink::Internal(index) => self.nodes[index],
UnivariateHenselProductTreeLink::Leaf(_) => return None,
};
if root
.children
.iter()
.any(|child| matches!(child, UnivariateHenselProductTreeLink::Leaf(_)))
{
return None;
}
let root_imbalance = self
.degree(root.children[0])
.abs_diff(self.degree(root.children[1]));
let pair = self.most_balanced_leaf_pair()?;
let pair_degree = self.leaf_degrees[pair[0]]
.checked_add(self.leaf_degrees[pair[1]])
.expect("Hensel factor degrees overflow");
let total_degree = self
.degree(root.children[0])
.checked_add(self.degree(root.children[1]))
.expect("Hensel factor degrees overflow");
(pair_degree.abs_diff(total_degree - pair_degree) < root_imbalance).then_some(pair)
}
/// Visits internal products from the root toward the leaves, yielding each
/// node before every internal child it references.
#[cfg(test)]
fn internal_nodes_top_down(
&self,
) -> impl Iterator<Item = (usize, &UnivariateHenselProductTreeNode)> {
self.nodes.iter().enumerate().rev()
}
/// Returns the original modular-factor indices covered by the root in
/// input order.
fn leaf_indices_in_input_order(&self) -> Vec<usize> {
fn collect(
topology: &UnivariateHenselProductTreeTopology,
link: UnivariateHenselProductTreeLink,
indices: &mut Vec<usize>,
) {
match link {
UnivariateHenselProductTreeLink::Leaf(index) => indices.push(index),
UnivariateHenselProductTreeLink::Internal(index) => {
for child in topology.nodes[index].children {
collect(topology, child, indices);
}
}
}
}
let mut indices = Vec::with_capacity(self.leaf_degrees.len());
if let Some(root) = self.root {
collect(self, root, &mut indices);
}
indices.sort_unstable();
indices
}
/// Returns the input-factor indices represented by one tree link.
fn leaf_indices_below(&self, link: UnivariateHenselProductTreeLink) -> Vec<usize> {
fn collect(
topology: &UnivariateHenselProductTreeTopology,
link: UnivariateHenselProductTreeLink,
indices: &mut Vec<usize>,
) {
match link {
UnivariateHenselProductTreeLink::Leaf(index) => indices.push(index),
UnivariateHenselProductTreeLink::Internal(index) => {
for child in topology.nodes[index].children {
collect(topology, child, indices);
}
}
}
}
let mut indices = Vec::new();
collect(self, link, &mut indices);
indices.sort_unstable();
indices
}
}
/// One product waiting to be paired while constructing a degree-greedy tree.
/// The insertion index resolves equal-degree choices reproducibly.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
struct PendingUnivariateHenselProduct {
degree: usize,
insertion_index: usize,
link: UnivariateHenselProductTreeLink,
}
/// Reusable priority-queue storage for constructing univariate Hensel product
/// trees from modular-factor degrees.
#[derive(Default)]
struct UnivariateHenselProductTreeBuildContext {
pending: BinaryHeap<Reverse<PendingUnivariateHenselProduct>>,
}
impl UnivariateHenselProductTreeBuildContext {
/// Repeatedly merges the two smallest pending degrees and returns the
/// resulting product-tree topology.
///
/// Every input degree must be positive. Equal-degree leaves retain their
/// input order, while an internal product is ordered after all products
/// that were already pending when it was created.
fn build(&mut self, leaf_degrees: &[usize]) -> UnivariateHenselProductTreeTopology {
assert!(
leaf_degrees.iter().all(|degree| *degree > 0),
"Hensel product-tree leaves must be nonconstant"
);
self.pending.clear();
self.pending.reserve(leaf_degrees.len());
for (index, °ree) in leaf_degrees.iter().enumerate() {
self.pending.push(Reverse(PendingUnivariateHenselProduct {
degree,
insertion_index: index,
link: UnivariateHenselProductTreeLink::Leaf(index),
}));
}
let mut nodes = Vec::with_capacity(leaf_degrees.len().saturating_sub(1));
while self.pending.len() > 1 {
let Reverse(left) = self.pending.pop().unwrap();
let Reverse(right) = self.pending.pop().unwrap();
let degree = left
.degree
.checked_add(right.degree)
.expect("Hensel product-tree degree overflow");
let index = nodes.len();
nodes.push(UnivariateHenselProductTreeNode {
children: [left.link, right.link],
degree,
});
self.pending.push(Reverse(PendingUnivariateHenselProduct {
degree,
insertion_index: leaf_degrees.len() + index,
link: UnivariateHenselProductTreeLink::Internal(index),
}));
}
let root = self.pending.pop().map(|Reverse(product)| product.link);
UnivariateHenselProductTreeTopology {
leaf_degrees: leaf_degrees.to_vec(),
nodes,
root,
}
}
}
/// Returns the prime-power exponents used by synchronized Hensel lifting.
///
/// The schedule starts at the base-prime precision and reaches `target` by
/// repeatedly increasing the current exponent by at most that exponent. This
/// makes every correction modulus divide the already known modulus.
fn univariate_hensel_precision_schedule(target: usize) -> Vec<usize> {
assert!(
target > 0,
"Hensel precision must contain a base-prime digit"
);
let mut schedule = vec![target];
while *schedule.last().unwrap() > 1 {
schedule.push(schedule.last().unwrap().div_ceil(2));
}
schedule.reverse();
schedule
}
/// Returns a smaller terminal exponent when a certified local decomposition
/// removes at least half of the remaining global precision work.
fn univariate_hensel_shortened_target(
current: usize,
required: usize,
global: usize,
) -> Option<usize> {
if current >= global || required <= current || required >= global {
return None;
}
let local_work = required - current;
let global_work = global - current;
(local_work.saturating_mul(2) <= global_work).then_some(required)
}
/// Orders three modular factors so the recursive Hensel root has the smallest
/// possible degree imbalance.
///
/// Three-factor reconstruction isolates the first factor from the other two.
/// Moving the factor closest to half of the total degree to the front reduces
/// the root correction degrees and can expose an exact two-factor split before
/// the remaining child is lifted at its local coefficient bound.
fn balance_three_factor_hensel_root<R: Ring, E: PositiveExponent>(
factors: &mut [MultivariatePolynomial<R, E, LexOrder>],
variable: usize,
) {
assert_eq!(factors.len(), 3);
let degrees = factors
.iter()
.map(|factor| factor.degree(variable).to_u32() as usize)
.collect::<Vec<_>>();
let total_degree = degrees
.iter()
.try_fold(0usize, |total, degree| total.checked_add(*degree))
.expect("Hensel factor degrees overflow");
let isolated_factor = degrees
.iter()
.enumerate()
.min_by_key(|(index, degree)| (degree.abs_diff(total_degree - **degree), *index))
.unwrap()
.0;
factors.swap(0, isolated_factor);
}
type DenseIntegerUnivariatePolynomial = Vec<Integer>;
/// Mutable factors, subtree products, and Bezout cofactors for one
/// synchronized univariate Hensel lift.
///
/// A node with children `u` and `w` stores cofactors `s` and `t` satisfying
/// `s*u + t*w = 1` at the current prime-power precision. During a top-down
/// lifting stage, the node's product is the target written by its parent. All
/// stored coefficients are the canonical nonnegative representatives at the
/// current precision.
struct UnivariateHenselProductTreeLiftContext {
topology: UnivariateHenselProductTreeTopology,
leaves: Vec<DenseIntegerUnivariatePolynomial>,
internal_products: Vec<DenseIntegerUnivariatePolynomial>,
bezout_cofactors: Vec<[DenseIntegerUnivariatePolynomial; 2]>,
}
impl UnivariateHenselProductTreeLiftContext {
/// Builds all subtree products and Bezout relations over the base field,
/// then stores their canonical nonnegative integer coefficients in
/// ascending degree order.
fn new<UField: FiniteFieldWorkspace, E: PositiveExponent>(
factors: &[MultivariatePolynomial<FiniteField<UField>, E, LexOrder>],
) -> Self
where
FiniteField<UField>: Field + PolynomialGCD<E> + FiniteFieldCore<UField>,
{
assert!(factors.len() >= 2);
let field = factors[0].ring().clone();
debug_assert!(factors.iter().all(|factor| factor.ring() == &field));
let modular_leaves = factors
.iter()
.cloned()
.map(MultivariatePolynomial::make_monic)
.collect::<Vec<_>>();
let leaf_degrees = modular_leaves
.iter()
.map(|factor| {
factor
.last_exponents()
.iter()
.copied()
.max()
.unwrap_or_else(E::zero)
.to_u32() as usize
})
.collect::<Vec<_>>();
let topology = UnivariateHenselProductTreeBuildContext::default().build(&leaf_degrees);
let mut modular_products = Vec::with_capacity(topology.nodes.len());
let mut modular_bezout = Vec::with_capacity(topology.nodes.len());
for node in &topology.nodes {
let value = |link| match link {
UnivariateHenselProductTreeLink::Leaf(index) => &modular_leaves[index],
UnivariateHenselProductTreeLink::Internal(index) => &modular_products[index],
};
let u = value(node.children[0]);
let w = value(node.children[1]);
let (gcd, s, t) = u.eea_univariate(w);
assert!(gcd.is_one(), "Hensel leaves must be pairwise coprime");
debug_assert!((&s * u + &t * w).is_one());
modular_products.push(u * w);
modular_bezout.push([s, t]);
}
let variable = modular_leaves[0]
.last_exponents()
.iter()
.position(|exponent| !exponent.is_zero())
.expect("a Hensel factor must be nonconstant");
let to_dense_integer =
|polynomial: &MultivariatePolynomial<FiniteField<UField>, E, LexOrder>| {
let degree = polynomial.degree(variable).to_u32() as usize;
let mut coefficients = vec![Integer::zero(); degree + 1];
for term in polynomial {
debug_assert!(
term.exponents
.iter()
.enumerate()
.all(|(index, exponent)| { index == variable || exponent.is_zero() })
);
coefficients[term.exponents[variable].to_u32() as usize] =
field.to_integer(term.coefficient);
}
coefficients
};
Self {
topology,
leaves: modular_leaves.iter().map(&to_dense_integer).collect(),
internal_products: modular_products.iter().map(&to_dense_integer).collect(),
bezout_cofactors: modular_bezout
.iter()
.map(|cofactors| {
[
to_dense_integer(&cofactors[0]),
to_dense_integer(&cofactors[1]),
]
})
.collect(),
}
}
/// Returns the current polynomial represented by a leaf or internal link.
fn value(&self, link: UnivariateHenselProductTreeLink) -> &[Integer] {
match link {
UnivariateHenselProductTreeLink::Leaf(index) => &self.leaves[index],
UnivariateHenselProductTreeLink::Internal(index) => &self.internal_products[index],
}
}
/// Replaces the current polynomial represented by a leaf or internal link.
fn set_value(
&mut self,
link: UnivariateHenselProductTreeLink,
value: DenseIntegerUnivariatePolynomial,
) {
match link {
UnivariateHenselProductTreeLink::Leaf(index) => self.leaves[index] = value,
UnivariateHenselProductTreeLink::Internal(index) => {
self.internal_products[index] = value
}
}
}
/// Removes and returns the polynomial represented by a leaf or internal
/// link, leaving an empty buffer in its place.
fn take_value(
&mut self,
link: UnivariateHenselProductTreeLink,
) -> DenseIntegerUnivariatePolynomial {
match link {
UnivariateHenselProductTreeLink::Leaf(index) => std::mem::take(&mut self.leaves[index]),
UnivariateHenselProductTreeLink::Internal(index) => {
std::mem::take(&mut self.internal_products[index])
}
}
}
}
#[cfg(test)]
std::thread_local! {
pub(crate) static LLL_RECOMBINATION_SUCCESSES: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static SEPARABLE_CONTENT_PAIR_PROBES: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static SEPARABLE_CONTENT_SINGLE_MONOMIAL_CERTIFICATES: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static SEPARABLE_CONTENT_PAIR_MONOMIAL_CERTIFICATES: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static SEPARABLE_CONTENT_NONTRIVIAL_MONOMIAL_FALLBACKS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static SEPARABLE_CONTENT_PAIR_REPLACEMENTS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static QUADRATIC_HENSEL_LIFT_CALLS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static QUADRATIC_HENSEL_NONUNIT_RETRIES: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static PRODUCT_TREE_HENSEL_LIFT_CALLS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static PRODUCT_TREE_EARLY_RECONSTRUCTION_ATTEMPTS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static PRODUCT_TREE_EARLY_RECONSTRUCTION_SUCCESSES: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static PRODUCT_TREE_EARLY_RECONSTRUCTION_EXPONENT: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static PRODUCT_TREE_LAST_BEZOUT_UPDATE_EXPONENT: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static PRODUCT_TREE_BALANCED_PAIR_ATTEMPTS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static PRODUCT_TREE_BALANCED_PAIR_CERTIFICATES: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static PRODUCT_TREE_BALANCED_PAIR_TARGET_EXPONENT: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static DENSE_INTEGER_I128_MULTIPLY_REMAINDERS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static GEOMETRIC_SMALL_PRIME_BACKFILLS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static COMPETITIVE_SMALL_PRIME_DDF_REJECTIONS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static MODULAR_INTEGER_EDF_CALLS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static DENSE_ZP_DDF_SCREENS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static DENSE_ZP_DDF_MODULUS_UPDATES: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static DENSE_ZP_FROBENIUS_MAP_BUILDS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static DENSE_ZP_EDF_BLOCKS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static BOUNDED_DDF_REJECTIONS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static LAST_BOUNDED_DDF_REJECTION_DEGREE: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static LAST_MODULAR_INTEGER_EDF_PRIME: std::cell::Cell<u32> = const {
std::cell::Cell::new(0)
};
static LAST_BIVARIATE_RECONSTRUCTION_PRIME: std::cell::Cell<u32> = const {
std::cell::Cell::new(0)
};
static EXACT_HENSEL_SUBTREE_SPLITS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static LOCAL_HENSEL_RECOMBINATION_NODES: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static BIVARIATE_SAMPLE_FACTORIZATIONS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static BIVARIATE_FIRST_SAMPLE_ACCEPTANCES: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static BIVARIATE_WANG_SAMPLE_TRIES: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static LINEAR_VARIABLE_IRREDUCIBILITY_CERTIFICATES: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static UNIVARIATE_IRREDUCIBILITY_SCOUTS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static UNIVARIATE_IRREDUCIBILITY_CERTIFICATES: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static EARLY_SEPARABLE_PAIR_PROBE_CALLS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static EARLY_SEPARABLE_PAIR_SPLITS: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
static EXACT_HENSEL_SUBTREE_MODULUS_BITS: std::cell::RefCell<Vec<u64>> = const {
std::cell::RefCell::new(Vec::new())
};
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum IntegerFactorStart {
Auto,
Univariate,
Bivariate,
Disabled,
}
enum ExactPolynomialSquareRoot<P> {
Root(P),
NotSquare,
}
/// An exact split found before the general separability and square-free pass.
enum EarlyIntegerFactorSplit<P> {
Quadratic { variable: usize, factors: [P; 2] },
Separable([P; 2]),
}
/// Finite-field powers reused by the early quadratic and coefficient-content
/// screens.
struct EarlyFactorModularProbeContext {
field: Zp,
powers: Vec<Vec<FiniteFieldElement<u32>>>,
inverse_powers: Vec<Vec<FiniteFieldElement<u32>>>,
degrees: Vec<usize>,
}
/// Data shared by the modular discriminant and coefficient-content screens for
/// one quadratic variable.
struct EarlyFactorModularImage {
quadratic_variable: usize,
base_retained_variable: Option<usize>,
term_values: Vec<FiniteFieldElement<u32>>,
layer_supports: Option<Vec<Vec<usize>>>,
}
enum EarlyQuadraticDiscriminantProbe {
Nonsquare(EarlyFactorModularImage),
Square(EarlyFactorModularImage),
}
impl EarlyFactorModularProbeContext {
/// Cache one deterministic finite-field coordinate power table for the
/// input degree box.
fn new(degrees: &[usize]) -> Self {
let field = Zp::new(EARLY_FACTOR_PROBE_PRIME);
let (powers, inverse_powers) = degrees
.iter()
.enumerate()
.map(|(variable, degree)| {
let coordinate = 3 + (variable as u32 + 1) * (variable as u32 + 3);
let coordinate = field.to_element(coordinate % EARLY_FACTOR_PROBE_PRIME);
let inverse_coordinate = field.inv(&coordinate);
let mut powers = Vec::with_capacity(degree + 1);
let mut inverse_powers = Vec::with_capacity(degree + 1);
powers.push(field.one());
inverse_powers.push(field.one());
for exponent in 1..=*degree {
powers.push(field.mul(&powers[exponent - 1], &coordinate));
inverse_powers
.push(field.mul(&inverse_powers[exponent - 1], &inverse_coordinate));
}
(powers, inverse_powers)
})
.unzip();
Self {
field,
powers,
inverse_powers,
degrees: degrees.to_vec(),
}
}
/// Build one finite-field specialization for a quadratic variable and
/// test its discriminant. The quadratic variable and one retained variable
/// are omitted from each cached term value, which evaluates both at one.
/// When requested, coefficient-layer support counts and these term values
/// are retained so coefficient-content probes can reuse the same integer
/// reductions and evaluations of every other variable.
#[inline(never)]
fn probe_quadratic_discriminant<E: PositiveExponent>(
&self,
polynomial: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
quadratic_variable: usize,
collect_content_data: bool,
) -> EarlyQuadraticDiscriminantProbe {
debug_assert_eq!(polynomial.degree(quadratic_variable).to_u32(), 2);
let retained_variable = self
.degrees
.iter()
.copied()
.enumerate()
.filter(|(variable, degree)| *variable != quadratic_variable && *degree > 0)
.max_by_key(|(variable, degree)| (*degree, Reverse(*variable)))
.map(|(variable, _)| variable);
let retained_length = retained_variable
.map(|variable| self.degrees[variable] + 1)
.unwrap_or(1);
let mut evaluated_layers: [Vec<FiniteFieldElement<u32>>; 3] =
std::array::from_fn(|_| vec![self.field.zero(); retained_length]);
let mut term_values = collect_content_data.then(|| Vec::with_capacity(polynomial.nterms()));
let mut layer_supports = collect_content_data.then(|| {
self.degrees
.iter()
.map(|degree| {
degree
.checked_add(1)
.filter(|count| *degree > 0 && *count <= 256)
.map_or_else(Vec::new, |count| vec![0usize; count])
})
.collect::<Vec<_>>()
});
for term in polynomial {
let layer = term.exponents[quadratic_variable].to_u32() as usize;
debug_assert!(layer <= 2);
let retained_degree = retained_variable
.map(|variable| term.exponents[variable].to_u32() as usize)
.unwrap_or(0);
let mut value = term.coefficient.to_finite_field(&self.field);
for (variable, exponent) in term.exponents.iter().enumerate() {
let exponent = exponent.to_u32() as usize;
if let Some(layer_supports) = &mut layer_supports {
if !layer_supports[variable].is_empty() {
layer_supports[variable][exponent] += 1;
}
}
if variable == quadratic_variable
|| Some(variable) == retained_variable
|| exponent == 0
{
continue;
}
self.field
.mul_assign(&mut value, &self.powers[variable][exponent]);
}
self.field
.add_assign(&mut evaluated_layers[layer][retained_degree], &value);
if let Some(term_values) = &mut term_values {
term_values.push(value);
}
}
let mut discriminant = self.dense_product(&evaluated_layers[1], &evaluated_layers[1]);
let product = self.dense_product(&evaluated_layers[2], &evaluated_layers[0]);
if discriminant.len() < product.len() {
discriminant.resize(product.len(), self.field.zero());
}
let four = self.field.to_element(4);
for (coefficient, product_coefficient) in discriminant.iter_mut().zip(product) {
let product_coefficient = self.field.mul(&product_coefficient, &four);
self.field.sub_assign(coefficient, &product_coefficient);
}
let modular_image = EarlyFactorModularImage {
quadratic_variable,
base_retained_variable: retained_variable,
term_values: term_values.unwrap_or_default(),
layer_supports,
};
if self.dense_polynomial_is_square(discriminant) {
EarlyQuadraticDiscriminantProbe::Square(modular_image)
} else {
EarlyQuadraticDiscriminantProbe::Nonsquare(modular_image)
}
}
fn trim(&self, polynomial: &mut Vec<FiniteFieldElement<u32>>) {
while polynomial
.last()
.is_some_and(|coefficient| self.field.is_zero(coefficient))
{
polynomial.pop();
}
}
/// Multiply two dense finite-field polynomials stored in increasing degree
/// order.
fn dense_product(
&self,
left: &[FiniteFieldElement<u32>],
right: &[FiniteFieldElement<u32>],
) -> Vec<FiniteFieldElement<u32>> {
if left.is_empty() || right.is_empty() {
return Vec::new();
}
let mut product = vec![self.field.zero(); left.len() + right.len() - 1];
for (left_degree, left_coefficient) in left.iter().enumerate() {
if self.field.is_zero(left_coefficient) {
continue;
}
for (right_degree, right_coefficient) in right.iter().enumerate() {
if self.field.is_zero(right_coefficient) {
continue;
}
let term = self.field.mul(left_coefficient, right_coefficient);
self.field
.add_assign(&mut product[left_degree + right_degree], &term);
}
}
self.trim(&mut product);
product
}
/// Return whether a dense polynomial is a square over the probe field.
/// The leading coefficient determines the root recursively; a final square
/// verifies every coefficient.
fn dense_polynomial_is_square(&self, mut polynomial: Vec<FiniteFieldElement<u32>>) -> bool {
self.trim(&mut polynomial);
if polynomial.is_empty() {
return true;
}
let degree = polynomial.len() - 1;
if !degree.is_multiple_of(2) {
return false;
}
let root_degree = degree / 2;
let leading_root = self.field.pow(
polynomial.last().unwrap(),
((EARLY_FACTOR_PROBE_PRIME + 1) / 4) as u64,
);
if self.field.mul(&leading_root, &leading_root) != *polynomial.last().unwrap() {
return false;
}
let mut root = vec![self.field.zero(); root_degree + 1];
root[root_degree] = leading_root;
let denominator = self
.field
.mul(&self.field.to_element(2), &root[root_degree]);
for root_index in (0..root_degree).rev() {
let coefficient_degree = root_degree + root_index;
let mut residual = polynomial[coefficient_degree];
for left_index in root_index + 1..=root_degree {
let right_index = coefficient_degree - left_index;
if right_index <= root_index || right_index > root_degree {
continue;
}
let product = self.field.mul(&root[left_index], &root[right_index]);
self.field.sub_assign(&mut residual, &product);
}
root[root_index] = self.field.div(&residual, &denominator);
}
self.dense_product(&root, &root) == polynomial
}
fn dense_remainder(
&self,
mut dividend: Vec<FiniteFieldElement<u32>>,
divisor: &[FiniteFieldElement<u32>],
) -> Vec<FiniteFieldElement<u32>> {
debug_assert!(!divisor.is_empty());
let inverse_leading = self.field.inv(divisor.last().unwrap());
while dividend.len() >= divisor.len() {
let shift = dividend.len() - divisor.len();
let quotient = self.field.mul(dividend.last().unwrap(), &inverse_leading);
for (degree, coefficient) in divisor.iter().enumerate() {
let product = self.field.mul("ient, coefficient);
self.field
.sub_assign(&mut dividend[shift + degree], &product);
}
self.trim(&mut dividend);
}
dividend
}
/// Construct univariate finite-field images of two coefficient layers from
/// the cached quadratic specialization. Variables omitted from the cache
/// are evaluated at one; every other non-retained variable uses its cached
/// deterministic coordinate.
fn coefficient_pair_images<E: PositiveExponent>(
&self,
polynomial: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
coefficient_variable: usize,
coefficient_degrees: [u32; 2],
modular_image: &EarlyFactorModularImage,
) -> Option<[Vec<FiniteFieldElement<u32>>; 2]> {
let Some(retained_variable) = self
.degrees
.iter()
.copied()
.enumerate()
.filter(|(variable, degree)| *variable != coefficient_variable && *degree > 0)
.max_by_key(|(variable, degree)| (*degree, Reverse(*variable)))
.map(|(variable, _)| variable)
else {
return None;
};
if modular_image.term_values.len() != polynomial.nterms() {
return None;
}
let mut images: [Vec<FiniteFieldElement<u32>>; 2] =
std::array::from_fn(|_| vec![self.field.zero(); self.degrees[retained_variable] + 1]);
for (term, cached_value) in polynomial.into_iter().zip(&modular_image.term_values) {
let coefficient_degree = term.exponents[coefficient_variable].to_u32();
let Some(image_index) = coefficient_degrees
.iter()
.position(|degree| *degree == coefficient_degree)
else {
continue;
};
let mut value = *cached_value;
for variable in [coefficient_variable, retained_variable] {
if variable == modular_image.quadratic_variable
|| Some(variable) == modular_image.base_retained_variable
{
continue;
}
let exponent = term.exponents[variable].to_u32() as usize;
if exponent != 0 {
self.field
.mul_assign(&mut value, &self.inverse_powers[variable][exponent]);
}
}
let retained_degree = term.exponents[retained_variable].to_u32() as usize;
self.field
.add_assign(&mut images[image_index][retained_degree], &value);
}
for image in &mut images {
self.trim(image);
}
Some(images)
}
/// Return false when the selected specialization has coprime coefficient
/// layers. Such a specialization is sufficient to skip this optional
/// early content scout; returning true asks the caller to compute the exact
/// multivariate GCD of the layers.
fn coefficient_pair_may_have_common_factor<E: PositiveExponent>(
&self,
polynomial: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
coefficient_variable: usize,
coefficient_degrees: [u32; 2],
modular_image: &EarlyFactorModularImage,
) -> bool {
let Some(images) = self.coefficient_pair_images(
polynomial,
coefficient_variable,
coefficient_degrees,
modular_image,
) else {
return true;
};
if images.iter().any(Vec::is_empty) {
return true;
}
let [mut left, mut right] = images;
while !right.is_empty() {
let remainder = self.dense_remainder(left, &right);
left = right;
right = remainder;
}
left.len() > 1
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum UnivariateSpecializationFactorization {
Irreducible,
ReducibleImage,
Inconclusive,
}
/// Accumulates the square of a sparse integer polynomial by visiting each
/// unordered term pair once. Diagonal and off-diagonal products are retained
/// separately so every coefficient product is computed once before the latter
/// sum is doubled.
struct PackedSparsePolynomialSquareContext<E> {
input_monomials: Vec<u64>,
coefficients: HashMap<u64, [Integer; 2]>,
exponents: Vec<E>,
}
impl<E: PositiveExponent> PackedSparsePolynomialSquareContext<E> {
fn new(nvars: usize, input_terms: usize) -> Self {
Self {
input_monomials: Vec::with_capacity(input_terms),
coefficients: HashMap::default(),
exponents: vec![E::zero(); nvars],
}
}
/// Square a polynomial whose exponent sums fit in eight packed bytes and
/// whose sparse pair workload is bounded. Dense layouts return `None` so
/// the established dense multiplication kernels remain in use.
fn try_square(
mut self,
polynomial: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
) -> Option<MultivariatePolynomial<IntegerRing, E, LexOrder>> {
if polynomial.nterms() < MIN_PACKED_SPARSE_SQUARE_TERMS
|| polynomial.nvars() > 8
|| !polynomial.is_polynomial()
|| polynomial.total_degree_dense_mul_is_bounded(polynomial)
{
return None;
}
let mut output_length = Some(1usize);
for variable in 0..polynomial.nvars() {
let degree = polynomial.degree(variable).to_u32();
if degree > u8::MAX as u32 / 2 {
return None;
}
output_length =
output_length.and_then(|length| length.checked_mul(2 * degree as usize + 1));
}
if output_length.is_some_and(|length| {
super::polynomial::mixed_radix_dense_mul_is_bounded(
length,
polynomial.nterms(),
polynomial.nterms(),
)
}) {
return None;
}
let pair_count = polynomial
.nterms()
.checked_mul(polynomial.nterms().checked_add(1)?)?
.checked_div(2)?;
if pair_count > MAX_PACKED_SPARSE_SQUARE_PAIRS
|| self.coefficients.try_reserve(pair_count).is_err()
{
return None;
}
self.input_monomials
.extend(polynomial.exponents_iter().map(E::pack));
for left in 0..polynomial.nterms() {
for right in left..polynomial.nterms() {
let monomial = self.input_monomials[left] + self.input_monomials[right];
let coefficients = self
.coefficients
.entry(monomial)
.or_insert_with(|| std::array::from_fn(|_| Integer::zero()));
let accumulator = usize::from(left != right);
Z.add_mul_assign(
&mut coefficients[accumulator],
&polynomial.coefficients[left],
&polynomial.coefficients[right],
);
}
}
let mut terms = Vec::with_capacity(self.coefficients.len());
for (monomial, [diagonal, mut off_diagonal]) in self.coefficients {
off_diagonal *= 2;
let coefficient = diagonal + off_diagonal;
if !coefficient.is_zero() {
terms.push((monomial, coefficient));
}
}
terms.sort_unstable_by_key(|(monomial, _)| *monomial);
let mut result = polynomial.zero_with_capacity(terms.len());
for (monomial, coefficient) in terms {
E::unpack(monomial, &mut self.exponents);
result.append_monomial_back(coefficient, &self.exponents);
}
Some(result)
}
}
/// Workspace for reconstructing an exact sparse square root in decreasing
/// lexicographic order. The heap merges products of recovered nonleading root
/// terms with the input. Any unmatched leading coefficient determines the next
/// root term after exact division by twice the root's leading coefficient.
struct SparsePolynomialSquareRootContext<E> {
input_monomials: Vec<u64>,
root_monomials: Vec<u64>,
root_exponents: Vec<E>,
maximum_root_exponents: Vec<E>,
root_coefficients: Vec<Integer>,
pending_products: HashMap<u64, Integer>,
product_heap: BinaryHeap<u64>,
product_count: usize,
current_exponents: Vec<E>,
next_root_exponents: Vec<E>,
}
impl<E: PositiveExponent> SparsePolynomialSquareRootContext<E> {
fn new(nvars: usize, input_terms: usize) -> Self {
Self {
input_monomials: Vec::with_capacity(input_terms),
root_monomials: Vec::new(),
root_exponents: Vec::new(),
maximum_root_exponents: vec![E::zero(); nvars],
root_coefficients: Vec::new(),
pending_products: HashMap::default(),
product_heap: BinaryHeap::new(),
product_count: 0,
current_exponents: vec![E::zero(); nvars],
next_root_exponents: vec![E::zero(); nvars],
}
}
/// Accumulate one known product of recovered root terms. Off-diagonal
/// products occur twice in the square.
fn accumulate_product(&mut self, left: usize, right: usize, doubled: bool) -> bool {
#[cfg(debug_assertions)]
{
let nvars = self.current_exponents.len();
debug_assert!(
self.root_exponents[left * nvars..(left + 1) * nvars]
.iter()
.zip(&self.root_exponents[right * nvars..(right + 1) * nvars])
.all(|(left, right)| { left.to_i32() + right.to_i32() <= u8::MAX as i32 })
);
}
let monomial = self.root_monomials[left] + self.root_monomials[right];
let coefficient = match self.pending_products.entry(monomial) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if self.product_heap.len() >= MAX_SPARSE_SQUARE_ROOT_PENDING_PRODUCTS {
return false;
}
self.product_heap.push(monomial);
entry.insert(Integer::zero())
}
};
Z.add_mul_assign(
coefficient,
&self.root_coefficients[left],
&self.root_coefficients[right],
);
if doubled {
Z.add_mul_assign(
coefficient,
&self.root_coefficients[left],
&self.root_coefficients[right],
);
}
true
}
/// Recover the exact square root of a polynomial whose nonnegative
/// exponents fit in eight packed bytes. `None` requests the general
/// square-free-decomposition fallback for an unsupported size or layout.
fn reconstruct(
mut self,
polynomial: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
) -> Option<ExactPolynomialSquareRoot<MultivariatePolynomial<IntegerRing, E, LexOrder>>> {
if polynomial.is_zero() {
return Some(ExactPolynomialSquareRoot::Root(polynomial.zero()));
}
if polynomial.nvars() > 8 || !polynomial.is_polynomial() {
return None;
}
for (var, maximum_root_exponent) in self.maximum_root_exponents.iter_mut().enumerate() {
let degree = polynomial.degree(var).to_u32();
if degree > u8::MAX as u32 {
return None;
}
if degree % 2 != 0 {
return Some(ExactPolynomialSquareRoot::NotSquare);
}
*maximum_root_exponent = E::from_u32(degree / 2);
}
self.input_monomials
.extend(polynomial.exponents_iter().map(E::pack));
let leading_coefficient = polynomial.lcoeff();
if leading_coefficient.is_negative() {
return Some(ExactPolynomialSquareRoot::NotSquare);
}
let root_leading_coefficient = leading_coefficient.root(2);
if &root_leading_coefficient * &root_leading_coefficient != leading_coefficient {
return Some(ExactPolynomialSquareRoot::NotSquare);
}
for (root_exponent, exponent) in self
.next_root_exponents
.iter_mut()
.zip(polynomial.last_exponents())
{
if exponent.to_i32() % 2 != 0 {
return Some(ExactPolynomialSquareRoot::NotSquare);
}
*root_exponent = E::from_i32(exponent.to_i32() / 2);
}
self.root_monomials.push(E::pack(&self.next_root_exponents));
self.root_exponents
.extend_from_slice(&self.next_root_exponents);
self.root_coefficients.push(root_leading_coefficient);
let denominator = &self.root_coefficients[0] * &Integer::from(2);
let leading_exponents = self.next_root_exponents.clone();
let mut input_length = polynomial.nterms() - 1;
while input_length > 0 || !self.product_heap.is_empty() {
let input_monomial = input_length
.checked_sub(1)
.map(|index| self.input_monomials[index]);
let product_monomial = self.product_heap.peek().copied();
let monomial = match (input_monomial, product_monomial) {
(Some(input), Some(product)) => input.max(product),
(Some(input), None) => input,
(None, Some(product)) => product,
(None, None) => break,
};
let mut residual = if input_monomial == Some(monomial) {
input_length -= 1;
polynomial.coefficients[input_length].clone()
} else {
Integer::zero()
};
if product_monomial == Some(monomial) {
self.product_heap.pop();
residual -= &self.pending_products.remove(&monomial).unwrap();
}
if residual.is_zero() {
continue;
}
E::unpack(monomial, &mut self.current_exponents);
for ((next, current), leading) in self
.next_root_exponents
.iter_mut()
.zip(&self.current_exponents)
.zip(&leading_exponents)
{
let exponent = current.to_i32() - leading.to_i32();
if exponent < 0 {
return Some(ExactPolynomialSquareRoot::NotSquare);
}
*next = E::from_i32(exponent);
}
if self
.next_root_exponents
.iter()
.zip(&self.maximum_root_exponents)
.any(|(exponent, maximum)| exponent > maximum)
{
return Some(ExactPolynomialSquareRoot::NotSquare);
}
let nvars = polynomial.nvars();
let previous_exponents = &self.root_exponents[self.root_exponents.len() - nvars..];
if self.next_root_exponents.as_slice() >= previous_exponents {
return Some(ExactPolynomialSquareRoot::NotSquare);
}
let Some(root_coefficient) = Z.try_div_owned(residual, &denominator) else {
return Some(ExactPolynomialSquareRoot::NotSquare);
};
if root_coefficient.is_zero() {
return Some(ExactPolynomialSquareRoot::NotSquare);
}
let root_index = self.root_coefficients.len();
if self.product_count.saturating_add(root_index) > MAX_SPARSE_SQUARE_ROOT_PAIR_PRODUCTS
{
return None;
}
self.product_count += root_index;
self.root_monomials.push(E::pack(&self.next_root_exponents));
self.root_exponents
.extend_from_slice(&self.next_root_exponents);
self.root_coefficients.push(root_coefficient);
for previous in 1..root_index {
if !self.accumulate_product(previous, root_index, true) {
return None;
}
}
if !self.accumulate_product(root_index, root_index, false) {
return None;
}
}
let mut root = polynomial.zero_with_capacity(self.root_coefficients.len());
for index in (0..self.root_coefficients.len()).rev() {
let coefficient =
std::mem::replace(&mut self.root_coefficients[index], Integer::zero());
root.append_monomial_back(
coefficient,
&self.root_exponents[index * polynomial.nvars()..(index + 1) * polynomial.nvars()],
);
}
#[cfg(debug_assertions)]
debug_assert!(&root * &root == *polynomial);
Some(ExactPolynomialSquareRoot::Root(root))
}
}
enum QuadraticFactorization<P> {
Split([P; 2]),
Irreducible,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum SparseDiophantineFallback {
Recursive,
RetrySample,
}
/// Dense coefficient grid for a polynomial in two retained variables.
/// Coefficients are stored by increasing `y` degree, then increasing `x` degree.
#[derive(Clone, Debug, Eq, PartialEq)]
struct DenseBivariateImage<T> {
x_len: usize,
y_len: usize,
coefficients: Vec<T>,
}
/// Nonzero coefficients of a univariate polynomial and their increasing degrees.
struct DenseIndexedUnivariate<T> {
coefficients: Vec<T>,
indices: Vec<u32>,
}
/// Coefficients below the omitted leading one of a monic univariate polynomial.
struct DenseMonicModulus<T> {
lower_coefficients: Vec<T>,
}
/// Data reused to solve the two modular correction equations at one image point.
struct DenseTwoFactorCorrectionContext<T> {
multipliers: [DenseIndexedUnivariate<T>; 2],
moduli: [DenseMonicModulus<T>; 2],
}
impl<T> DenseBivariateImage<T> {
#[inline]
fn index(&self, x_degree: usize, y_degree: usize) -> usize {
debug_assert!(x_degree < self.x_len);
debug_assert!(y_degree < self.y_len);
y_degree * self.x_len + x_degree
}
#[inline]
fn coefficient(&self, x_degree: usize, y_degree: usize) -> &T {
&self.coefficients[self.index(x_degree, y_degree)]
}
}
/// Bézout coefficients for a pair of coprime univariate factor images.
struct TwoFactorImageBezout<P> {
s: P,
t: P,
}
/// Stores the geometric interpolation base and univariate Bézout coefficients
/// reused by the sparse Hensel corrections in one lifting stage.
struct SparseDiophantineContext<P, C> {
two_factor_bezout: HashMap<(P, P), Option<TwoFactorImageBezout<P>>>,
two_factor_base_points: Option<Vec<(usize, C)>>,
}
impl<P, C> SparseDiophantineContext<P, C> {
fn new() -> Self {
Self {
two_factor_bezout: HashMap::default(),
two_factor_base_points: None,
}
}
fn clear_two_factor_images(&mut self) {
self.two_factor_bezout.clear();
self.two_factor_base_points = None;
}
}
/// Controls where a multivariate Hensel lift starts and how it handles a
/// sparse correction that cannot be reconstructed exactly.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct MultivariateHenselContext {
start_index: usize,
sparse_diophantine_fallback: SparseDiophantineFallback,
}
/// Tracks failed bivariate lifting attempts and the variable order needed by
/// the automatic univariate fallback. Each failure advances the deterministic
/// sampling cursor by ten; the fallback starts after a bounded block of failed
/// bivariate samples.
struct BivariateFactorRetryContext {
failed_attempts: usize,
univariate_state: Option<UnivariateFactorFallbackState>,
}
/// Restores the route-local sampling state when automatic factorization moves
/// from a bivariate image to a univariate image.
struct UnivariateFactorFallbackState {
order: Vec<usize>,
coefficient_upper_bound: i64,
}
impl BivariateFactorRetryContext {
fn new(univariate_state: Option<UnivariateFactorFallbackState>) -> Self {
Self {
failed_attempts: 0,
univariate_state,
}
}
/// Advance to the next deterministic sample and return the saved
/// univariate state after the bounded bivariate retry block is exhausted.
fn advance(
&mut self,
coefficient_upper_bound: &mut i64,
) -> Option<UnivariateFactorFallbackState> {
*coefficient_upper_bound += 10;
self.failed_attempts += 1;
(self.failed_attempts >= INTEGER_FACTOR_BIVARIATE_AUTO_RETRIES)
.then(|| self.univariate_state.take())
.flatten()
}
}
impl MultivariateHenselContext {
/// Start lifting at `start_index` and use the recursive Taylor-quotient
/// solver when sparse correction reconstruction fails.
const fn new(start_index: usize) -> Self {
Self {
start_index,
sparse_diophantine_fallback: SparseDiophantineFallback::Recursive,
}
}
/// Return an error for a failed higher-dimensional sparse correction so
/// that the caller can retry with a different evaluation sample. Evaluated
/// stages may then certify the completed unshifted lift once at the end.
const fn retry_sample_on_sparse_failure(mut self) -> Self {
self.sparse_diophantine_fallback = SparseDiophantineFallback::RetrySample;
self
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MultivariateHenselError {
SparseDiophantineFailed,
}
fn integer_factor_start_mode() -> IntegerFactorStart {
match (
GLOBAL_SETTINGS
.use_univariate_factorization
.load(std::sync::atomic::Ordering::Relaxed),
GLOBAL_SETTINGS
.use_bivariate_factorization
.load(std::sync::atomic::Ordering::Relaxed),
) {
(true, true) => IntegerFactorStart::Auto,
(true, false) => IntegerFactorStart::Univariate,
(false, true) => IntegerFactorStart::Bivariate,
(false, false) => IntegerFactorStart::Disabled,
}
}
/// Estimate how many input terms map to each exponent pair for two variables.
/// Automatic integer factorization uses this score to select its initial
/// bivariate or univariate image factorization.
fn integer_factor_bivariate_box_density(
term_count: usize,
first_degree: usize,
second_degree: usize,
) -> f64 {
term_count as f64 / ((first_degree as f64 + 1.0) * (second_degree as f64 + 1.0))
}
/// Return whether the selected bivariate degree-box score is high enough for
/// one-image Wang leading-coefficient reconstruction.
fn integer_factor_bivariate_wang_density_supported(
term_count: usize,
first_degree: usize,
second_degree: usize,
) -> bool {
integer_factor_bivariate_box_density(term_count, first_degree, second_degree)
> INTEGER_FACTOR_BIVARIATE_WANG_MIN_BOX_DENSITY
}
/// Move a low-degree variable to the front for an anisotropic sparse input.
/// A small leading layer bounds leading-coefficient reconstruction work, while
/// the candidate box score selects a low-degree univariate image with margin.
fn reorder_integer_factor_variables_for_sparse_univariate(
automatic: bool,
term_count: usize,
degrees: &[usize],
leading_layer_lengths: &[usize],
order: &mut [usize],
) -> bool {
debug_assert_eq!(degrees.len(), leading_layer_lengths.len());
debug_assert!(order.iter().all(|&variable| variable < degrees.len()));
if !automatic
|| term_count < INTEGER_FACTOR_SPARSE_UNIVARIATE_MIN_TERMS
|| order.len() < 2
|| degrees.iter().any(|°ree| degree == 2)
{
return false;
}
let current_degree = degrees[order[0]];
let current_density =
integer_factor_bivariate_box_density(term_count, current_degree, degrees[order[1]]);
if current_density > INTEGER_FACTOR_BIVARIATE_SPARSE_BOX_DENSITY_THRESHOLD {
return false;
}
let minimum_leading_layer = order
.iter()
.map(|&variable| leading_layer_lengths[variable])
.min()
.unwrap();
let maximum_candidate_leading_layer =
minimum_leading_layer.saturating_mul(INTEGER_FACTOR_SPARSE_UNIVARIATE_MAX_LCOEFF_RATIO);
let candidate_position = (1..order.len())
.filter(|&position| {
let variable = order[position];
let degree = degrees[variable];
leading_layer_lengths[variable] <= maximum_candidate_leading_layer
&& degree.saturating_mul(INTEGER_FACTOR_SPARSE_UNIVARIATE_MIN_DEGREE_RATIO)
<= current_degree
&& integer_factor_bivariate_box_density(term_count, degree, current_degree)
>= INTEGER_FACTOR_SPARSE_UNIVARIATE_MIN_BOX_DENSITY
})
.min_by_key(|&position| {
let variable = order[position];
(degrees[variable], leading_layer_lengths[variable], position)
});
let Some(candidate_position) = candidate_position else {
return false;
};
order[..=candidate_position].rotate_right(1);
true
}
/// A polynomial that can be factorized.
pub trait Factorize: Sized {
/// Perform a square-free factorization.
/// The output is `a_1^e1*...*a_n^e_n`
/// where each `a_i` is relative prime.
fn square_free_factorization(&self) -> Vec<(Self, usize)>;
/// Factor a polynomial over its coefficient ring.
fn factor(&self) -> Vec<(Self, usize)>;
/// Return whether the polynomial is irreducible over its coefficient ring.
fn is_irreducible(&self) -> bool;
}
/// Finds coefficient content while separating factors that omit a variable.
///
/// The common-monomial test is shared by every coefficient view of the current
/// polynomial. It lets a monomial coefficient divisor prove that no
/// nonconstant polynomial content remains.
struct SeparableCoefficientContentContext {
has_trivial_common_monomial: bool,
}
impl SeparableCoefficientContentContext {
fn new<F: EuclideanDomain + PolynomialGCD<E>, E: PositiveExponent>(
polynomial: &MultivariatePolynomial<F, E, LexOrder>,
) -> Self {
let has_trivial_common_monomial = !polynomial.is_zero()
&& (0..polynomial.nvars())
.all(|variable| polynomial.degree_bounds(variable).0 == E::zero());
Self {
has_trivial_common_monomial,
}
}
/// Construct the coefficient-content context after the common monomial
/// has been divided from the polynomial.
fn after_common_monomial_removal() -> Self {
Self {
has_trivial_common_monomial: true,
}
}
/// Return the nonconstant GCD of the coefficient layers, if one exists.
///
/// A constant layer proves constant content immediately. A monomial layer
/// does the same when the full polynomial has no common monomial, because
/// every polynomial divisor of a monomial is itself a monomial. For
/// sufficiently unbalanced layer supports, an exact GCD of the two
/// sparsest layers can provide the same certificate before `gcd_multiple`
/// constructs its much larger first-stage operand.
fn nonconstant_content<F: EuclideanDomain + PolynomialGCD<E>, E: PositiveExponent>(
&self,
mut layers: Vec<MultivariatePolynomial<F, E, LexOrder>>,
) -> Option<MultivariatePolynomial<F, E, LexOrder>> {
debug_assert!(!layers.is_empty());
if layers.iter().any(|layer| layer.is_constant()) {
return None;
}
if self.has_trivial_common_monomial && layers.iter().any(|layer| layer.nterms() == 1) {
#[cfg(test)]
SEPARABLE_CONTENT_SINGLE_MONOMIAL_CERTIFICATES.with(|count| count.set(count.get() + 1));
return None;
}
if layers.len() > 2 {
let mut support_order: Vec<_> = (0..layers.len()).collect();
support_order.sort_unstable_by_key(|&index| (layers[index].nterms(), index));
let left_index = support_order[0];
let right_index = support_order[1];
let left_support = layers[left_index].nterms();
let right_support = layers[right_index].nterms();
let accumulated_support = support_order
.iter()
.skip(1)
.take(20)
.fold(0usize, |sum, &index| {
sum.saturating_add(layers[index].nterms())
});
// The exact-pair and accumulated first-stage proxies both contain
// `left_support`, so cancel it to make the one-eighth comparison
// without overflowing `usize`.
if right_support <= accumulated_support / 8 {
#[cfg(test)]
SEPARABLE_CONTENT_PAIR_PROBES.with(|count| count.set(count.get() + 1));
let pair_gcd = layers[left_index].gcd(&layers[right_index]);
if pair_gcd.is_constant() {
return None;
}
if pair_gcd.nterms() == 1 {
if self.has_trivial_common_monomial {
#[cfg(test)]
SEPARABLE_CONTENT_PAIR_MONOMIAL_CERTIFICATES
.with(|count| count.set(count.get() + 1));
return None;
}
#[cfg(test)]
SEPARABLE_CONTENT_NONTRIVIAL_MONOMIAL_FALLBACKS
.with(|count| count.set(count.get() + 1));
}
// Replacing the pair by its exact GCD preserves the full
// coefficient content. Keep the replacement only when its
// support is no larger than the two inputs it summarizes.
if pair_gcd.nterms() <= left_support.saturating_add(right_support) {
let larger_index = left_index.max(right_index);
let smaller_index = left_index.min(right_index);
layers.swap_remove(larger_index);
layers.swap_remove(smaller_index);
layers.push(pair_gcd);
#[cfg(test)]
SEPARABLE_CONTENT_PAIR_REPLACEMENTS.with(|count| count.set(count.get() + 1));
}
}
}
let gcd = PolynomialGCD::gcd_multiple(layers);
if gcd.is_constant() { None } else { Some(gcd) }
}
/// Try the two coefficient views whose two sparsest layers have the
/// largest support products, returning an exact factor that omits the
/// corresponding variable.
///
/// A factor independent of a variable multiplies every coefficient layer,
/// so even the two sparsest layers in that view tend to remain well
/// supported. Their GCD is intersected with every remaining layer before
/// the factor is accepted, then exact division certifies the split.
#[inline(never)]
fn split_from_well_supported_layer_pairs<E: PositiveExponent>(
&self,
polynomial: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
excluded_variable: usize,
modular_probe_context: &EarlyFactorModularProbeContext,
modular_image: &EarlyFactorModularImage,
) -> Option<[MultivariatePolynomial<IntegerRing, E, LexOrder>; 2]> {
let layer_supports = modular_image
.layer_supports
.as_ref()
.expect("coefficient-content data was requested for this modular image");
let mut candidates = Vec::with_capacity(polynomial.nvars().saturating_sub(1));
for variable in 0..polynomial.nvars() {
if variable == excluded_variable || layer_supports[variable].is_empty() {
continue;
}
let mut supported_layers = layer_supports[variable]
.iter()
.copied()
.enumerate()
.filter(|(_, support)| *support != 0)
.collect::<Vec<_>>();
if supported_layers.len() < 2 {
continue;
}
supported_layers.sort_unstable_by_key(|(degree, support)| (*support, *degree));
if self.has_trivial_common_monomial && supported_layers[0].1 == 1 {
continue;
}
let score = supported_layers[0].1.saturating_mul(supported_layers[1].1);
candidates.push((
score,
variable,
[supported_layers[0].0 as u32, supported_layers[1].0 as u32],
));
}
candidates.sort_unstable_by_key(|(score, variable, _)| (Reverse(*score), *variable));
for (_, variable, coefficient_degrees) in candidates
.into_iter()
.take(EARLY_SEPARABLE_CONTENT_PAIR_PROBES)
{
#[cfg(test)]
EARLY_SEPARABLE_PAIR_PROBE_CALLS.with(|count| count.set(count.get() + 1));
if !modular_probe_context.coefficient_pair_may_have_common_factor(
polynomial,
variable,
coefficient_degrees,
modular_image,
) {
continue;
}
let mut layers = polynomial
.to_univariate_polynomial_list(variable)
.into_iter()
.map(|(coefficient, _)| coefficient)
.collect::<Vec<_>>();
let mut support_order = (0..layers.len()).collect::<Vec<_>>();
support_order.sort_unstable_by_key(|&index| (layers[index].nterms(), index));
let left_index = support_order[0];
let right_index = support_order[1];
let pair_gcd = layers[left_index].gcd(&layers[right_index]);
if pair_gcd.is_constant()
|| (self.has_trivial_common_monomial && pair_gcd.nterms() == 1)
{
continue;
}
let larger_index = left_index.max(right_index);
let smaller_index = left_index.min(right_index);
layers.swap_remove(larger_index);
layers.swap_remove(smaller_index);
layers.push(pair_gcd);
let content = PolynomialGCD::gcd_multiple(layers);
if content.is_constant() {
continue;
}
let Some(quotient) = polynomial.try_div(&content) else {
continue;
};
#[cfg(test)]
EARLY_SEPARABLE_PAIR_SPLITS.with(|count| count.set(count.get() + 1));
return Some([content, quotient]);
}
None
}
}
impl<F: EuclideanDomain + PolynomialGCD<E>, E: PositiveExponent>
MultivariatePolynomial<F, E, LexOrder>
{
/// Find factors that do not contain all variables.
pub fn factor_separable(&self) -> Vec<Self> {
let mut stripped = self.clone();
let content_context = SeparableCoefficientContentContext::new(&stripped);
let mut factors = vec![];
for x in 0..self.nvars() {
if stripped.degree(x) == E::zero() {
continue;
}
let c = stripped.to_univariate_polynomial_list(x);
let cs = c.into_iter().map(|x| x.0).collect();
if let Some(gcd) = content_context.nonconstant_content(cs) {
stripped = stripped / &gcd;
let mut fs = gcd.factor_separable();
factors.append(&mut fs);
}
}
factors.push(stripped);
factors
}
/// Perform a square free factorization using Yun's algorithm.
///
/// The characteristic of the ring must be 0 and all variables
/// must occur in every factor.
fn square_free_factorization_0_char(&self) -> Vec<(Self, usize)> {
if self.is_constant() {
if self.is_one() {
return vec![];
} else {
return vec![(self.clone(), 1)];
}
}
// any variable can be selected
// select the one with the lowest degree
let lowest_rank_var = (0..self.nvars())
.filter_map(|x| {
let d = self.degree(x);
if d > E::zero() { Some((x, d)) } else { None }
})
.min_by_key(|a| a.1)
.unwrap()
.0;
let b = self.derivative(lowest_rank_var);
let c = self.gcd(&b);
if c.is_one() {
return vec![(self.clone(), 1)];
}
let mut factors = vec![];
let mut w = self / &c;
let mut y = &b / &c;
let mut i = 1;
while !w.is_constant() {
let z = y - w.derivative(lowest_rank_var);
let g = w.gcd(&z);
w = w / &g;
y = z / &g;
if !g.is_one() {
factors.push((g, i));
}
i += 1
}
factors
}
/// Use Newton's polygon method to test if a bivariate polynomial is irreducible.
/// If this method returns `false`, the test is inconclusive.
///
/// The polynomial must have overall factors of single variables removed.
fn bivariate_irreducibility_test(&self) -> bool {
/// Compute the convex hull via the Monotone chain algorithm.
fn convex_hull(mut points: Vec<(isize, isize)>) -> Vec<(isize, isize)> {
points.sort();
if points.len() < 2 {
return points;
}
// Cross product of o-a and o-b vectors, positive means ccw turn, negative means cw turn and 0 means collinear.
fn cross(o: &(isize, isize), a: &(isize, isize), b: &(isize, isize)) -> isize {
(a.0 - o.0) * (b.1 - o.1) - (a.1 - o.1) * (b.0 - o.0)
}
let mut lower = vec![];
let mut upper = vec![];
for (t, rev) in [(&mut lower, false), (&mut upper, true)] {
for i in 0..points.len() {
let p = if rev {
points[points.len() - 1 - i]
} else {
points[i]
};
while t.len() >= 2 && cross(&t[t.len() - 2], &t[t.len() - 1], &p) <= 0 {
t.pop();
}
t.push(p);
}
}
lower.pop();
upper.pop();
lower.extend(upper);
lower
}
let vars: Vec<_> = (0..self.nvars())
.filter(|v| self.degree(*v) > E::zero())
.collect();
if vars.len() != 2 {
return false;
}
let points = self
.exponents
.chunks(self.nvars())
.map(|e| (e[vars[0]].to_u32() as isize, e[vars[1]].to_u32() as isize))
.collect();
let hull = convex_hull(points);
match hull.len() {
2 => {
let x_deg = hull[0].0.abs_diff(hull[1].0);
let y_deg = hull[0].1.abs_diff(hull[1].1);
gcd_unsigned(x_deg as u64, y_deg as u64) == 1
}
3 => {
// the hull has the form (n, 0), (0, m), (u, v)
let (mut n, mut m, mut u, mut v) = (-1, -1, -1, -1);
for (x, y) in hull {
if x != 0 && y == 0 {
n = x;
} else if y != 0 && x == 0 {
m = y;
} else {
u = x;
v = y;
}
}
n != -1
&& m != -1
&& u != -1
&& v != -1
&& gcd_unsigned(
gcd_unsigned(gcd_unsigned(n as u64, m as u64), u as u64),
v as u64,
) == 1
}
_ => false,
}
}
}
impl<R: EuclideanDomain, E: PositiveExponent> MultivariatePolynomial<R, E, LexOrder> {
/// Check if a parse lift is possible.
#[allow(dead_code)]
fn sparse_lift_possible(&self, factors: &[Self], order: &[usize]) -> bool {
// check if all bivariate monomials occur in the product of factors
let mut all_monomials = HashSet::with_capacity(self.nterms());
for e in self.exponents.chunks(self.nvars()) {
all_monomials.insert((e[order[0]], e[order[1]]));
}
let mut total = factors[0].clone();
for f in &factors[1..] {
total = &total * f;
}
let mut all_monomials_in_factors = HashSet::with_capacity(self.nterms());
for e in total.exponents.chunks(total.nvars()) {
all_monomials_in_factors.insert((e[order[0]], e[order[1]]));
}
all_monomials == all_monomials_in_factors
}
}
/// Dense univariate integer arithmetic used by quadratic and synchronized Hensel lifting.
///
/// Coefficients are stored from constant to leading degree. Products are
/// computed over the integers so that Hensel residuals can be divided exactly;
/// modular operations return trimmed representatives in `[0, modulus)`.
struct DenseIntegerModularUnivariateContext<'a, E: PositiveExponent> {
modulus: &'a Integer,
variable: usize,
template: &'a MultivariatePolynomial<IntegerRing, E, LexOrder>,
dense_indices: &'a [u32],
}
/// A dense monic associate prepared for repeated remainders modulo a unit-leading divisor.
///
/// Scaling by the inverse of the leading coefficient preserves the divisor's generated ideal.
/// The remainder loop can then clear each pivot without computing another modular inverse.
struct DenseIntegerModularUnivariateDivisor {
coefficients: DenseIntegerUnivariatePolynomial,
}
impl<'a, E: PositiveExponent> DenseIntegerModularUnivariateContext<'a, E> {
fn new(
modulus: &'a Integer,
variable: usize,
template: &'a MultivariatePolynomial<IntegerRing, E, LexOrder>,
dense_indices: &'a [u32],
) -> Self {
assert!(!modulus.is_zero() && !modulus.is_negative());
debug_assert!(template.exponents_iter().all(|exponents| {
exponents
.iter()
.enumerate()
.all(|(index, exponent)| index == variable || exponent.is_zero())
}));
debug_assert!(
dense_indices
.iter()
.enumerate()
.all(|(index, dense_index)| *dense_index as usize == index)
);
Self {
modulus,
variable,
template,
dense_indices,
}
}
/// Removes all zero leading coefficients, representing the zero
/// polynomial by an empty vector.
fn trim(coefficients: &mut DenseIntegerUnivariatePolynomial) {
while coefficients.last().is_some_and(Integer::is_zero) {
coefficients.pop();
}
}
/// Converts a sparse univariate polynomial to ascending dense
/// coefficients without changing its integer representatives.
fn dense_coefficients(
&self,
polynomial: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
) -> DenseIntegerUnivariatePolynomial {
if polynomial.is_zero() {
return Vec::new();
}
let degree = polynomial.degree(self.variable).to_u32() as usize;
let mut coefficients = vec![Integer::zero(); degree + 1];
for term in polynomial {
debug_assert!(
term.exponents
.iter()
.enumerate()
.all(|(index, exponent)| { index == self.variable || exponent.is_zero() })
);
coefficients[term.exponents[self.variable].to_u32() as usize] =
term.coefficient.clone();
}
coefficients
}
/// Converts ascending dense coefficients to the polynomial layout used by
/// the caller of the Hensel lift.
fn from_dense_coefficients(
&self,
coefficients: DenseIntegerUnivariatePolynomial,
) -> MultivariatePolynomial<IntegerRing, E, LexOrder> {
let capacity = coefficients
.iter()
.filter(|coefficient| !coefficient.is_zero())
.count();
let mut polynomial = self.template.zero_with_capacity(capacity);
let mut exponents = vec![E::zero(); self.template.nvars()];
for (degree, coefficient) in coefficients.into_iter().enumerate() {
if coefficient.is_zero() {
continue;
}
exponents[self.variable] = E::from_u32(degree as u32);
polynomial.append_monomial_back(coefficient, &exponents);
}
polynomial
}
/// Canonicalizes every coefficient into `[0, modulus)` and removes zero
/// leading cells.
fn reduce_in_place(&self, coefficients: &mut DenseIntegerUnivariatePolynomial) {
for coefficient in coefficients.iter_mut() {
let value = std::mem::replace(coefficient, Integer::zero());
*coefficient = value % self.modulus;
}
Self::trim(coefficients);
}
fn reduce(&self, coefficients: &[Integer]) -> DenseIntegerUnivariatePolynomial {
let mut reduced = coefficients.to_vec();
self.reduce_in_place(&mut reduced);
reduced
}
/// Converts canonical nonnegative coefficients to symmetric
/// representatives without another modular division.
fn symmetrize_in_place(&self, coefficients: &mut DenseIntegerUnivariatePolynomial) {
let half_modulus = self.modulus / 2;
for coefficient in coefficients {
debug_assert!(!coefficient.is_negative() && &*coefficient < self.modulus);
if *coefficient > half_modulus {
*coefficient -= self.modulus;
}
}
}
/// Multiplies two dense polynomials exactly over the integers.
///
/// The integer polynomial kernel selects its bounded or multiprecision
/// convolution strategy. If it declines the request, the generic ring
/// loop accumulates the same convolution with fused integer products.
fn multiply_raw(
&self,
left: &[Integer],
right: &[Integer],
) -> DenseIntegerUnivariatePolynomial {
if left.is_empty() || right.is_empty() {
return Vec::new();
}
let output_len = left
.len()
.checked_add(right.len())
.and_then(|length| length.checked_sub(1))
.expect("dense polynomial product length overflow");
assert!(left.len() <= self.dense_indices.len());
assert!(right.len() <= self.dense_indices.len());
assert!(output_len <= self.dense_indices.len());
// Lift buffers have contiguous, effectively dense support. Interior
// zero cells retain the contiguous indices used by dense convolution
// strategies.
if let Some(coefficients) = Z.kernels().polynomial().and_then(|kernels| {
kernels.try_dense_mul(DensePolynomialMulRequest {
output_len,
left_coefficients: left,
left_indices: &self.dense_indices[..left.len()],
right_coefficients: right,
right_indices: &self.dense_indices[..right.len()],
})
}) {
let mut product = vec![Integer::zero(); output_len];
for (index, coefficient) in coefficients {
product[index as usize] = coefficient;
}
Self::trim(&mut product);
return product;
}
let mut product = vec![Integer::zero(); output_len];
for (left_degree, left_coefficient) in left.iter().enumerate() {
if left_coefficient.is_zero() {
continue;
}
for (right_degree, right_coefficient) in right.iter().enumerate() {
if right_coefficient.is_zero() {
continue;
}
Z.add_mul_assign(
&mut product[left_degree + right_degree],
left_coefficient,
right_coefficient,
);
}
}
Self::trim(&mut product);
product
}
/// Multiplies and then canonicalizes the product in the context ring.
#[cfg(test)]
fn multiply_mod(
&self,
left: &[Integer],
right: &[Integer],
) -> DenseIntegerUnivariatePolynomial {
let mut product = self.multiply_raw(left, right);
self.reduce_in_place(&mut product);
product
}
/// Reduces an owned dense polynomial modulo a monic dense divisor.
///
/// Lower cells accumulate exact integer updates until they become pivots
/// or survive in the remainder, limiting modular reductions to one per
/// pivot and one per returned coefficient.
fn remainder_monic(
&self,
mut remainder: DenseIntegerUnivariatePolynomial,
divisor: &[Integer],
) -> DenseIntegerUnivariatePolynomial {
assert!(divisor.last().is_some_and(Integer::is_one));
if remainder.is_empty() {
return remainder;
}
if remainder.len() < divisor.len() {
self.reduce_in_place(&mut remainder);
return remainder;
}
let divisor_degree = divisor.len() - 1;
for power in (divisor_degree..remainder.len()).rev() {
let value = std::mem::replace(&mut remainder[power], Integer::zero());
let pivot = value % self.modulus;
if pivot.is_zero() {
continue;
}
let shift = power - divisor_degree;
for (offset, divisor_coefficient) in divisor.iter().take(divisor_degree).enumerate() {
Z.sub_mul_assign(&mut remainder[shift + offset], &pivot, divisor_coefficient);
}
}
remainder.truncate(divisor_degree);
self.reduce_in_place(&mut remainder);
remainder
}
/// Prepares the monic associate of a canonical dense divisor whose leading coefficient is
/// invertible modulo the context modulus.
fn prepare_unit_leading_divisor(
&self,
coefficients: DenseIntegerUnivariatePolynomial,
) -> DenseIntegerModularUnivariateDivisor {
debug_assert!(
coefficients
.iter()
.all(|coefficient| { !coefficient.is_negative() && coefficient < self.modulus })
);
let leading_inverse = coefficients
.last()
.expect("a Hensel divisor must be nonzero modulo the correction modulus")
.mod_inverse(self.modulus);
self.prepare_unit_leading_divisor_with_inverse(coefficients, &leading_inverse)
}
/// Scales a canonical dense divisor to its monic associate using a known leading inverse.
fn prepare_unit_leading_divisor_with_inverse(
&self,
mut coefficients: DenseIntegerUnivariatePolynomial,
leading_inverse: &Integer,
) -> DenseIntegerModularUnivariateDivisor {
for coefficient in &mut coefficients {
let value = std::mem::replace(coefficient, Integer::zero()) * leading_inverse;
*coefficient = value % self.modulus;
}
debug_assert!(coefficients.last().is_some_and(Integer::is_one));
DenseIntegerModularUnivariateDivisor { coefficients }
}
/// Prepares two monic divisors, sharing the modular inverse when their leading units agree.
fn prepare_unit_leading_divisor_pair(
&self,
left: DenseIntegerUnivariatePolynomial,
right: DenseIntegerUnivariatePolynomial,
) -> (
DenseIntegerModularUnivariateDivisor,
DenseIntegerModularUnivariateDivisor,
) {
debug_assert!(
left.iter()
.chain(&right)
.all(|coefficient| !coefficient.is_negative() && coefficient < self.modulus)
);
let left_leading = left
.last()
.expect("a Hensel divisor must be nonzero modulo the correction modulus");
let right_leading = right
.last()
.expect("a Hensel divisor must be nonzero modulo the correction modulus");
if left_leading == right_leading {
let leading_inverse = left_leading.mod_inverse(self.modulus);
(
self.prepare_unit_leading_divisor_with_inverse(left, &leading_inverse),
self.prepare_unit_leading_divisor_with_inverse(right, &leading_inverse),
)
} else {
(
self.prepare_unit_leading_divisor(left),
self.prepare_unit_leading_divisor(right),
)
}
}
/// Computes `((value rem divisor) * multiplier) rem divisor` with native `i128`
/// accumulators when a checked bound covers both long divisions and the intervening
/// convolution.
///
/// Every input coefficient must already be canonical modulo this context. During monic long
/// division, one cell receives at most `degree(divisor)` products of a canonical pivot and a
/// divisor coefficient. The precheck bounds those updates, the convolution, and the second
/// division before converting any coefficient.
fn try_i128_multiply_remainder_monic(
&self,
value: &[Integer],
multiplier: &[Integer],
divisor: &[Integer],
) -> Option<DenseIntegerUnivariatePolynomial> {
if !divisor.last().is_some_and(Integer::is_one) {
return None;
}
if value.is_empty() || multiplier.is_empty() || divisor.len() == 1 {
return Some(Vec::new());
}
let fixed_value = |coefficient: &Integer| match coefficient {
Integer::Single(value) => Some(i128::from(*value)),
Integer::Double(value) => Some(value.get()),
Integer::Large(_) => None,
};
let modulus = fixed_value(self.modulus)?;
if modulus <= 0 {
return None;
}
let divisor_degree = divisor.len() - 1;
let maximum_pivot = (modulus - 1) as u128;
let maximum_product = maximum_pivot.checked_mul(maximum_pivot)?;
let division_updates = (divisor_degree as u128).checked_mul(maximum_product)?;
let first_remainder_bound = maximum_pivot.checked_add(division_updates)?;
let reduced_value_len = value.len().min(divisor_degree);
let collision_count = reduced_value_len.min(multiplier.len()) as u128;
let second_remainder_bound = collision_count
.checked_mul(maximum_product)?
.checked_add(division_updates)?;
if first_remainder_bound > i128::MAX as u128 || second_remainder_bound > i128::MAX as u128 {
return None;
}
let convert_canonical = |coefficients: &[Integer]| {
let mut converted = Vec::with_capacity(coefficients.len());
for coefficient in coefficients {
let coefficient = fixed_value(coefficient)?;
if coefficient < 0 || coefficient >= modulus {
return None;
}
converted.push(coefficient);
}
Some(converted)
};
let mut value = convert_canonical(value)?;
let multiplier = convert_canonical(multiplier)?;
let divisor = convert_canonical(divisor)?;
#[inline]
fn remainder_monic_i128(
mut remainder: Vec<i128>,
divisor: &[i128],
modulus: i128,
) -> Vec<i128> {
let divisor_degree = divisor.len() - 1;
if remainder.len() >= divisor.len() {
for power in (divisor_degree..remainder.len()).rev() {
let pivot = std::mem::replace(&mut remainder[power], 0).rem_euclid(modulus);
if pivot == 0 {
continue;
}
let shift = power - divisor_degree;
for (coefficient, divisor_coefficient) in remainder[shift..power]
.iter_mut()
.zip(&divisor[..divisor_degree])
{
*coefficient -= pivot * divisor_coefficient;
}
}
remainder.truncate(divisor_degree);
}
for coefficient in &mut remainder {
*coefficient = coefficient.rem_euclid(modulus);
}
while remainder
.last()
.is_some_and(|coefficient| *coefficient == 0)
{
remainder.pop();
}
remainder
}
value = remainder_monic_i128(value, &divisor, modulus);
if value.is_empty() {
return Some(Vec::new());
}
let output_len = value
.len()
.checked_add(multiplier.len())
.and_then(|length| length.checked_sub(1))?;
let mut product = vec![0i128; output_len];
for (left_degree, left_coefficient) in value.into_iter().enumerate() {
for (right_degree, right_coefficient) in multiplier.iter().copied().enumerate() {
product[left_degree + right_degree] += left_coefficient * right_coefficient;
}
}
let remainder = remainder_monic_i128(product, &divisor, modulus);
#[cfg(test)]
DENSE_INTEGER_I128_MULTIPLY_REMAINDERS
.with(|operations| operations.set(operations.get() + 1));
Some(remainder.into_iter().map(Integer::from_double).collect())
}
/// Computes a dense modular product and remainder against an already-monic divisor.
fn multiply_remainder_monic(
&self,
value: DenseIntegerUnivariatePolynomial,
multiplier: &[Integer],
divisor: &[Integer],
) -> DenseIntegerUnivariatePolynomial {
if let Some(remainder) = self.try_i128_multiply_remainder_monic(&value, multiplier, divisor)
{
return remainder;
}
let reduced_value = self.remainder_monic(value, divisor);
let product = self.multiply_raw(&reduced_value, multiplier);
self.remainder_monic(product, divisor)
}
/// Computes `((value rem divisor) * multiplier) rem divisor` in the
/// context ring.
///
/// Reducing `value` before the multiplication bounds the intermediate
/// degree by the divisor and multiplier degrees. The returned dense
/// coefficients are canonical representatives in `[0, modulus)`.
fn multiply_remainder(
&self,
value: DenseIntegerUnivariatePolynomial,
multiplier: &[Integer],
divisor: &DenseIntegerModularUnivariateDivisor,
) -> DenseIntegerUnivariatePolynomial {
self.multiply_remainder_monic(value, multiplier, &divisor.coefficients)
}
/// Divides every coefficient exactly by `divisor`, then returns its
/// canonical image in the context ring.
fn exact_scalar_quotient_mod(
&self,
mut numerator: DenseIntegerUnivariatePolynomial,
divisor: &Integer,
) -> DenseIntegerUnivariatePolynomial {
for coefficient in &mut numerator {
let value = std::mem::replace(coefficient, Integer::zero());
debug_assert!((&value % divisor).is_zero());
*coefficient = Z.exact_div_owned(value, divisor) % self.modulus;
}
Self::trim(&mut numerator);
numerator
}
/// Computes the exact dense residual `target-left*right`.
fn exact_product_residual(
&self,
target: &[Integer],
left: &[Integer],
right: &[Integer],
) -> DenseIntegerUnivariatePolynomial {
let mut residual = self.multiply_raw(left, right);
residual.resize(residual.len().max(target.len()), Integer::zero());
for (index, coefficient) in residual.iter_mut().enumerate() {
let product_coefficient = std::mem::replace(coefficient, Integer::zero());
let mut value = -product_coefficient;
if let Some(target_coefficient) = target.get(index) {
value += target_coefficient;
}
*coefficient = value;
}
Self::trim(&mut residual);
residual
}
/// Computes `(target-left*right)/divisor` exactly over the integers and
/// returns its canonical image modulo the correction modulus.
fn exact_product_residual_quotient_mod(
&self,
target: &[Integer],
left: &[Integer],
right: &[Integer],
divisor: &Integer,
) -> DenseIntegerUnivariatePolynomial {
let residual = self.exact_product_residual(target, left, right);
self.exact_scalar_quotient_mod(residual, divisor)
}
/// Computes `(1-s*u-t*w)/divisor` exactly and returns its canonical image
/// modulo the correction modulus.
fn exact_bezout_residual_quotient_mod(
&self,
s: &[Integer],
u: &[Integer],
t: &[Integer],
w: &[Integer],
divisor: &Integer,
) -> DenseIntegerUnivariatePolynomial {
let mut residual = self.multiply_raw(s, u);
for coefficient in &mut residual {
let value = std::mem::replace(coefficient, Integer::zero());
*coefficient = -value;
}
if residual.is_empty() {
residual.push(Integer::one());
} else {
residual[0] += 1;
}
let second_product = self.multiply_raw(t, w);
residual.resize(residual.len().max(second_product.len()), Integer::zero());
for (coefficient, product_coefficient) in residual.iter_mut().zip(second_product) {
*coefficient -= product_coefficient;
}
for coefficient in &mut residual {
let value = std::mem::replace(coefficient, Integer::zero());
debug_assert!((&value % divisor).is_zero());
let value = Z.exact_div_owned(value, divisor);
*coefficient = value % self.modulus;
}
Self::trim(&mut residual);
residual
}
/// Adds the exact scaled correction `scale*delta` to a dense polynomial.
fn add_scaled_exact(
&self,
mut polynomial: DenseIntegerUnivariatePolynomial,
delta: &[Integer],
scale: &Integer,
) -> DenseIntegerUnivariatePolynomial {
polynomial.resize(polynomial.len().max(delta.len()), Integer::zero());
for (coefficient, correction) in polynomial.iter_mut().zip(delta) {
Z.add_mul_assign(coefficient, correction, scale);
}
Self::trim(&mut polynomial);
polynomial
}
/// Applies a Hensel correction `old + scale*delta` without modular
/// reduction.
///
/// Every coefficient of `old` is in `[0, scale)`, every coefficient of
/// `delta` is in `[0, modulus / scale)`, and `scale` divides this context's
/// modulus. Consequently each corrected coefficient lies directly in
/// `[0, modulus)`:
///
/// `old + scale*delta <= (scale-1) + scale*(modulus/scale-1) = modulus-1`.
fn lift_correction(
&self,
mut old: DenseIntegerUnivariatePolynomial,
delta: &[Integer],
scale: &Integer,
) -> DenseIntegerUnivariatePolynomial {
debug_assert!(!scale.is_zero() && !scale.is_negative());
debug_assert!((self.modulus % scale).is_zero());
#[cfg(debug_assertions)]
{
let correction_modulus = self.modulus / scale;
debug_assert!(
old.iter()
.all(|coefficient| !coefficient.is_negative() && coefficient < scale)
);
debug_assert!(delta.iter().all(|coefficient| {
!coefficient.is_negative() && coefficient < &correction_modulus
}));
}
old.resize(old.len().max(delta.len()), Integer::zero());
for (coefficient, correction) in old.iter_mut().zip(delta) {
Z.add_mul_assign(coefficient, correction, scale);
}
debug_assert!(
old.iter()
.all(|coefficient| !coefficient.is_negative() && coefficient < self.modulus)
);
Self::trim(&mut old);
old
}
#[cfg(debug_assertions)]
fn product_matches(&self, target: &[Integer], left: &[Integer], right: &[Integer]) -> bool {
let product = self.multiply_raw(left, right);
let length = target.len().max(product.len());
(0..length).all(|index| {
let target_coefficient = target.get(index).cloned().unwrap_or_else(Integer::zero);
let product_coefficient = product.get(index).cloned().unwrap_or_else(Integer::zero);
((target_coefficient - product_coefficient) % self.modulus).is_zero()
})
}
#[cfg(debug_assertions)]
fn bezout_identity_matches(
&self,
s: &[Integer],
u: &[Integer],
t: &[Integer],
w: &[Integer],
) -> bool {
let su = self.multiply_raw(s, u);
let tw = self.multiply_raw(t, w);
let length = su.len().max(tw.len()).max(1);
(0..length).all(|index| {
let expected = if index == 0 {
Integer::one()
} else {
Integer::zero()
};
let actual = su.get(index).cloned().unwrap_or_else(Integer::zero)
+ tw.get(index).cloned().unwrap_or_else(Integer::zero);
((actual - expected) % self.modulus).is_zero()
})
}
}
/// Sparse-polynomial reference arithmetic for testing modular univariate
/// products, quotients, and remainders with symmetric representatives.
#[cfg(test)]
struct IntegerModularUnivariateContext<'a, E: PositiveExponent> {
modulus: &'a Integer,
half_modulus: Integer,
minimum_symmetric: Integer,
variable: usize,
template: &'a MultivariatePolynomial<IntegerRing, E, LexOrder>,
}
/// A monic dense divisor used by the sparse-polynomial modular reference
/// arithmetic.
#[cfg(test)]
struct IntegerModularUnivariateDivisor {
coefficients: Vec<Integer>,
}
#[cfg(test)]
impl<'a, E: PositiveExponent> IntegerModularUnivariateContext<'a, E> {
fn new(
modulus: &'a Integer,
template: &'a MultivariatePolynomial<IntegerRing, E, LexOrder>,
) -> Self {
assert!(!modulus.is_zero() && !modulus.is_negative());
let variable = template
.last_exponents()
.iter()
.position(|exponent| !exponent.is_zero())
.expect("a Hensel factor must be nonconstant");
debug_assert!(template.exponents_iter().all(|exponents| {
exponents
.iter()
.enumerate()
.all(|(index, exponent)| index == variable || exponent.is_zero())
}));
let half_modulus = modulus / 2;
let minimum_symmetric = &half_modulus + Integer::one() - modulus;
Self {
modulus,
half_modulus,
minimum_symmetric,
variable,
template,
}
}
/// Return the canonical representative in the interval selected by `Integer::symmetric_mod`.
/// Values already in that interval are returned without a modular division.
fn symmetric_reduce(&self, value: Integer) -> Integer {
if value >= self.minimum_symmetric && value <= self.half_modulus {
return value;
}
if !value.is_negative() && value < *self.modulus {
return value - self.modulus;
}
let value = value % self.modulus;
if value > self.half_modulus {
value - self.modulus
} else {
value
}
}
fn reduce(
&self,
polynomial: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
) -> MultivariatePolynomial<IntegerRing, E, LexOrder> {
polynomial.map_coeff(|coefficient| self.symmetric_reduce(coefficient.clone()), Z)
}
#[cfg(test)]
fn multiply(
&self,
left: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
right: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
) -> MultivariatePolynomial<IntegerRing, E, LexOrder> {
self.reduce(&(left * right))
}
#[cfg(test)]
fn add(
&self,
left: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
right: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
) -> MultivariatePolynomial<IntegerRing, E, LexOrder> {
self.reduce(&(left + right))
}
fn dense_coefficients(
&self,
polynomial: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
) -> Vec<Integer> {
let degree = polynomial.degree(self.variable).to_u32() as usize;
let mut coefficients = vec![Integer::zero(); degree + 1];
for term in polynomial {
debug_assert!(
term.exponents
.iter()
.enumerate()
.all(|(index, exponent)| index == self.variable || exponent.is_zero())
);
coefficients[term.exponents[self.variable].to_u32() as usize] =
self.symmetric_reduce(term.coefficient.clone());
}
coefficients
}
fn from_dense_coefficients(
&self,
coefficients: Vec<Integer>,
) -> MultivariatePolynomial<IntegerRing, E, LexOrder> {
let capacity = coefficients
.iter()
.filter(|coefficient| !coefficient.is_zero())
.count();
let mut polynomial = self.template.zero_with_capacity(capacity);
let mut exponents = vec![E::zero(); self.template.nvars()];
for (degree, coefficient) in coefficients.into_iter().enumerate() {
if coefficient.is_zero() {
continue;
}
exponents[self.variable] = E::from_u32(degree as u32);
polynomial.append_monomial_back(coefficient, &exponents);
}
polynomial
}
/// Prepare a divisor whose leading coefficient is invertible modulo the context modulus.
fn prepare_divisor(
&self,
divisor: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
) -> IntegerModularUnivariateDivisor {
assert!(!divisor.is_zero());
let mut coefficients = self.dense_coefficients(divisor);
let leading_inverse = coefficients
.last()
.expect("a nonzero divisor has a leading coefficient")
.mod_inverse(self.modulus);
for coefficient in &mut coefficients {
let value = std::mem::replace(coefficient, Integer::zero()) * &leading_inverse;
*coefficient = self.symmetric_reduce(value);
}
debug_assert!(coefficients.last().is_some_and(Integer::is_one));
IntegerModularUnivariateDivisor { coefficients }
}
/// Return the canonical symmetric remainder modulo a prepared divisor.
///
/// Dense conversion canonicalizes each input coefficient once. Updates to lower cells are
/// then accumulated as exact integers until those cells become pivots or survive in the final
/// remainder.
fn remainder(
&self,
dividend: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
divisor: &IntegerModularUnivariateDivisor,
) -> MultivariatePolynomial<IntegerRing, E, LexOrder> {
if dividend.is_zero() {
return self.template.zero();
}
let mut remainder = self.dense_coefficients(dividend);
if divisor.coefficients.len() == 1 {
return self.template.zero();
}
if remainder.len() < divisor.coefficients.len() {
return self.from_dense_coefficients(remainder);
}
let divisor_degree = divisor.coefficients.len() - 1;
for power in (divisor_degree..remainder.len()).rev() {
let value = std::mem::replace(&mut remainder[power], Integer::zero());
let pivot = self.symmetric_reduce(value);
if pivot.is_zero() {
continue;
}
let shift = power - divisor_degree;
for (offset, divisor_coefficient) in
divisor.coefficients.iter().take(divisor_degree).enumerate()
{
Z.sub_mul_assign(&mut remainder[shift + offset], &pivot, divisor_coefficient);
}
}
remainder.truncate(divisor_degree);
for coefficient in &mut remainder {
let value = std::mem::replace(coefficient, Integer::zero());
*coefficient = self.symmetric_reduce(value);
}
self.from_dense_coefficients(remainder)
}
/// Compute `((value rem divisor) * multiplier) rem divisor` modulo the context modulus.
///
/// This is the degree-bounded multiplication used by the direct factor and Bezout correction
/// formulas in quadratic Hensel lifting. The intermediate product is formed exactly and its
/// coefficients are canonicalized once when the following remainder consumes it.
fn multiply_remainder(
&self,
value: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
multiplier: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
divisor: &IntegerModularUnivariateDivisor,
) -> MultivariatePolynomial<IntegerRing, E, LexOrder> {
let reduced_value = self.remainder(value, divisor);
self.remainder(&(&reduced_value * multiplier), divisor)
}
/// Divide modulo the context modulus and return a canonical symmetric quotient and remainder.
///
/// The divisor's leading coefficient must be invertible modulo the context modulus, as it is
/// for the modular factors used during Hensel lifting. Each cell is reduced when it becomes
/// the current pivot. Subtractions into lower cells remain exact integers until those cells
/// become pivots or survive in the final remainder, avoiding a modular division after every
/// coefficient update.
#[cfg(test)]
fn quot_rem(
&self,
dividend: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
divisor: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
) -> (
MultivariatePolynomial<IntegerRing, E, LexOrder>,
MultivariatePolynomial<IntegerRing, E, LexOrder>,
) {
assert!(!divisor.is_zero());
if dividend.is_zero() {
return (self.template.zero(), self.template.zero());
}
let mut remainder = self.dense_coefficients(dividend);
let divisor = self.dense_coefficients(divisor);
if remainder.len() < divisor.len() {
return (
self.template.zero(),
self.from_dense_coefficients(remainder),
);
}
let divisor_degree = divisor.len() - 1;
let leading_inverse =
self.symmetric_reduce(divisor[divisor_degree].mod_inverse(self.modulus));
let mut quotient = vec![Integer::zero(); remainder.len() - divisor_degree];
for power in (divisor_degree..remainder.len()).rev() {
let pivot =
self.symmetric_reduce(std::mem::replace(&mut remainder[power], Integer::zero()));
if pivot.is_zero() {
continue;
}
let coefficient = self.symmetric_reduce(pivot * &leading_inverse);
if coefficient.is_zero() {
continue;
}
for (offset, divisor_coefficient) in divisor.iter().take(divisor_degree).enumerate() {
let index = power - divisor_degree + offset;
Z.sub_mul_assign(&mut remainder[index], &coefficient, divisor_coefficient);
}
quotient[power - divisor_degree] = coefficient;
}
remainder.truncate(divisor_degree);
for coefficient in &mut remainder {
let value = std::mem::replace(coefficient, Integer::zero());
*coefficient = self.symmetric_reduce(value);
}
(
self.from_dense_coefficients(quotient),
self.from_dense_coefficients(remainder),
)
}
/// Reduce a dense univariate polynomial modulo a monic divisor.
///
/// Each leading cell is reduced modulo the context modulus when it becomes the current
/// pivot. The monic leading term can then be cleared without computing a quotient or a
/// modular inverse. Updates to lower cells are accumulated as integers until those cells
/// become pivots or survive in the final remainder, avoiding repeated modular reductions.
/// The returned coefficients are canonical symmetric representatives.
#[cfg(test)]
fn remainder_monic(
&self,
dividend: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
divisor: &MultivariatePolynomial<IntegerRing, E, LexOrder>,
) -> MultivariatePolynomial<IntegerRing, E, LexOrder> {
let divisor = self.prepare_divisor(divisor);
self.remainder(dividend, &divisor)
}
}
impl<E: PositiveExponent> MultivariatePolynomial<IntegerRing, E, LexOrder> {
/// Remove the monomial that divides every term and return its exponent in
/// each variable together with the remaining polynomial.
fn remove_common_monomial(mut self) -> (Vec<E>, Self) {
if self.is_constant() {
return (vec![E::zero(); self.nvars()], self);
}
let mut powers = self.exponents(0).to_vec();
for exponents in self.exponents_iter().skip(1) {
for (power, exponent) in powers.iter_mut().zip(exponents) {
*power = (*power).min(*exponent);
}
}
if powers.iter().any(|power| *power != E::zero()) {
for exponents in self.exponents_iter_mut() {
for (exponent, power) in exponents.iter_mut().zip(&powers) {
*exponent = *exponent - *power;
}
}
}
(powers, self)
}
/// Square a coefficient polynomial for a quadratic discriminant. Sparse
/// packed inputs use triangular accumulation; all other inputs retain the
/// general multiplication dispatch.
fn square_for_quadratic_discriminant(&self) -> Self {
PackedSparsePolynomialSquareContext::new(self.nvars(), self.nterms())
.try_square(self)
.unwrap_or_else(|| self * self)
}
/// Add a factor to a factorization, combining its multiplicity with an
/// identical factor that was already recovered.
fn append_integer_factor(factors: &mut Vec<(Self, usize)>, factor: Self, multiplicity: usize) {
if factor.is_one() {
return;
}
if let Some((_, existing_multiplicity)) =
factors.iter_mut().find(|(existing, _)| existing == &factor)
{
*existing_multiplicity += multiplicity;
} else {
factors.push((factor, multiplicity));
}
}
/// Try to split a large integer polynomial before the general separability
/// scan. The split is certified either by exact coefficient content or by
/// an exact quadratic discriminant square and reconstructed product.
fn factor_quadratic_before_square_free(&self) -> Option<Vec<(Self, usize)>> {
if self.is_zero()
|| self.nterms() < MIN_EARLY_QUADRATIC_FACTOR_TERMS
|| self.nvars() > 8
|| integer_factor_start_mode() == IntegerFactorStart::Disabled
{
return None;
}
let mut active_variables = 0;
let mut has_quadratic_variable = false;
let degrees = (0..self.nvars())
.map(|var| {
let (minimum, maximum) = self.degree_bounds(var);
let degree = (maximum - minimum).to_u32() as usize;
active_variables += usize::from(degree > 0);
has_quadratic_variable |= degree == 2;
degree
})
.collect::<Vec<_>>();
if active_variables <= 2 || !has_quadratic_variable {
return None;
}
let mut integer_content = self.content();
let primitive = self.clone().div_coeff(&integer_content);
let (monomial_powers, mut core) = primitive.remove_common_monomial();
if core.lcoeff().is_negative() {
integer_content = -integer_content;
core = -core;
}
let split = core.try_early_factor_split(°rees)?;
let mut factors = Vec::with_capacity(core.nvars() + 3);
if !integer_content.is_one() {
Self::append_integer_factor(&mut factors, self.constant(integer_content), 1);
}
for (var, power) in monomial_powers.into_iter().enumerate() {
if power == E::zero() {
continue;
}
let mut exponents = vec![E::zero(); self.nvars()];
exponents[var] = E::one();
Self::append_integer_factor(
&mut factors,
self.monomial(self.ring().one(), exponents),
power.to_u32() as usize,
);
}
match split {
EarlyIntegerFactorSplit::Quadratic {
variable,
factors: split,
} => {
for factor in split {
let content = factor.univariate_content(variable);
if content.is_constant() && content.get_constant().abs().is_one() {
Self::append_integer_factor(&mut factors, factor, 1);
} else {
for (nested_factor, multiplicity) in factor.factor() {
Self::append_integer_factor(&mut factors, nested_factor, multiplicity);
}
}
}
}
EarlyIntegerFactorSplit::Separable(split) => {
for factor in split {
for (nested_factor, multiplicity) in factor.factor() {
Self::append_integer_factor(&mut factors, nested_factor, multiplicity);
}
}
}
}
Some(factors)
}
/// Return an exact integer-polynomial square root when one exists.
/// Packed sparse inputs are reconstructed directly from their leading term
/// and known root-pair products. Larger layouts use square-free
/// decomposition to determine whether every factor multiplicity is even.
fn exact_square_root(&self) -> ExactPolynomialSquareRoot<Self> {
SparsePolynomialSquareRootContext::new(self.nvars(), self.nterms())
.reconstruct(self)
.unwrap_or_else(|| self.square_root_from_square_free_decomposition())
}
/// Return the exact square root of an integer polynomial when every
/// square-free component occurs with even multiplicity.
///
/// This is used for quadratic discriminants. The coefficient content is
/// handled separately because square-free factorization keeps it as a
/// constant rather than assigning it a multiplicity.
fn square_root_from_square_free_decomposition(&self) -> ExactPolynomialSquareRoot<Self> {
if self.is_zero() {
return ExactPolynomialSquareRoot::Root(self.zero());
}
if self.lcoeff().is_negative() {
return ExactPolynomialSquareRoot::NotSquare;
}
let content = self.content();
if content.is_negative() {
return ExactPolynomialSquareRoot::NotSquare;
}
let root_content = content.root(2);
if &root_content * &root_content != content {
return ExactPolynomialSquareRoot::NotSquare;
}
let primitive = self.clone().div_coeff(&content);
let mut root = self.constant(root_content);
for (factor, multiplicity) in primitive.square_free_factorization() {
if factor.is_one() {
continue;
}
if factor.is_constant() || multiplicity % 2 != 0 {
return ExactPolynomialSquareRoot::NotSquare;
}
root = &root * &factor.pow(multiplicity / 2);
}
assert!(
&root * &root == *self,
"square-free decomposition reconstructed an invalid polynomial square root"
);
ExactPolynomialSquareRoot::Root(root)
}
/// Split `self = a*x_var^2 + b*x_var + c` into its three coefficient
/// polynomials. All returned polynomials have degree zero in `var`.
fn quadratic_coefficients(&self, var: usize) -> Option<(Self, Self, Self)> {
if self.degree(var) != E::from_u32(2) {
return None;
}
let mut coefficients = [self.zero(), self.zero(), self.zero()];
for (coefficient, degree) in self.to_univariate_polynomial_list(var) {
let degree = degree.to_u32() as usize;
if degree > 2 {
return None;
}
coefficients[degree] = coefficient;
}
let [c, b, a] = coefficients;
if a.is_zero() { None } else { Some((a, b, c)) }
}
/// Estimate the multiplication work needed to form the discriminant
/// `b^2 - 4*a*c` when `var` is used as the quadratic variable.
fn quadratic_discriminant_cost(&self, var: usize) -> usize {
let mut terms = [0usize; 3];
for exponents in self.exponents_iter() {
let degree = exponents[var].to_u32() as usize;
if degree <= 2 {
terms[degree] += 1;
}
}
terms[1]
.saturating_mul(terms[1])
.saturating_add(terms[2].saturating_mul(terms[0]))
}
/// Check that every discriminant exponent fits the packed sparse
/// square-root representation used by this early shortcut.
fn quadratic_discriminant_fits_packed(&self, var: usize) -> bool {
let mut layer_degrees = vec![[0u32; 3]; self.nvars()];
for exponents in self.exponents_iter() {
let layer = exponents[var].to_u32() as usize;
debug_assert!(layer <= 2);
for (variable, exponent) in exponents.iter().enumerate() {
layer_degrees[variable][layer] =
layer_degrees[variable][layer].max(exponent.to_u32());
}
}
layer_degrees.into_iter().all(|degrees| {
degrees[1]
.checked_mul(2)
.zip(degrees[2].checked_add(degrees[0]))
.is_some_and(|(square, product)| square.max(product) <= u8::MAX as u32)
})
}
/// Check that both products used by `b^2 - 4*a*c` fit the polynomial's
/// exponent representation in every variable.
fn quadratic_discriminant_fits_exponents(a: &Self, b: &Self, c: &Self) -> bool {
(0..a.nvars()).all(|variable| {
let b_degree = b.degree(variable);
b_degree.checked_add(&b_degree).is_some()
&& a.degree(variable)
.checked_add(&c.degree(variable))
.is_some()
})
}
/// Reconstruct the two linear factors of a quadratic from an exact square
/// root of its discriminant. Every division and the final product are
/// checked so the method can also certify an opportunistic early split.
fn reconstruct_quadratic_split(
&self,
var: usize,
a: &Self,
b: &Self,
c: &Self,
square_root: Self,
) -> Option<[Self; 2]> {
let two = self.constant(Integer::from(2));
for signed_root in [square_root.clone(), -square_root] {
let Some(h) = (b.clone() + signed_root).try_div_owned(&two) else {
continue;
};
let g = a.gcd(&h);
if g.is_zero() {
continue;
}
let Some(a_over_g) = a.clone().try_div_owned(&g) else {
continue;
};
let Some(h_over_g) = h.try_div_owned(&g) else {
continue;
};
if h_over_g.is_zero() {
continue;
}
let Some(c_over_h) = c.clone().try_div_owned(&h_over_g) else {
continue;
};
let mut first = a_over_g;
for exponents in first.exponents_iter_mut() {
debug_assert!(exponents[var].is_zero());
exponents[var] = E::one();
}
first = first + h_over_g;
let mut second = g;
for exponents in second.exponents_iter_mut() {
debug_assert!(exponents[var].is_zero());
exponents[var] = E::one();
}
second = second + c_over_h;
if first.lcoeff().is_negative() {
first = -first;
second = -second;
}
if &first * &second == *self {
return Some([first, second]);
}
}
None
}
/// Try degree-two variables in estimated discriminant-cost order. Modular
/// nonresidues discard impossible discriminants; before the first exact
/// discriminant, two coefficient-layer pairs are checked for a factor that
/// omits another variable.
fn try_early_factor_split(&self, degrees: &[usize]) -> Option<EarlyIntegerFactorSplit<Self>> {
let degree_box_fits_packed = degrees.iter().all(|degree| *degree <= u8::MAX as usize / 2);
let mut candidates = degrees
.iter()
.enumerate()
.filter_map(|(var, degree)| {
(*degree == 2
&& (degree_box_fits_packed || self.quadratic_discriminant_fits_packed(var)))
.then(|| (self.quadratic_discriminant_cost(var), var))
})
.collect::<Vec<_>>();
candidates.sort_unstable();
if candidates.is_empty() {
return None;
}
let modular_probe_context = EarlyFactorModularProbeContext::new(degrees);
let content_context = SeparableCoefficientContentContext::after_common_monomial_removal();
let mut content_scouted = false;
for (discriminant_cost, var) in candidates {
let scout_content = !content_scouted
&& discriminant_cost
>= self
.nterms()
.saturating_mul(EARLY_SEPARABLE_CONTENT_MIN_PRODUCT_RATIO);
let modular_image = match modular_probe_context.probe_quadratic_discriminant(
self,
var,
scout_content,
) {
EarlyQuadraticDiscriminantProbe::Nonsquare(image) => {
drop(image);
continue;
}
EarlyQuadraticDiscriminantProbe::Square(image) => image,
};
if scout_content {
content_scouted = true;
if let Some(factors) = content_context.split_from_well_supported_layer_pairs(
self,
var,
&modular_probe_context,
&modular_image,
) {
return Some(EarlyIntegerFactorSplit::Separable(factors));
}
}
let (a, b, c) = self
.quadratic_coefficients(var)
.expect("a degree-two variable has three quadratic coefficient layers");
let discriminant =
b.square_for_quadratic_discriminant() - (&a * &c).mul_coeff(Integer::from(4));
let square_root = match SparsePolynomialSquareRootContext::new(
discriminant.nvars(),
discriminant.nterms(),
)
.reconstruct(&discriminant)
{
Some(ExactPolynomialSquareRoot::Root(root)) if !root.is_zero() => root,
Some(ExactPolynomialSquareRoot::Root(_))
| Some(ExactPolynomialSquareRoot::NotSquare)
| None => continue,
};
if let Some(factors) = self.reconstruct_quadratic_split(var, &a, &b, &c, square_root) {
return Some(EarlyIntegerFactorSplit::Quadratic {
variable: var,
factors,
});
}
}
None
}
/// Factor a quadratic from coefficient layers whose discriminant products
/// fit the current exponent representation.
fn factor_quadratic_from_coefficients(
&self,
var: usize,
a: &Self,
b: &Self,
c: &Self,
) -> QuadraticFactorization<Self> {
let discriminant =
b.square_for_quadratic_discriminant() - (a * c).mul_coeff(Integer::from(4));
let square_root = match discriminant.exact_square_root() {
ExactPolynomialSquareRoot::Root(root) => root,
ExactPolynomialSquareRoot::NotSquare => {
return QuadraticFactorization::Irreducible;
}
};
assert!(
!square_root.is_zero(),
"a square-free quadratic cannot have zero discriminant"
);
let factors = self
.reconstruct_quadratic_split(var, a, b, c, square_root)
.expect("quadratic discriminant factors failed exact reconstruction");
QuadraticFactorization::Split(factors)
}
/// Factor a primitive square-free polynomial that is quadratic in `var` by
/// taking the exact square root of its discriminant. Every nonconstant
/// factor must depend on `var`, as guaranteed after `factor_separable`.
///
/// For `a*x^2 + b*x + c`, let `h = (b + sqrt(b^2-4*a*c))/2` and
/// `g = gcd(a, h)`. Exact division then reconstructs the two linear factors
/// `(a/g*x + h/g)` and `(g*x + c/(h/g))`. If an intermediate exponent does
/// not fit `E`, the calculation uses `u32` exponents and converts the
/// resulting factors back to the input representation.
fn factor_quadratic_variable(&self, var: usize) -> Option<QuadraticFactorization<Self>> {
let (a, b, c) = self
.quadratic_coefficients(var)
.expect("quadratic factorization requires a degree-two variable");
if Self::quadratic_discriminant_fits_exponents(&a, &b, &c) {
return Some(self.factor_quadratic_from_coefficients(var, &a, &b, &c));
}
let wide = self.map_exp(|exponent| exponent.to_u32());
let (wide_a, wide_b, wide_c) = wide
.quadratic_coefficients(var)
.expect("widening exponents preserves the quadratic degree");
if !MultivariatePolynomial::<IntegerRing, u32, LexOrder>::
quadratic_discriminant_fits_exponents(&wide_a, &wide_b, &wide_c)
{
return None;
}
let result = wide.factor_quadratic_from_coefficients(var, &wide_a, &wide_b, &wide_c);
Some(match result {
QuadraticFactorization::Split([first, second]) => QuadraticFactorization::Split([
first.map_exp(|exponent| E::from_u32(*exponent)),
second.map_exp(|exponent| E::from_u32(*exponent)),
]),
QuadraticFactorization::Irreducible => QuadraticFactorization::Irreducible,
})
}
/// Certify that a component returned by `factor_separable` is irreducible
/// from a degree-preserving univariate specialization.
///
/// Every nonconstant factor of such a component contains every active
/// variable. A variable of degree one therefore proves irreducibility
/// directly. Otherwise, evaluating all other variables at one preserves a
/// hypothetical factorization whenever it preserves the degree in that
/// variable, so an irreducible primitive image certifies the component.
fn univariate_specialization_factorization(
&self,
degrees: &[usize],
) -> UnivariateSpecializationFactorization {
let Some((variable, degree)) = degrees
.iter()
.copied()
.enumerate()
.filter(|(_, degree)| *degree > 0)
.min_by_key(|(_, degree)| *degree)
else {
return UnivariateSpecializationFactorization::Inconclusive;
};
if degree == 1 {
#[cfg(test)]
LINEAR_VARIABLE_IRREDUCIBILITY_CERTIFICATES.with(|count| count.set(count.get() + 1));
return UnivariateSpecializationFactorization::Irreducible;
}
if degree > INTEGER_FACTOR_IRREDUCIBILITY_SCOUT_MAX_DEGREE {
return UnivariateSpecializationFactorization::Inconclusive;
}
#[cfg(test)]
UNIVARIATE_IRREDUCIBILITY_SCOUTS.with(|count| count.set(count.get() + 1));
let mut coefficients = vec![Integer::zero(); degree + 1];
for term in self {
let image_degree = term.exponents[variable].to_u32() as usize;
Z.add_assign(&mut coefficients[image_degree], term.coefficient);
}
if coefficients[degree].is_zero() {
return UnivariateSpecializationFactorization::Inconclusive;
}
let mut image = self.zero_with_capacity(coefficients.len());
let mut exponents = vec![E::zero(); self.nvars()];
for (image_degree, coefficient) in coefficients.into_iter().enumerate() {
if coefficient.is_zero() {
continue;
}
exponents[variable] = E::from_u32(image_degree as u32);
image.append_monomial_back(coefficient, &exponents);
}
let content = image.content();
let mut primitive = image.div_coeff(&content);
if primitive.lcoeff().is_negative() {
primitive = -primitive;
}
if !primitive.gcd(&primitive.derivative(variable)).is_constant() {
return UnivariateSpecializationFactorization::ReducibleImage;
}
let irreducible = primitive.factor_reconstruct().len() == 1;
#[cfg(test)]
if irreducible {
UNIVARIATE_IRREDUCIBILITY_CERTIFICATES.with(|count| count.set(count.get() + 1));
}
if irreducible {
UnivariateSpecializationFactorization::Irreducible
} else {
UnivariateSpecializationFactorization::ReducibleImage
}
}
/// Factor using the degree-two variable whose discriminant has the smallest
/// estimated coefficient-product count. After `factor_separable`, every
/// irreducible factor has positive degree in this variable. Those degrees
/// can therefore only partition two as `2` or `1 + 1`, so one quadratic
/// variable decides whether the polynomial is irreducible or splits.
fn factor_quadratic(&self, degrees: &[usize]) -> Option<QuadraticFactorization<Self>> {
let candidate = degrees
.iter()
.enumerate()
.filter_map(|(var, degree)| {
(*degree == 2).then(|| (self.quadratic_discriminant_cost(var), var))
})
.min();
candidate.and_then(|(_, var)| self.factor_quadratic_variable(var))
}
}
impl<E: PositiveExponent> Factorize for MultivariatePolynomial<IntegerRing, E, LexOrder> {
fn square_free_factorization(&self) -> Vec<(Self, usize)> {
if self.is_zero() {
return vec![];
}
let mut c = self.content();
let stripped = self.clone().div_coeff(&c);
let (monomial_powers, stripped) = stripped.remove_common_monomial();
let mut factors = vec![];
for (var, power) in monomial_powers.into_iter().enumerate() {
if power == E::zero() {
continue;
}
let mut exponents = vec![E::zero(); self.nvars()];
exponents[var] = E::one();
factors.push((
self.monomial(self.ring().one(), exponents),
power.to_u32() as usize,
));
}
let fs = stripped.factor_separable();
for mut f in fs {
// make sure f is primitive
if f.lcoeff().is_negative() {
c = -c;
f = -f;
}
let mut nf = f.square_free_factorization_0_char();
factors.append(&mut nf);
}
if !c.is_one() {
factors.insert(0, (self.constant(c), 1));
}
if factors.is_empty() {
factors.push((self.one(), 1))
}
factors
}
fn factor(&self) -> Vec<(Self, usize)> {
if let Some(factors) = self.factor_quadratic_before_square_free() {
return factors;
}
let sf = self.square_free_factorization();
let mut factors = vec![];
let mut degrees = vec![0; self.nvars()];
for (f, p) in sf {
debug!("SFF {} {}", f, p);
let mut var_count = 0;
for (v, d) in degrees.iter_mut().enumerate() {
*d = f.degree(v).to_u32() as usize;
if *d > 0 {
var_count += 1;
}
}
let minimum_active_degree = degrees.iter().copied().filter(|degree| *degree > 0).min();
let early_specialization = if var_count > 2
&& integer_factor_start_mode() == IntegerFactorStart::Auto
&& minimum_active_degree.is_some_and(|degree| degree <= 2)
{
f.univariate_specialization_factorization(°rees)
} else {
UnivariateSpecializationFactorization::Inconclusive
};
if early_specialization == UnivariateSpecializationFactorization::Irreducible {
factors.push((f, p));
continue;
}
match var_count {
0 | 1 => factors.extend(f.factor_reconstruct().into_iter().map(|ff| (ff, p))),
2 => {
let mut order: Vec<_> = degrees
.iter()
.enumerate()
.filter(|(_, d)| **d > 0)
.collect();
order.sort_by_key(|o| Reverse(o.1));
let order: Vec<_> = order.into_iter().map(|(v, _)| v).collect();
factors.extend(
f.bivariate_factor_reconstruct(order[0], order[1])
.into_iter()
.map(|ff| (ff, p)),
)
}
_ => {
if integer_factor_start_mode() != IntegerFactorStart::Disabled {
match f.factor_quadratic(°rees) {
Some(QuadraticFactorization::Split(quadratic_factors)) => {
factors.extend(quadratic_factors.into_iter().map(|ff| (ff, p)));
continue;
}
Some(QuadraticFactorization::Irreducible) => {
factors.push((f, p));
continue;
}
None => {}
}
}
// select the variable with the smallest leading coefficient and the highest degree to be first
let mut lcoeff_length = vec![0; self.nvars()];
for x in f.exponents_iter() {
for ((lc, e), d) in lcoeff_length.iter_mut().zip(x).zip(°rees) {
if e.to_i32() as usize == *d {
*lc += 1;
}
}
}
let first = (0..self.nvars())
.min_by(|a, b| {
lcoeff_length[*a]
.cmp(&lcoeff_length[*b])
.then_with(|| degrees[*b].cmp(°rees[*a]))
})
.unwrap();
// TODO: find better order
let mut order: Vec<_> = degrees
.iter()
.enumerate()
.filter(|(_, d)| **d > 0)
.collect();
order.sort_by_key(|o| {
if o.0 == first {
Reverse(&usize::MAX)
} else {
Reverse(o.1)
}
});
let mut order: Vec<_> = order.into_iter().map(|(v, _)| v).collect();
let bivariate_fallback_order = order.clone();
let reordered_for_sparse_univariate =
reorder_integer_factor_variables_for_sparse_univariate(
integer_factor_start_mode() == IntegerFactorStart::Auto,
f.nterms(),
°rees,
&lcoeff_length,
&mut order,
);
let planned_bivariate_start = integer_factor_start_mode()
== IntegerFactorStart::Auto
&& f.integer_factor_start_auto_decision(&order).0;
let specialization = if minimum_active_degree.is_some_and(|degree| degree > 2)
&& integer_factor_start_mode() == IntegerFactorStart::Auto
&& planned_bivariate_start
{
f.univariate_specialization_factorization(°rees)
} else {
UnivariateSpecializationFactorization::Inconclusive
};
if specialization == UnivariateSpecializationFactorization::Irreducible {
factors.push((f, p));
continue;
}
let prefer_univariate_start =
specialization == UnivariateSpecializationFactorization::ReducibleImage;
factors.extend(
f.multivariate_factorization(
&mut order,
10,
None,
reordered_for_sparse_univariate
.then_some(bivariate_fallback_order.as_slice()),
prefer_univariate_start,
)
.into_iter()
.map(|ff| (ff, p)),
)
}
}
}
factors
}
fn is_irreducible(&self) -> bool {
let mut sf = self.square_free_factorization();
if sf.len() > 1 {
return false;
}
let (f, _) = sf.pop().unwrap();
let mut degrees = vec![0; self.nvars()];
let mut var_count = 0;
for (v, d) in degrees.iter_mut().enumerate() {
*d = f.degree(v).to_u32() as usize;
if *d > 0 {
var_count += 1;
}
}
let minimum_active_degree = degrees.iter().copied().filter(|degree| *degree > 0).min();
let early_specialization = if var_count > 2
&& integer_factor_start_mode() == IntegerFactorStart::Auto
&& minimum_active_degree.is_some_and(|degree| degree <= 2)
{
f.univariate_specialization_factorization(°rees)
} else {
UnivariateSpecializationFactorization::Inconclusive
};
if early_specialization == UnivariateSpecializationFactorization::Irreducible {
return true;
}
match var_count {
0 | 1 => f.factor_reconstruct().len() == 1,
2 => {
let mut order: Vec<_> = degrees
.iter()
.enumerate()
.filter(|(_, d)| **d > 0)
.collect();
order.sort_by_key(|o| Reverse(o.1));
let order: Vec<_> = order.into_iter().map(|(v, _)| v).collect();
f.bivariate_factor_reconstruct(order[0], order[1]).len() == 1
}
_ => {
if integer_factor_start_mode() != IntegerFactorStart::Disabled {
match f.factor_quadratic(°rees) {
Some(QuadraticFactorization::Split(_)) => return false,
Some(QuadraticFactorization::Irreducible) => return true,
None => {}
}
}
// TODO: find better order
let mut order: Vec<_> = degrees
.iter()
.enumerate()
.filter(|(_, d)| **d > 0)
.collect();
order.sort_by_key(|o| Reverse(o.1));
let mut order: Vec<_> = order.into_iter().map(|(v, _)| v).collect();
let planned_bivariate_start = integer_factor_start_mode()
== IntegerFactorStart::Auto
&& f.integer_factor_start_auto_decision(&order).0;
let specialization = if minimum_active_degree.is_some_and(|degree| degree > 2)
&& integer_factor_start_mode() == IntegerFactorStart::Auto
&& planned_bivariate_start
{
f.univariate_specialization_factorization(°rees)
} else {
UnivariateSpecializationFactorization::Inconclusive
};
if specialization == UnivariateSpecializationFactorization::Irreducible {
return true;
}
f.multivariate_factorization(
&mut order,
10,
None,
None,
specialization == UnivariateSpecializationFactorization::ReducibleImage,
)
.len()
== 1
}
}
}
}
impl<E: PositiveExponent> Factorize for MultivariatePolynomial<RationalField, E, LexOrder> {
fn square_free_factorization(&self) -> Vec<(Self, usize)> {
if self.is_zero() {
return vec![];
}
let c = self.content();
let stripped = self.map_coeff(
|coeff| {
let coeff = self.ring().div(coeff, &c);
debug_assert!(coeff.is_integer());
coeff.numerator()
},
Z,
);
let fs = stripped.square_free_factorization();
let mut factors: Vec<_> = fs
.into_iter()
.map(|(f, e)| (f.map_coeff(|coeff| coeff.into(), Q), e))
.collect();
if !c.is_one() {
factors.push((self.constant(c), 1));
}
factors
}
fn factor(&self) -> Vec<(Self, usize)> {
if self.is_zero() {
return vec![];
}
let c = self.content();
let stripped = self.map_coeff(
|coeff| {
let coeff = self.ring().div(coeff, &c);
debug_assert!(coeff.is_integer());
coeff.numerator()
},
Z,
);
let mut factors: Vec<_> = stripped
.factor()
.into_iter()
.map(|(ff, p)| (ff.map_coeff(|coeff| coeff.into(), Q), p))
.collect();
if !c.is_one() {
factors.push((self.constant(c), 1));
}
factors
}
fn is_irreducible(&self) -> bool {
let c = self.content();
let stripped = self.map_coeff(
|coeff| {
let coeff = self.ring().div(coeff, &c);
debug_assert!(coeff.is_integer());
coeff.numerator()
},
Z,
);
stripped.is_irreducible()
}
}
impl<E: PositiveExponent> Factorize
for MultivariatePolynomial<AlgebraicExtension<RationalField>, E, LexOrder>
{
fn square_free_factorization(&self) -> Vec<(Self, usize)> {
if self.is_zero() {
return vec![];
}
let c = self.lcoeff();
let stripped = self.clone().make_monic();
let mut factors = vec![];
let fs = stripped.factor_separable();
for f in fs {
let mut nf = f.square_free_factorization_0_char();
factors.append(&mut nf);
}
if factors.is_empty() || !self.ring().is_one(&c) {
factors.insert(0, (self.constant(c), 1));
}
factors
}
/// Perform Trager's algorithm for factorization.
fn factor(&self) -> Vec<(Self, usize)> {
if self.is_zero() {
return vec![];
}
let sf = self.square_free_factorization();
let mut constant = self.ring().one();
let mut full_factors = vec![];
for (f, p) in &sf {
if f.is_constant() {
self.ring()
.mul_assign(&mut constant, self.ring().pow(&f.get_constant(), *p as u64));
continue;
}
let (v, s, g, n) = f.norm_with_shift_data();
let mut factors = n.factor();
factors.retain(|(f, _)| !f.is_constant());
if factors.len() == 1 {
full_factors.push((f.clone(), *p));
continue;
}
let mut g_f = g.to_number_field(&self.ring());
let alpha_poly = g.variable(&self.get_vars_ref()[v]).unwrap()
+ g.variable(&self.ring().poly().variables()[0]).unwrap()
* &g.constant((s as u64).into());
let last_factor = factors.len() - 1;
for (factor_index, (f, b)) in factors.into_iter().enumerate() {
debug!("Rational factor {}", f);
let gcd = if factor_index == last_factor {
// The square-free norm associates every rational factor with a unique factor
// of g, so the final unfactored remainder is the final lift.
g_f.clone()
} else {
let f = f.to_number_field(&self.ring());
let gcd = f.gcd(&g_f);
g_f = g_f
.try_div(&gcd)
.expect("the lifted norm factor must divide the shifted polynomial");
gcd
};
let g = MultivariatePolynomial::from_number_field(&gcd)
.replace_with_poly(v, &alpha_poly)
.to_number_field(&self.ring());
let lc = g.lcoeff();
self.ring()
.mul_assign(&mut constant, &self.ring().pow(&lc, (b * p) as u64));
full_factors.push((g.mul_coeff(self.ring().inv(&lc)), b * p));
}
}
if !self.ring().is_one(&constant) || full_factors.is_empty() {
full_factors.push((self.constant(constant), 1));
}
full_factors
}
fn is_irreducible(&self) -> bool {
// TODO: improve
self.factor().len() == 1
}
}
impl<
UField: FiniteFieldWorkspace,
F: GaloisField<Base = FiniteField<UField>>
+ PolynomialGCD<E>
+ SampleableRing<SamplingPolicy = RangeInclusive<i64>>,
E: PositiveExponent,
> Factorize for MultivariatePolynomial<F, E, LexOrder>
where
FiniteField<UField>: Field + FiniteFieldCore<UField> + PolynomialGCD<u16>,
<FiniteField<UField> as Set>::Element: Copy,
AlgebraicExtension<<F as GaloisField>::Base>: PolynomialGCD<E>,
{
fn square_free_factorization(&self) -> Vec<(Self, usize)> {
let c = self.lcoeff();
let stripped = self.clone().make_monic();
let mut factors = vec![];
let fs = stripped.factor_separable();
for f in fs {
let mut nf = f.square_free_factorization_bernardin();
factors.append(&mut nf);
}
if factors.is_empty() || !self.ring().is_one(&c) {
factors.push((self.constant(c), 1))
}
factors
}
fn factor(&self) -> Vec<(Self, usize)> {
if self.is_zero() {
return vec![];
}
let sf = self.square_free_factorization();
let mut factors = vec![];
let mut degrees = vec![0; self.nvars()];
for (f, p) in sf {
debug!("SFF {} {}", f, p);
let mut var_count = 0;
for v in 0..self.nvars() {
degrees[v] = f.degree(v).to_u32() as usize;
if degrees[v] > 0 {
var_count += 1;
}
}
match var_count {
0 => {
factors.push((f, p));
}
1 => {
for (d2, f2) in f.distinct_degree_factorization() {
debug!("DDF {} {}", f2, d2);
for f3 in f2.equal_degree_factorization(d2) {
debug!("EDF {} {}", f3, p);
factors.push((f3, p));
}
}
}
2 => {
let mut order: Vec<_> = degrees
.iter()
.enumerate()
.filter(|(_, d)| **d > 0)
.collect();
order.sort_by_key(|o| Reverse(o.1));
let order: Vec<_> = order.into_iter().map(|(v, _)| v).collect();
factors.extend(
f.bivariate_factorization(order[0], order[1])
.into_iter()
.map(|ff| (ff, p)),
)
}
_ => {
// TODO: find better order
let mut order: Vec<_> = degrees
.iter()
.enumerate()
.filter(|(_, d)| **d > 0)
.collect();
order.sort_by_key(|o| Reverse(o.1));
let mut order: Vec<_> = order.into_iter().map(|(v, _)| v).collect();
factors.extend(
f.multivariate_factorization(&mut order, 10, None)
.into_iter()
.map(|ff| (ff, p)),
)
}
}
}
factors
}
fn is_irreducible(&self) -> bool {
let mut sf = self.square_free_factorization();
if sf.len() > 1 {
return false;
}
let (f, p) = sf.pop().unwrap();
let mut degrees = vec![0; self.nvars()];
debug!("SFF {} {}", f, p);
let mut var_count = 0;
for v in 0..self.nvars() {
degrees[v] = f.degree(v).to_u32() as usize;
if degrees[v] > 0 {
var_count += 1;
}
}
match var_count {
0 => true,
1 => {
let mut d = f.distinct_degree_factorization();
if d.len() > 1 {
return false;
}
let (d2, f2) = d.pop().unwrap();
f2.equal_degree_factorization(d2).len() == 1
}
2 => {
let mut order: Vec<_> = degrees
.iter()
.enumerate()
.filter(|(_, d)| **d > 0)
.collect();
order.sort_by_key(|o| Reverse(o.1));
let order: Vec<_> = order.into_iter().map(|(v, _)| v).collect();
f.bivariate_factorization(order[0], order[1]).len() == 1
}
_ => {
// TODO: find better order
let mut order: Vec<_> = degrees
.iter()
.enumerate()
.filter(|(_, d)| **d > 0)
.collect();
order.sort_by_key(|o| Reverse(o.1));
let mut order: Vec<_> = order.into_iter().map(|(v, _)| v).collect();
f.multivariate_factorization(&mut order, 10, None).len() == 1
}
}
}
}
impl<
UField: FiniteFieldWorkspace,
F: GaloisField<Base = FiniteField<UField>>
+ PolynomialGCD<E>
+ SampleableRing<SamplingPolicy = RangeInclusive<i64>>,
E: PositiveExponent,
> MultivariatePolynomial<F, E, LexOrder>
where
FiniteField<UField>: Field + FiniteFieldCore<UField> + PolynomialGCD<u16>,
<FiniteField<UField> as Set>::Element: Copy,
AlgebraicExtension<<F as GaloisField>::Base>: PolynomialGCD<E>,
{
/// Bernardin's algorithm for square free factorization.
fn square_free_factorization_bernardin(&self) -> Vec<(Self, usize)> {
if self.is_constant() {
if self.is_one() {
return vec![];
} else {
return vec![(self.clone(), 1)];
}
}
let mut f = self.clone();
let mut h = HashMap::default();
let mut hr;
for var in 0..self.nvars() {
if f.degree(var) > E::zero() {
(f, hr) = f.square_free_factorization_ff_yun(var);
for (part, pow) in hr {
h.entry(pow)
.and_modify(|f| {
*f = &*f * ∂
})
.or_insert(part);
}
}
}
// take the pth root
// the coefficients remain unchanged, since x^1/p = x
// since the derivative in every var is 0, all powers are divisible by p
let p = self.ring().characteristic().to_u64().unwrap() as usize;
let mut b = f.clone();
for es in b.exponents_iter_mut() {
for e in es {
if e.is_zero() {
continue;
}
if p < u32::MAX as usize {
debug_assert_eq!(e.to_u32() as usize % p, 0);
*e = *e / E::from_u32(p as u32);
} else {
// at the moment exponents are limited to 32-bits
// so only the case where e = 0 is supported
assert!(*e == E::zero());
}
}
}
let mut factors = vec![];
let sub_factors = b.square_free_factorization_bernardin();
for (mut k, n) in sub_factors {
for (powh, hi) in &mut h {
if *powh < p {
let g = k.gcd(hi);
if !g.is_constant() {
k = k / &g;
*hi = &*hi / &g;
factors.push((g, n * p + *powh));
}
}
}
if !k.is_constant() {
factors.push((k, n * p));
}
}
for (powh, hi) in h {
if !hi.is_constant() {
factors.push((hi, powh));
}
}
factors
}
/// A modified version of Yun's square free factorization algorithm.
fn square_free_factorization_ff_yun(&self, var: usize) -> (Self, Vec<(Self, usize)>) {
let b = self.derivative(var);
let mut c = self.gcd(&b);
let mut w = self / &c;
let mut v = &b / &c;
let mut factors = vec![];
let mut i = 1;
while !w.is_constant() && i < self.ring().characteristic().to_u64().unwrap() as usize {
let z = v - w.derivative(var);
let g = w.gcd(&z);
w = w / &g;
v = z / &g;
c = c / &w;
if !g.is_one() {
factors.push((g, i));
}
i += 1
}
(c, factors)
}
/// Perform distinct degree factorization on a monic, univariate and square-free polynomial.
pub fn distinct_degree_factorization(&self) -> Vec<(usize, Self)> {
if self.is_constant() {
return vec![(0, self.clone())];
}
self.distinct_degree_factorization_bounded(None)
.unwrap()
.blocks
}
/// Compute distinct-degree blocks and their exact irreducible-factor count.
///
/// If `max_factor_count` is present, return the first proven lower bound that exceeds this
/// inclusive limit instead of completing the remaining blocks.
fn distinct_degree_factorization_bounded(
&self,
max_factor_count: Option<usize>,
) -> Result<DistinctDegreeFactorization<Self>, usize> {
let Some(var) = self.last_exponents().iter().position(|x| *x > E::zero()) else {
debug_assert!(self.is_one(), "bounded DDF requires a monic polynomial");
return Ok(DistinctDegreeFactorization {
blocks: vec![],
factor_count: 0,
});
};
let mut e = self.last_exponents().to_vec();
e[var] = E::one();
let x = self.monomial(self.ring().one(), e);
let mut factors = vec![];
let mut factor_count = 0usize;
let mut h = x.clone();
let mut f = self.clone();
let mut i: usize = 0;
if max_factor_count.is_some_and(|limit| limit == 0) {
#[cfg(test)]
{
BOUNDED_DDF_REJECTIONS.with(|rejections| rejections.set(rejections.get() + 1));
LAST_BOUNDED_DDF_REJECTION_DEGREE.with(|degree| degree.set(0));
}
return Err(1);
}
while !f.is_one() {
i += 1;
h = h.exp_mod_univariate(self.ring().size().unwrap(), &mut f);
let g = f.univariate_gcd(&(&h - &x));
if !g.is_one() {
f = f.quot_rem_univariate_monic(&g).0;
let block_degree = g.degree(var).to_u32() as usize;
debug_assert_eq!(block_degree % i, 0);
factor_count += block_degree / i;
factors.push((i, g));
}
let factor_count_lower_bound = factor_count + usize::from(!f.is_constant());
if max_factor_count.is_some_and(|limit| factor_count_lower_bound > limit) {
#[cfg(test)]
{
BOUNDED_DDF_REJECTIONS.with(|rejections| rejections.set(rejections.get() + 1));
LAST_BOUNDED_DDF_REJECTION_DEGREE.with(|degree| degree.set(i));
}
return Err(factor_count_lower_bound);
}
let residual_degree = f.last_exponents()[var].to_u32() as usize;
if residual_degree < 2 * (i + 1) {
// f cannot be split more
if !f.is_constant() {
factor_count += 1;
factors.push((residual_degree, f));
}
break;
}
}
Ok(DistinctDegreeFactorization {
blocks: factors,
factor_count,
})
}
/// Perform Cantor-Zassenhaus's probabilistic algorithm for
/// finding irreducible factors of degree `d`.
pub fn equal_degree_factorization(&self, d: usize) -> Vec<Self> {
let s = self.clone().make_monic();
let Some(var) = self.last_exponents().iter().position(|x| *x > E::zero()) else {
if d == 1 {
return vec![s];
} else {
panic!("Degree mismatch for {self}: {d}");
}
};
let n = self.degree(var).to_u32() as usize;
if n == d {
return vec![s];
}
let mut rng = rng();
let mut random_poly = self.zero_with_capacity(d);
let mut exp = vec![E::zero(); self.nvars()];
let characteristic = self.ring().characteristic();
// compute inverse
let mut s_rev = s.clone();
s_rev.reverse();
let s_inv = s_rev.inverse_univariate(var, E::from_u32(n as u32 + 1));
let factor = loop {
// generate a random non-constant polynomial
random_poly.clear();
for i in 0..n {
let upper_bound = characteristic.to_i64().unwrap_or(i64::MAX);
let r = self
.ring()
.sample(&mut rng, &(0..=upper_bound.saturating_sub(1)));
if !self.ring().is_zero(&r) {
exp[var] = E::from_u32(i as u32);
random_poly.append_monomial(r, &exp);
}
}
if random_poly.degree(var) == E::zero() {
continue;
}
*random_poly.coefficients.last_mut().unwrap() = self.ring().one();
let g = random_poly.gcd(&s);
if !g.is_one() {
break g;
}
let b = if self.ring().characteristic() == 2 {
let max = self.ring().extension_degree() as usize * d;
let mut b = random_poly.clone();
let mut vcur = b.clone();
for _ in 1..max {
vcur = (&vcur * &vcur).rem(&s);
b = b + vcur.clone();
}
b
} else {
// TODO: use Frobenius map and modular composition to prevent computing large exponent poly^(p^d)
let p = self.ring().size().unwrap();
random_poly.exp_mod_univariate_fast(
var,
&(&p.pow(d as u64) - &1i64.into()) / &2i64.into(),
&s,
&s_inv,
) - self.one()
};
if b.is_constant() {
continue;
}
let g = b.gcd(&s);
if !g.is_one() && g != s {
break g;
}
};
let mut factors = factor.equal_degree_factorization(d);
factors.extend((self / &factor).equal_degree_factorization(d));
factors
}
/// Perform distinct and equal degree factorization on a square-free univariate polynomial.
fn factor_distinct_equal_degree(&self) -> Vec<Self> {
let mut factors = vec![];
for (d2, f2) in self.distinct_degree_factorization() {
debug!("DDF {} {}", f2, d2);
for f3 in f2.equal_degree_factorization(d2) {
debug!("EDF {}", f3);
factors.push(f3);
}
}
factors
}
/// Bernardin's algorithm based on
/// "A new bivariate Hensel lifting algorithm for n factors"
/// by Garrett Paluck. The formulation of the algorithm in other sources contain serious errors.
// TODO: merge with an almost similar method for the integer case. A modification that needs
// to be made here is to make the lcoeff_y=0 monic
fn bivariate_hensel_lift_bernardin(
&self,
interpolation_var: usize,
lcoeff: &Self,
univariate_factors: &[Self],
iterations: usize,
) -> Vec<Self> {
let y_poly = self.to_univariate_polynomial_list(interpolation_var);
// add the leading coefficient as a first factor
let mut factors = vec![lcoeff.replace(interpolation_var, &self.ring().zero())];
factors.extend_from_slice(univariate_factors);
// extract coefficients in y
let mut u: Vec<_> = factors
.iter()
.map(|f| {
let mut dense = vec![self.zero(); iterations + 1];
dense[0] = f.clone();
dense
})
.collect();
// update the first polynomial as it may contain y, since it's lcoeff
let y_lcoeff = lcoeff.to_univariate_polynomial_list(interpolation_var);
for (p, e) in y_lcoeff {
u[0][e.to_u32() as usize] = p;
}
let mut p = u.clone();
let mut cur_p = p[0][0].clone();
for x in &mut p.iter_mut().skip(1) {
cur_p = cur_p * &x[0];
x[0] = cur_p.clone();
}
let delta = Self::diophantine_univariate(&mut factors, &self.one());
for k in 1..iterations {
// extract the coefficient required to compute the error in y^k
// computed using a convolution
p[0][k] = u[0][k].clone();
for i in 1..factors.len() {
for j in 0..k {
p[i][k] = &p[i][k] + &(&p[i - 1][k - j] * &u[i][j]);
}
}
// find the kth power of y in f
// since we compute the error per power of y, we cannot stop on a 0 error
let e = if let Some((v, _)) = y_poly.iter().find(|e| e.1.to_u32() as usize == k) {
v - &p.last().unwrap()[k]
} else {
-p.last().unwrap()[k].clone()
};
if e.is_zero() {
continue;
}
for ((dp, f), d) in u.iter_mut().zip(factors.iter_mut()).zip(&delta) {
dp[k] = &dp[k] + &(d * &e).quot_rem_univariate(f).1;
}
// update the coefficients with the new y^k contributions
// note that the lcoeff[k] contribution is not new
let mut t = self.zero();
for i in 1..factors.len() {
t = &u[i][0] * &t + &u[i][k] * &p[i - 1][0];
p[i][k] = &p[i][k] + &t;
}
}
// convert dense polynomials to multivariate polynomials
u.into_iter()
.map(|ts| {
let mut new_poly = self.zero_with_capacity(ts.len());
for (i, mut f) in ts.into_iter().enumerate() {
for x in f.exponents_iter_mut() {
x[interpolation_var] = E::from_u32(i as u32);
}
new_poly = new_poly + f;
}
new_poly
})
.collect()
}
/// Compute the bivariate factorization of a square-free polynomial.
fn bivariate_factorization(&self, main_var: usize, interpolation_var: usize) -> Vec<Self> {
assert!(main_var != interpolation_var);
if self.bivariate_irreducibility_test() {
return vec![self.clone()];
}
// check for problems arising from canceling terms in the derivative
let der = self.derivative(main_var);
if der.is_zero() {
return self.bivariate_factorization(interpolation_var, main_var);
}
let g = self.gcd(&der);
if !g.is_constant() {
let mut factors = g.bivariate_factorization(main_var, interpolation_var);
factors.extend((self / &g).bivariate_factorization(main_var, interpolation_var));
return factors;
}
let mut sample_point = self.ring().zero();
let mut uni_f = self.replace(interpolation_var, &sample_point);
let mut i = 0;
let mut rng = rng();
loop {
i += 1;
if self.ring().size() == Some(i.into()) {
let field = self
.ring()
.upgrade(self.ring().extension_degree().to_u64().unwrap() as usize + 1);
debug!(
"Upgrading to Galois field with exponent {}",
field.extension_degree()
);
let s_l = self.map_coeff(|c| self.ring().upgrade_element(c, &field), field.clone());
let facs = s_l.bivariate_factorization(main_var, interpolation_var);
return facs
.into_iter()
.map(|f| f.map_coeff(|c| self.ring().downgrade_element(c), self.ring().clone()))
.collect();
}
if self.degree(main_var) == uni_f.degree(main_var)
&& uni_f.gcd(&uni_f.derivative(main_var)).is_constant()
{
break;
}
sample_point = self.ring().sample(&mut rng, &(0..=i));
uni_f = self.replace(interpolation_var, &sample_point);
}
let mut d = self.degree(interpolation_var).to_u32();
let shifted_poly = if !self.ring().is_zero(&sample_point) {
self.shift_var_cached(interpolation_var, &sample_point)
} else {
self.clone()
};
let fs = uni_f.factor_distinct_equal_degree();
let mut lcoeff = shifted_poly.lcoeff_last_varorder(&[main_var, interpolation_var]);
let mut lc_d = lcoeff.degree(interpolation_var).to_u32();
let iter = (d + lc_d + 1) as usize;
let mut factors =
shifted_poly.bivariate_hensel_lift_bernardin(interpolation_var, &lcoeff, &fs, iter);
factors.swap_remove(0); // remove the lcoeff
let mut rec_factors = vec![];
// factor recombination
let mut s = 1;
let mut rest = shifted_poly;
'len: while 2 * s <= factors.len() {
let mut fs = CombinationIterator::new(factors.len(), s);
while let Some(cs) = fs.next() {
// TODO: multiply in the leading coefficient here,
// then we can skip the Pade approximation and reduce the
// number of iterations in the Hensel lifting to d + 1, like in the integer case?
let mut g = rest.constant(rest.lcoeff());
for (i, f) in factors.iter().enumerate() {
if cs.contains(&i) {
g = (&g * f).mod_var(interpolation_var, E::from_u32(iter as u32 + 1));
}
}
let y_polys: Vec<_> = g
.to_univariate_polynomial_list(main_var)
.into_iter()
.map(|(x, _)| x)
.collect();
let mut g_lcoeff = Self::lcoeff_reconstruct(&y_polys, d, lc_d);
g = (&g * &g_lcoeff)
.mod_var(interpolation_var, E::from_u32(d + 1))
.make_monic();
let (h, r) = rest.quot_rem(&g, true);
if r.is_zero() {
rec_factors.push(g);
for i in cs.iter().rev() {
factors.remove(*i);
}
rest = h;
lcoeff = lcoeff.quot_rem_univariate(&mut g_lcoeff).0;
lc_d = lcoeff.degree(interpolation_var).to_u32();
d = rest.degree(interpolation_var).to_u32();
continue 'len;
}
}
s += 1;
}
rec_factors.push(rest);
if !self.ring().is_zero(&sample_point) {
for x in &mut rec_factors {
// shift the polynomial to y - sample
*x = x.shift_var_cached(interpolation_var, &self.ring().neg(&sample_point));
}
}
rec_factors
}
/// Reconstruct the leading coefficient using a Pade approximation with numerator degree `deg_n` and
/// denominator degree `deg_d`. The resulting denominator should be a factor of the leading coefficient.
fn lcoeff_reconstruct(coeffs: &[Self], deg_n: u32, deg_d: u32) -> Self {
let mut lcoeff = coeffs[0].constant(coeffs[0].ring().one());
for x in coeffs {
let d = x.rational_approximant_univariate(deg_n, deg_d).unwrap().1;
if !d.is_one() {
let g = d.gcd(&lcoeff);
lcoeff = lcoeff * &(d / &g);
}
}
lcoeff
}
/// Sort the bivariate factors based on their univariate image so that they are
/// aligned between the different vars.
fn canonical_sort(
biv_polys: &[Self],
replace_var: usize,
sample_points: &[(usize, <F as Set>::Element)],
) -> Vec<(Self, <F as Set>::Element, Self)> {
let mut univariate_factors = biv_polys
.iter()
.map(|f| {
let mut u = f.clone();
for (v, p) in sample_points {
if *v == replace_var {
u = u.replace(*v, p);
}
}
(f.clone(), u.lcoeff(), u.make_monic())
})
.collect::<Vec<_>>();
univariate_factors.sort_by(|(_, _, a), (_, _, b)| {
a.exponents
.cmp(&b.exponents)
.then(a.coefficients.internal_cmp(&b.coefficients))
});
univariate_factors
}
/// Precompute the leading coefficients of the polynomial factors, using an
/// adapted version of Kaltofen's algorithm that has modifications of Martin Lee and Stanislav Poslavsky.
fn lcoeff_precomputation(
&self,
bivariate_factors: &[Self],
sample_points: &[(usize, <F as Set>::Element)],
order: &[usize],
) -> Result<(Vec<Self>, Vec<Self>), usize> {
let lcoeff = self.univariate_lcoeff(order[0]);
let sqf = lcoeff.square_free_factorization();
let mut lcoeff_square_free = self.one();
for (f, _) in &sqf {
lcoeff_square_free = lcoeff_square_free * f;
}
let sorted_main_factors = Self::canonical_sort(bivariate_factors, order[1], sample_points);
let mut true_lcoeffs: Vec<_> = sorted_main_factors
.iter()
.map(|(_, u, _)| self.constant(u.clone()))
.collect();
let main_bivariate_factors: Vec<_> =
sorted_main_factors.into_iter().map(|(f, _, _)| f).collect();
let mut lcoeff_left = lcoeff.clone();
for f in &true_lcoeffs {
lcoeff_left = lcoeff_left / f;
}
// TODO: smarter ordering
for (i, &var) in order[1..].iter().enumerate() {
if lcoeff_left.is_one() {
break;
}
if lcoeff_left.degree(var).is_zero() {
continue;
}
// only construct factors that depend on var
let c = lcoeff_square_free.univariate_content(var);
// make sure that the content removal does not change the unit
let mut c_eval = c.clone();
for (v, p) in sample_points {
c_eval = c_eval.replace(*v, p);
}
let lcoeff_square_free_pp = &lcoeff_square_free / &c * &c_eval;
debug!("Content-free lcsqf {}", lcoeff_square_free_pp);
// check if the evaluated leading coefficient remains square free
let mut poly_eval = lcoeff_square_free_pp.clone();
for (v, p) in sample_points {
if *v != var {
poly_eval = poly_eval.replace(*v, p);
}
}
let sqf = poly_eval.square_free_factorization();
if sqf.len() != 1 || sqf[0].1 != 1 {
debug!("Polynomial is not square free: {}", poly_eval);
return Err(main_bivariate_factors.len());
}
let bivariate_factors = if var == order[1] {
main_bivariate_factors.to_vec()
} else {
let mut poly_eval = self.clone();
for (v, p) in sample_points {
if *v != var {
poly_eval = poly_eval.replace(*v, p);
}
}
if poly_eval.degree(order[0]) != self.degree(order[0])
|| poly_eval.degree(var) != self.degree(var)
|| poly_eval.univariate_lcoeff(order[0]).degree(var) != lcoeff.degree(var)
{
debug!("Bad sample for reconstructing lcoeff: degrees do not match");
return Err(main_bivariate_factors.len());
}
let bivariate_factors: Vec<_> =
poly_eval.factor().into_iter().map(|(f, _)| f).collect();
if bivariate_factors.len() != main_bivariate_factors.len() {
return Err(bivariate_factors.len().min(main_bivariate_factors.len()));
}
Self::canonical_sort(&bivariate_factors, var, sample_points)
.into_iter()
.map(|(f, _, _)| f)
.collect()
};
let square_free_lc_biv_factors: Vec<_> = bivariate_factors
.iter()
.map(|f| f.univariate_lcoeff(order[0]).square_free_factorization())
.collect();
let basis = Self::gcd_free_basis(
square_free_lc_biv_factors
.iter()
.flatten()
.map(|x| x.0.clone())
.filter(|x| !x.is_constant())
.collect(),
);
if basis.is_empty() {
continue;
}
let lifted = if basis.len() == 1 {
vec![lcoeff_square_free_pp.clone()]
} else {
let mut new_order = order.to_vec();
new_order.swap(1, i + 1);
new_order.remove(0);
lcoeff_square_free_pp.multivariate_hensel_lift_with_auto_lcoeff_fixing(
&basis,
sample_points,
&new_order,
)
};
for (l, fac) in true_lcoeffs.iter_mut().zip(&square_free_lc_biv_factors) {
let mut contrib = self.one();
for (full, b) in lifted.iter().zip(&basis) {
// check if a GCD-free basis element is a factor of the leading coefficient of this bivariate factor
if let Some((_, m)) = fac.iter().find(|(f, _)| f == b || f.try_div(b).is_some())
{
for _ in 0..*m {
contrib = &contrib * full;
}
}
}
let g = contrib.gcd(l);
let mut new = contrib / &g;
// make sure the new part keeps the desired image coeff intact
let mut b_lc_eval = new.clone();
for (v, p) in sample_points {
b_lc_eval = b_lc_eval.replace(*v, p);
}
new = new / &b_lc_eval;
*l = &*l * &new;
lcoeff_left = &lcoeff_left / &new;
}
}
if !lcoeff_left.is_one() {
panic!(
"Could not reconstruct leading coefficient of {self}: order={order:?}, samples={sample_points:?} Rest = {lcoeff_left}"
);
}
Ok((main_bivariate_factors, true_lcoeffs))
}
fn multivariate_hensel_lift_with_auto_lcoeff_fixing(
&self,
factors: &[Self],
sample_points: &[(usize, <F as Set>::Element)],
order: &[usize],
) -> Vec<Self> {
let lcoeff = self.univariate_lcoeff(order[0]);
if lcoeff.is_constant() {
// the factors should be properly normalized
let (mut uni, delta) =
Self::univariate_diophantine_field(factors, order, sample_points);
return self
.multivariate_hensel_lifting(
factors,
&mut uni,
&delta,
sample_points,
None,
order,
MultivariateHenselContext::new(1),
)
.unwrap();
}
// repeat the leading coefficient for every factor so that the leading coefficient is known
let padded_lcoeffs = vec![lcoeff.clone(); factors.len()];
let mut self_adjusted = self.clone();
for _ in 1..factors.len() {
self_adjusted = &self_adjusted * &lcoeff;
}
// set the proper lc
let mut lc_var_eval = lcoeff.clone();
for (v, p) in sample_points {
if *v != order[0] {
lc_var_eval = lc_var_eval.replace(*v, p);
}
}
let adjusted_factors: Vec<_> = factors
.iter()
.map(|f| f.clone().make_monic() * &lc_var_eval)
.collect();
let (mut uni, delta) =
Self::univariate_diophantine_field(&adjusted_factors, order, sample_points);
self_adjusted
.multivariate_hensel_lifting(
&adjusted_factors,
&mut uni,
&delta,
sample_points,
Some(&padded_lcoeffs),
order,
MultivariateHenselContext::new(1),
)
.unwrap()
.into_iter()
.map(|f| {
let c = f.univariate_content(order[0]);
f / &c
})
.collect()
}
fn univariate_diophantine_field(
factors: &[Self],
order: &[usize],
sample_points: &[(usize, <F as Set>::Element)],
) -> (Vec<Self>, Vec<Self>) {
// produce univariate factors and univariate delta
let mut univariate_factors = factors.to_vec();
for f in &mut univariate_factors {
for (v, s) in sample_points {
if order[0] != *v {
*f = f.replace(*v, s);
}
}
}
let univariate_deltas = Self::diophantine_univariate(
&mut univariate_factors,
&factors[0].constant(factors[0].ring().one()),
);
(univariate_factors, univariate_deltas)
}
/// Perform multivariate factorization on a square-free polynomial.
fn multivariate_factorization(
&self,
order: &mut [usize],
mut coefficient_upper_bound: u64,
max_bivariate_factors: Option<usize>,
) -> Vec<Self> {
if let Some(m) = max_bivariate_factors
&& m == 1
{
return vec![self.clone()];
}
// check for problems arising from canceling terms in the derivative
let der = self.derivative(order[0]);
if der.is_zero() {
let mut new_order = order.to_vec();
let v = new_order.remove(0);
new_order.push(v);
return self.multivariate_factorization(
&mut new_order,
coefficient_upper_bound,
max_bivariate_factors,
);
}
let g = self.gcd(&der);
if !g.is_constant() {
let mut factors =
g.multivariate_factorization(order, coefficient_upper_bound, max_bivariate_factors);
factors.extend((self / &g).multivariate_factorization(
order,
coefficient_upper_bound,
max_bivariate_factors,
));
return factors;
}
// select a suitable evaluation point
let mut sample_points: Vec<_> = order[1..]
.iter()
.map(|i| (*i, self.ring().zero()))
.collect();
let mut uni_f;
let mut biv_f;
let mut rng = rng();
let degree = self.degree(order[0]);
let uni_lcoeff = self.univariate_lcoeff(order[0]);
let mut content_fail_count = 0;
let mut sample_fail = Integer::zero();
'new_sample: loop {
sample_fail += &1.into();
if &sample_fail * &2.into() > self.ring().size().unwrap() {
// the field is too small, upgrade
let field = self
.ring()
.upgrade(self.ring().extension_degree().to_u64().unwrap() as usize + 1);
debug!(
"Upgrading to Galois field with exponent {}",
field.extension_degree()
);
let s_l = self.map_coeff(|c| self.ring().upgrade_element(c, &field), field.clone());
let facs = s_l.multivariate_factorization(
order,
coefficient_upper_bound,
max_bivariate_factors,
);
return facs
.into_iter()
.map(|f| f.map_coeff(|c| self.ring().downgrade_element(c), self.ring().clone()))
.collect();
}
for s in &mut sample_points {
s.1 = self
.ring()
.nth(rng.random_range(0..=coefficient_upper_bound).into());
}
biv_f = self.clone();
for ((v, s), rem_var) in sample_points[1..].iter().zip(&order[1..]).rev() {
biv_f = biv_f.replace(*v, s);
if biv_f.degree(*rem_var) != self.degree(*rem_var) {
coefficient_upper_bound += 10;
continue 'new_sample;
}
}
// requirement for leading coefficient precomputation
if biv_f.univariate_lcoeff(order[0]).degree(order[1]) != uni_lcoeff.degree(order[1]) {
debug!(
"Degree of x{} in leading coefficient of bivariate image is wrong",
order[1]
);
coefficient_upper_bound += 10;
continue 'new_sample;
}
let biv_df = biv_f.derivative(order[0]);
uni_f = biv_f.replace(sample_points[0].0, &sample_points[0].1);
let uni_df = uni_f.derivative(order[0]);
if degree == biv_f.degree(order[0])
&& degree == uni_f.degree(order[0])
&& biv_f.gcd(&biv_df).is_constant()
&& uni_f.gcd(&uni_df).is_constant()
{
if !biv_f.univariate_content(order[0]).is_constant() {
content_fail_count += 1;
debug!("Univariate content is not constant");
if content_fail_count == 4 {
// it is likely that we will always find content for this variable ordering, so change the
// second variable
// TODO: is this guaranteed to work or should we also change the first variable?
let sec_var = order[1];
order.copy_within(2..order.len(), 1);
order[order.len() - 1] = sec_var;
for ((vs, _), v) in sample_points.iter_mut().zip(&order[1..]) {
*vs = *v;
}
debug!("Changed the second variable to {}", order[1]);
content_fail_count = 0;
}
} else {
break;
}
}
coefficient_upper_bound += 10;
}
for (v, s) in &sample_points {
debug!("Sample point {}={}", v, self.ring().printer(s));
}
let bivariate_factors = biv_f.bivariate_factorization(order[0], order[1]);
if bivariate_factors.len() == 1 {
// the polynomial is irreducible
return vec![self.clone()];
}
if let Some(max) = max_bivariate_factors
&& bivariate_factors.len() > max
{
return self.multivariate_factorization(
order,
coefficient_upper_bound,
max_bivariate_factors,
);
}
let (sorted_biv_factors, true_lcoeffs) =
match self.lcoeff_precomputation(&bivariate_factors, &sample_points, order) {
Ok((sorted_biv_factors, true_lcoeffs)) => (sorted_biv_factors, true_lcoeffs),
Err(max_biv) => {
// the leading coefficient computation failed because the bivaraite factorization was wrong
// try again with other sample points and a better bound
return self.multivariate_factorization(
order,
coefficient_upper_bound + 10,
Some(max_biv),
);
}
};
for (b, l) in sorted_biv_factors.iter().zip(&true_lcoeffs) {
debug!("Bivariate factor {} with true lcoeff {}", b, l);
}
let sorted_biv_factors = self.impose_true_lcoeffs_on_factors(
&sorted_biv_factors,
&true_lcoeffs,
&sample_points,
order,
2,
);
let (mut uni, delta) = MultivariatePolynomial::univariate_diophantine_field(
&sorted_biv_factors,
order,
&sample_points,
);
let factorization = match self.multivariate_hensel_lifting(
&sorted_biv_factors,
&mut uni,
&delta,
&sample_points,
Some(&true_lcoeffs),
order,
MultivariateHenselContext::new(2),
) {
Ok(factorization) => factorization,
Err(_) => {
return self.multivariate_factorization(
order,
coefficient_upper_bound + 10,
max_bivariate_factors,
);
}
};
// test the factorization
let mut test = self.one();
for f in &factorization {
debug!("Factor = {}", f);
test = &test * f;
}
if &test == self {
factorization
} else {
debug!(
"No immediate factorization of {} for sample points {:?}",
self, sample_points
);
// the bivariate factorization has too many factors, try again with other sample points
self.multivariate_factorization(
order,
coefficient_upper_bound + 10,
Some(max_bivariate_factors.unwrap_or(bivariate_factors.len()) - 1),
)
}
}
}
impl<F: Field + SampleableRing<SamplingPolicy = RangeInclusive<i64>>, E: PositiveExponent>
MultivariatePolynomial<F, E, LexOrder>
{
fn multivariate_diophantine(
univariate_deltas: &[Self],
univariate_factors: &mut [Self],
prods: &[Self],
error: &Self,
order: &[usize],
sample_points: &[(usize, F::Element)],
degrees: &[usize],
mod_vars: &[MultivariatePolynomial<F, E, LexOrder>],
) -> Vec<Self> {
if order.len() == 1 {
return univariate_deltas
.iter()
.zip(univariate_factors)
.map(|(d, f)| (d * error).quot_rem_univariate(f).1)
.collect();
}
let last_var = *order.last().unwrap();
let shift = &sample_points.iter().find(|s| s.0 == last_var).unwrap().1;
let prods_mod = prods
.iter()
.map(|f| f.replace(last_var, shift))
.collect::<Vec<_>>();
let error_mod = error.replace(last_var, shift);
let previous_mod_vars = &mod_vars[..mod_vars.len().saturating_sub(1)];
debug!("dioph e[x{}^0] = {}", last_var, error_mod);
let mut deltas = Self::multivariate_diophantine(
univariate_deltas,
univariate_factors,
&prods_mod,
&error_mod,
&order[..order.len() - 1],
sample_points,
°rees[..order.len() - 1],
&mod_vars[..order.len() - 1],
);
for d in &mut deltas {
for m in mod_vars {
*d = d.quot_rem(m, false).1;
}
}
let mut exp = vec![E::zero(); error.nvars()];
exp[last_var] = E::one();
let var_pow = error
.monomial(error.ring().one(), exp)
.shift_var(last_var, &error.ring().neg(shift));
let mut cur_exponent;
let mut next_exponent = var_pow.clone();
for j in 1..=*degrees.last().unwrap() {
cur_exponent = next_exponent.clone();
next_exponent = &next_exponent * &var_pow;
let mut e = error.clone();
for (d, p) in deltas.iter().zip(prods) {
debug!("delta {} p {}", d, p);
e = &e - &(d * p);
for m in previous_mod_vars {
e = e.quot_rem(m, false).1;
}
// TODO: mod with (x-shift)^(j+1)?
// then we cannot break on 0 error
}
debug!("dioph e at x{}^{} = {}", last_var, j, e);
if e.is_zero() {
break;
}
// Take the jth Taylor coefficient in the current variable. Lower
// powers may survive as representatives in the ambient polynomial
// ring, but they do not contribute to this coefficient.
let shifted_e = e.shift_var(last_var, shift);
let mut e_mod = error.zero();
for (coefficient, degree) in shifted_e.to_univariate_polynomial_list(last_var) {
if degree.to_u32() as usize == j {
e_mod = e_mod + coefficient;
}
}
debug!("dioph e[x{}^{}] = {}", last_var, j, e_mod);
if e_mod.is_zero() {
continue;
}
let mut new_deltas = Self::multivariate_diophantine(
univariate_deltas,
univariate_factors,
&prods_mod,
&e_mod,
&order[..order.len() - 1],
sample_points,
°rees[..order.len() - 1],
&mod_vars[..order.len() - 1],
);
for (d, nd) in deltas.iter_mut().zip(&mut new_deltas) {
debug!("dioph d[x{}^{}] = {}", last_var, j, nd);
// multiply (y-s)^j and keep the accumulated correction reduced
let nd = &*nd * &cur_exponent;
*d = &*d + &nd;
for m in mod_vars {
*d = d.quot_rem(m, false).1;
}
}
}
deltas
}
fn sparse_multivariate_diophantine_from_skeleton(
factors: &[Self],
prods: &[Self],
error: &Self,
skeletons: &[Self],
order: &[usize],
context: &mut SparseDiophantineContext<Self, F::Element>,
) -> Option<Vec<Self>> {
if prods.len() != skeletons.len() {
return None;
}
if let Some(deltas) = Self::sparse_multivariate_diophantine_two_factor_by_sampling(
factors, prods, error, skeletons, order, context,
) {
return Some(deltas);
}
if factors.len() > 2 {
if let Some(deltas) = Self::sparse_multivariate_diophantine_by_sampling(
factors, prods, error, skeletons, order,
) {
return Some(deltas);
}
}
None
}
fn evaluate_using_exponents_univariate_grouped(
&self,
exp_evals: &[F::Element],
main_var: usize,
out: &mut Self,
) {
out.clear();
let degree = self.degree(main_var).to_u32() as usize;
let mut coefficients = vec![self.ring().zero(); degree + 1];
for (term, eval) in self.into_iter().zip(exp_evals) {
let degree = term.exponents[main_var].to_u32() as usize;
self.ring()
.add_mul_assign(&mut coefficients[degree], term.coefficient, eval);
}
let mut exponent = vec![E::zero(); self.nvars()];
for (degree, coefficient) in coefficients.into_iter().enumerate() {
if !self.ring().is_zero(&coefficient) {
exponent[main_var] = E::from_u32(degree as u32);
out.coefficients.push(coefficient);
out.exponents.extend_from_slice(&exponent);
exponent[main_var] = E::zero();
}
}
}
/// Evaluate all sampled variables along a geometric sequence while retaining
/// `x` and `y` as a dense bivariate coefficient grid. The first image uses
/// the base points themselves, and each following image raises them to the
/// next positive integer power.
fn evaluate_geometric_bivariate_images(
&self,
x: usize,
y: usize,
base_points: &[(usize, F::Element)],
sample_count: usize,
cache: &mut [Vec<F::Element>],
) -> Vec<DenseBivariateImage<F::Element>> {
debug_assert_ne!(x, y);
debug_assert!(
base_points
.iter()
.all(|(variable, _)| *variable != x && *variable != y)
);
if sample_count == 0 {
return Vec::new();
}
let x_len = self.degree(x).to_u32() as usize + 1;
let y_len = self.degree(y).to_u32() as usize + 1;
let image_len = x_len
.checked_mul(y_len)
.expect("dense bivariate image is too large");
let mut offsets = vec![0usize; image_len + 1];
for term in self {
let x_degree = term.exponents[x].to_u32() as usize;
let y_degree = term.exponents[y].to_u32() as usize;
offsets[y_degree * x_len + x_degree + 1] += 1;
}
for index in 1..offsets.len() {
offsets[index] += offsets[index - 1];
}
// Group terms by output cell so that each sample sums and advances all
// geometric sequences in a cell with one coefficient-domain operation.
let mut positions = offsets[..image_len].to_vec();
let mut grouped = std::iter::repeat_with(|| None)
.take(self.nterms())
.collect::<Vec<Option<(F::Element, F::Element)>>>();
let ratios = self.evaluate_exponents(base_points, cache);
for (term, ratio) in self.into_iter().zip(ratios) {
let x_degree = term.exponents[x].to_u32() as usize;
let y_degree = term.exponents[y].to_u32() as usize;
let cell = y_degree * x_len + x_degree;
let position = positions[cell];
positions[cell] += 1;
let current = self.ring().mul(term.coefficient, &ratio);
grouped[position] = Some((current, ratio));
}
let mut current = Vec::with_capacity(grouped.len());
let mut ratios = Vec::with_capacity(grouped.len());
for entry in grouped {
let (value, ratio) = entry.expect("every image term must belong to one dense cell");
current.push(value);
ratios.push(ratio);
}
let geometric_sequence_kernels = self.ring().kernels().geometric_sequences();
let mut images = Vec::with_capacity(sample_count);
for sample_index in 0..sample_count {
let mut coefficients = vec![self.ring().zero(); image_len];
for (cell, range) in offsets.windows(2).enumerate() {
let [start, end] = [range[0], range[1]];
if start == end {
continue;
}
let current_cell = &mut current[start..end];
let ratio_cell = &ratios[start..end];
coefficients[cell] = (sample_index + 1 < sample_count)
.then(|| geometric_sequence_kernels)
.flatten()
.and_then(|kernels| {
kernels.try_sum_and_advance_geometric_sequences(
GeometricSequenceStepRequest {
current: &mut *current_cell,
ratios: ratio_cell,
},
)
})
.unwrap_or_else(|| {
let mut coefficient = self.ring().zero();
for (current, ratio) in current_cell.iter_mut().zip(ratio_cell) {
self.ring().add_assign(&mut coefficient, &*current);
if sample_index + 1 < sample_count {
self.ring().mul_assign(current, ratio);
}
}
coefficient
});
}
images.push(DenseBivariateImage {
x_len,
y_len,
coefficients,
});
}
images
}
/// Convert one dense `x` row of a bivariate image to a polynomial that
/// contains only the retained variable `x`.
fn dense_bivariate_x_row_to_univariate(
&self,
image: &DenseBivariateImage<F::Element>,
x: usize,
y_degree: usize,
) -> Self {
let mut row = self.zero_with_capacity(image.x_len);
if y_degree >= image.y_len {
return row;
}
let mut exponent = vec![E::zero(); self.nvars()];
for x_degree in 0..image.x_len {
let coefficient = image.coefficient(x_degree, y_degree);
if !self.ring().is_zero(coefficient) {
exponent[x] = E::from_u32(x_degree as u32);
row.append_monomial(coefficient.clone(), &exponent);
exponent[x] = E::zero();
}
}
row
}
/// Assemble a polynomial from coefficients in increasing powers of `y`.
fn from_dense_y_coefficients(&self, coefficients: &[Self], y: usize) -> Self {
let capacity = coefficients.iter().map(Self::nterms).sum();
let mut polynomial = self.zero_with_capacity(capacity);
for (y_degree, coefficient) in coefficients.iter().enumerate() {
for term in coefficient {
let mut exponent = term.exponents.to_vec();
debug_assert_eq!(exponent[y], E::zero());
exponent[y] = E::from_u32(y_degree as u32);
polynomial.append_monomial(term.coefficient.clone(), &exponent);
}
}
polynomial
}
/// Compute one coefficient row of `target - left * right` in the retained
/// variable `y`, with the row represented densely in `x`.
fn dense_bivariate_product_error_row(
&self,
target: &DenseBivariateImage<F::Element>,
left: &DenseBivariateImage<F::Element>,
right: &DenseBivariateImage<F::Element>,
y_degree: usize,
) -> Vec<F::Element> {
let product_x_len = left
.x_len
.checked_add(right.x_len)
.and_then(|length| length.checked_sub(1))
.expect("dense bivariate product image is too large");
let x_len = target.x_len.max(product_x_len);
let mut error = vec![self.ring().zero(); x_len];
if y_degree < target.y_len {
for (x_degree, coefficient) in error.iter_mut().enumerate().take(target.x_len) {
*coefficient = target.coefficient(x_degree, y_degree).clone();
}
}
for left_y_degree in 0..=y_degree.min(left.y_len.saturating_sub(1)) {
let right_y_degree = y_degree - left_y_degree;
if right_y_degree >= right.y_len {
continue;
}
for left_x_degree in 0..left.x_len {
let left_coefficient = left.coefficient(left_x_degree, left_y_degree);
if self.ring().is_zero(left_coefficient) {
continue;
}
for right_x_degree in 0..right.x_len {
let right_coefficient = right.coefficient(right_x_degree, right_y_degree);
if !self.ring().is_zero(right_coefficient) {
let product = self.ring().mul(left_coefficient, right_coefficient);
self.ring()
.sub_assign(&mut error[left_x_degree + right_x_degree], product);
}
}
}
}
error
}
/// Pack the nonzero coefficients of a dense univariate polynomial for the
/// coefficient-domain multiplication kernels.
fn dense_indexed_univariate_from_coefficients(
ring: &F,
coefficients: &[F::Element],
) -> Option<DenseIndexedUnivariate<F::Element>> {
let mut packed_coefficients = Vec::new();
let mut indices = Vec::new();
for (degree, coefficient) in coefficients.iter().enumerate() {
if !ring.is_zero(coefficient) {
packed_coefficients.push(coefficient.clone());
indices.push(u32::try_from(degree).ok()?);
}
}
Some(DenseIndexedUnivariate {
coefficients: packed_coefficients,
indices,
})
}
/// Convert a polynomial containing only `x` to the packed univariate form
/// used by the dense correction solver.
fn dense_indexed_univariate(
&self,
polynomial: &Self,
x: usize,
) -> Option<DenseIndexedUnivariate<F::Element>> {
if x >= polynomial.nvars() {
return None;
}
let mut coefficients = vec![self.ring().zero(); polynomial.degree(x).to_u32() as usize + 1];
for term in polynomial {
if term
.exponents
.iter()
.enumerate()
.any(|(variable, exponent)| variable != x && *exponent != E::zero())
{
return None;
}
let degree = term.exponents[x].to_u32() as usize;
self.ring()
.add_assign(&mut coefficients[degree], term.coefficient);
}
Self::dense_indexed_univariate_from_coefficients(&self.ring(), &coefficients)
}
/// Normalize the constant `y` row of an image to a monic polynomial in
/// `x`, omitting its leading coefficient because it is one.
fn dense_monic_modulus(
&self,
image: &DenseBivariateImage<F::Element>,
) -> Option<DenseMonicModulus<F::Element>> {
if image.x_len == 0 || image.y_len == 0 {
return None;
}
let leading = image.coefficient(image.x_len - 1, 0);
if self.ring().is_zero(leading) {
return None;
}
let leading_inverse = self.ring().inv(leading);
let lower_coefficients = (0..image.x_len - 1)
.map(|degree| {
self.ring()
.mul(image.coefficient(degree, 0), &leading_inverse)
})
.collect();
Some(DenseMonicModulus { lower_coefficients })
}
/// Multiply two packed univariate polynomials and return a dense
/// coefficient vector. Prime fields use their delayed-reduction kernel.
fn dense_univariate_mul(
ring: &F,
left: &DenseIndexedUnivariate<F::Element>,
right: &DenseIndexedUnivariate<F::Element>,
) -> Option<Vec<F::Element>> {
let (Some(&left_degree), Some(&right_degree)) = (left.indices.last(), right.indices.last())
else {
return Some(Vec::new());
};
let output_len = (left_degree as usize)
.checked_add(right_degree as usize)?
.checked_add(1)?;
if let Some(product) = ring.kernels().polynomial().and_then(|kernels| {
kernels.try_dense_mul(DensePolynomialMulRequest {
output_len,
left_coefficients: &left.coefficients,
left_indices: &left.indices,
right_coefficients: &right.coefficients,
right_indices: &right.indices,
})
}) {
let mut coefficients = vec![ring.zero(); output_len];
for (degree, coefficient) in product {
let degree = degree as usize;
if degree >= output_len {
return None;
}
coefficients[degree] = coefficient;
}
while coefficients
.last()
.is_some_and(|coefficient| ring.is_zero(coefficient))
{
coefficients.pop();
}
return Some(coefficients);
}
let mut coefficients = vec![ring.zero(); output_len];
for (left_coefficient, &left_degree) in left.coefficients.iter().zip(&left.indices) {
for (right_coefficient, &right_degree) in right.coefficients.iter().zip(&right.indices)
{
ring.add_mul_assign(
&mut coefficients[left_degree as usize + right_degree as usize],
left_coefficient,
right_coefficient,
);
}
}
while coefficients
.last()
.is_some_and(|coefficient| ring.is_zero(coefficient))
{
coefficients.pop();
}
Some(coefficients)
}
/// Reduce a dense univariate polynomial in place modulo a cached monic
/// polynomial and return its canonical remainder.
fn dense_remainder_monic(
ring: &F,
mut coefficients: Vec<F::Element>,
modulus: &DenseMonicModulus<F::Element>,
) -> Vec<F::Element> {
let modulus_degree = modulus.lower_coefficients.len();
if modulus_degree == 0 {
return Vec::new();
}
while coefficients.len() > modulus_degree {
let leading = coefficients.pop().unwrap();
if ring.is_zero(&leading) {
continue;
}
let shift = coefficients.len() - modulus_degree;
for (coefficient, modulus_coefficient) in coefficients[shift..]
.iter_mut()
.zip(&modulus.lower_coefficients)
{
ring.sub_mul_assign(coefficient, modulus_coefficient, &leading);
}
}
while coefficients
.last()
.is_some_and(|coefficient| ring.is_zero(coefficient))
{
coefficients.pop();
}
coefficients
}
/// Solve both modular correction equations for one sampled error row and
/// pad the remainders to their factor image widths.
fn dense_two_factor_corrections(
&self,
error: &[F::Element],
context: &DenseTwoFactorCorrectionContext<F::Element>,
output_lens: [usize; 2],
) -> Option<[Vec<F::Element>; 2]> {
if error
.iter()
.all(|coefficient| self.ring().is_zero(coefficient))
{
return Some([
vec![self.ring().zero(); output_lens[0]],
vec![self.ring().zero(); output_lens[1]],
]);
}
let error = Self::dense_indexed_univariate_from_coefficients(&self.ring(), error)?;
let solve = |factor_index: usize| {
let product = Self::dense_univariate_mul(
&self.ring(),
&context.multipliers[factor_index],
&error,
)?;
let mut correction =
Self::dense_remainder_monic(&self.ring(), product, &context.moduli[factor_index]);
if correction.len() > output_lens[factor_index] {
return None;
}
correction.resize(output_lens[factor_index], self.ring().zero());
Some(correction)
};
Some([solve(0)?, solve(1)?])
}
/// Compare `left * right` with `self` by accumulating coefficient
/// differences under exact mixed-radix exponent keys.
fn product_matches_by_packed_accumulation(&self, left: &Self, right: &Self) -> Option<bool> {
if self.nvars() != left.nvars()
|| self.nvars() != right.nvars()
|| self.ring() != left.ring()
|| self.ring() != right.ring()
|| self.variables() != left.variables()
|| self.variables() != right.variables()
|| !self.is_polynomial()
|| !left.is_polynomial()
|| !right.is_polynomial()
{
return None;
}
let mut strides = Vec::with_capacity(self.nvars());
let mut radices = Vec::with_capacity(self.nvars());
let mut stride = 1u128;
for variable in 0..self.nvars() {
let maximum_degree = left
.degree(variable)
.to_u32()
.checked_add(right.degree(variable).to_u32())?;
let radix = maximum_degree as u128 + 1;
strides.push(stride);
radices.push(radix);
stride = stride.checked_mul(radix)?;
}
let encode = |exponents: &[E]| {
let mut key = 0u128;
for ((exponent, &stride), &radix) in exponents.iter().zip(&strides).zip(&radices) {
let exponent = exponent.to_u32() as u128;
if exponent >= radix {
return None;
}
key = key.checked_add(exponent.checked_mul(stride)?)?;
}
Some(key)
};
let left_keys = left
.exponents_iter()
.map(encode)
.collect::<Option<Vec<_>>>()?;
let right_keys = right
.exponents_iter()
.map(encode)
.collect::<Option<Vec<_>>>()?;
let mut differences: HashMap<u128, F::Element> = HashMap::default();
differences.reserve(self.nterms());
for term in self {
let key = encode(term.exponents)?;
if differences
.insert(key, self.ring().neg(term.coefficient))
.is_some()
{
return None;
}
}
for (left_coefficient, &left_key) in left.coefficients.iter().zip(&left_keys) {
for (right_coefficient, &right_key) in right.coefficients.iter().zip(&right_keys) {
let key = left_key.checked_add(right_key)?;
let difference = differences.entry(key).or_insert_with(|| self.ring().zero());
self.ring()
.add_mul_assign(difference, left_coefficient, right_coefficient);
}
}
Some(
differences
.values()
.all(|difference| self.ring().is_zero(difference)),
)
}
/// Return whether exact product verification should accumulate sparse coefficient
/// products instead of constructing the product through the dense multiplier.
fn packed_product_accumulation_is_preferred(left: &Self, right: &Self) -> bool {
if left.nvars() != right.nvars()
|| left.ring() != right.ring()
|| left.variables() != right.variables()
|| !left.is_polynomial()
|| !right.is_polynomial()
|| left.nterms() <= 1
|| right.nterms() <= 1
|| left.total_degree_dense_mul_is_bounded(right)
{
return false;
}
let mut output_len = Some(1usize);
let mut active_variables = 0;
for variable in 0..left.nvars() {
let Some(maximum_degree) = left
.degree(variable)
.to_u32()
.checked_add(right.degree(variable).to_u32())
else {
return false;
};
let radix = maximum_degree as usize + 1;
active_variables += usize::from(radix > 1);
output_len = output_len.and_then(|length| length.checked_mul(radix));
}
active_variables > 1
&& output_len.is_none_or(|length| {
!super::polynomial::mixed_radix_dense_mul_is_bounded(
length,
left.nterms(),
right.nterms(),
)
})
}
/// Check a sampled two-factor correction identity using dense coefficient
/// convolution in `x`.
#[cfg(debug_assertions)]
fn dense_two_factor_correction_matches(
&self,
error: &[F::Element],
left: &DenseBivariateImage<F::Element>,
right: &DenseBivariateImage<F::Element>,
corrections: &[Vec<F::Element>; 2],
) -> bool {
let Some(first_len) = corrections[0]
.len()
.checked_add(right.x_len)
.and_then(|length| length.checked_sub(1))
else {
return false;
};
let Some(second_len) = corrections[1]
.len()
.checked_add(left.x_len)
.and_then(|length| length.checked_sub(1))
else {
return false;
};
let result_len = error.len().max(first_len).max(second_len);
let mut reconstructed = vec![self.ring().zero(); result_len];
for (correction, factor) in [(&corrections[0], right), (&corrections[1], left)] {
for (correction_degree, correction_coefficient) in correction.iter().enumerate() {
if self.ring().is_zero(correction_coefficient) {
continue;
}
for factor_degree in 0..factor.x_len {
let factor_coefficient = factor.coefficient(factor_degree, 0);
if !self.ring().is_zero(factor_coefficient) {
self.ring().add_mul_assign(
&mut reconstructed[correction_degree + factor_degree],
correction_coefficient,
factor_coefficient,
);
}
}
}
}
reconstructed.iter().enumerate().all(|(degree, actual)| {
let difference = if let Some(expected) = error.get(degree) {
self.ring().sub(actual, expected)
} else {
actual.clone()
};
self.ring().is_zero(&difference)
})
}
fn sparse_multivariate_diophantine_two_factor_by_sampling(
factors: &[Self],
prods: &[Self],
error: &Self,
skeletons: &[Self],
order: &[usize],
context: &mut SparseDiophantineContext<Self, F::Element>,
) -> Option<Vec<Self>> {
if factors.len() != 2 || prods.len() != 2 || skeletons.len() != 2 || order.len() < 2 {
return None;
}
let characteristic = error.ring().characteristic();
if !characteristic.is_zero() && !characteristic.is_prime(0) {
return None;
}
let sparse_factor = skeletons
.iter()
.enumerate()
.filter(|(_, skeleton)| !skeleton.is_zero())
.min_by_key(|(_, skeleton)| skeleton.nterms())
.map(|(factor_index, _)| factor_index)?;
let dense_factor = 1 - sparse_factor;
let skeleton = &skeletons[sparse_factor];
let main_var = order[0];
let sample_vars = &order[1..];
let mut groups: Vec<(E, Vec<Vec<E>>, Vec<F::Element>)> = Vec::new();
for exponent in skeleton.exponents.chunks(skeleton.nvars()) {
let degree = exponent[main_var];
if let Some((_, exponents, _)) = groups.iter_mut().find(|(d, _, _)| *d == degree) {
exponents.push(exponent.to_vec());
} else {
groups.push((degree, vec![exponent.to_vec()], Vec::new()));
}
}
let samples_needed = groups
.iter()
.map(|(_, exponents, _)| exponents.len())
.max()
.unwrap_or(0);
if samples_needed == 0 || skeleton.nterms() > 512 {
return None;
}
let cached_base_points = context.two_factor_base_points.take().filter(|points| {
points.len() == sample_vars.len()
&& points
.iter()
.zip(sample_vars)
.all(|((point_var, _), sample_var)| point_var == sample_var)
});
if cached_base_points.is_none() {
context.two_factor_bezout.clear();
}
let mut rng = rng();
let sample_base_attempts =
SPARSE_MDP_SAMPLE_BASE_ATTEMPTS + usize::from(cached_base_points.is_some());
let mut cached_base_points = cached_base_points;
'sample_base: for _ in 0..sample_base_attempts {
let base_points = cached_base_points.take().unwrap_or_else(|| {
Self::sparse_interpolation_base_points(error, sample_vars, &mut rng)
});
for (_, exponents, sample_generators) in &mut groups {
sample_generators.clear();
for exponent in exponents {
let generator =
Self::evaluate_monomial_exponent(&error.ring(), exponent, &base_points);
if !Self::sparse_interpolation_generator_is_usable(
error,
&generator,
sample_generators,
) {
context.clear_two_factor_images();
continue 'sample_base;
}
sample_generators.push(generator);
}
}
context.two_factor_base_points = Some(base_points.clone());
let mut rhs: Vec<Vec<F::Element>> = groups
.iter()
.map(|_| Vec::with_capacity(samples_needed))
.collect();
let mut cache = Self::sample_cache(error, factors, prods);
let error_base = error.evaluate_exponents(&base_points, &mut cache);
let factors_base = factors
.iter()
.map(|f| f.evaluate_exponents(&base_points, &mut cache))
.collect::<Vec<_>>();
let mut error_current = Cow::Borrowed(&error_base);
let mut factors_current = factors_base.iter().map(Cow::Borrowed).collect::<Vec<_>>();
let mut error_image =
error.zero_with_capacity(error.degree(main_var).to_u32() as usize + 1);
let mut factor_images = factors
.iter()
.map(|f| f.zero_with_capacity(f.degree(main_var).to_u32() as usize + 1))
.collect::<Vec<_>>();
for sample_index in 0..samples_needed {
if sample_index > 0 {
for (current, base) in error_current.to_mut().iter_mut().zip(&error_base) {
error.ring().mul_assign(current, base);
}
for (current, base) in factors_current.iter_mut().zip(&factors_base) {
for (c, b) in current.to_mut().iter_mut().zip(base) {
error.ring().mul_assign(c, b);
}
}
}
error.evaluate_using_exponents_univariate_grouped(
&error_current,
main_var,
&mut error_image,
);
for ((factor, current), image) in
factors.iter().zip(&factors_current).zip(&mut factor_images)
{
factor.evaluate_using_exponents_univariate_grouped(current, main_var, image);
}
let Some(delta_image) = Self::try_two_factor_univariate_correction(
&mut factor_images,
&error_image,
sparse_factor,
context,
) else {
context.clear_two_factor_images();
continue 'sample_base;
};
for (group_index, (degree, _, _)) in groups.iter().enumerate() {
rhs[group_index].push(Self::univariate_coefficient(
&delta_image,
main_var,
*degree,
));
}
}
let mut sparse_delta = skeleton.zero();
for ((_, exponents, sample_generators), rhs) in groups.iter().zip(&rhs) {
let coefficients = error.solve_shifted_transposed_vandermonde(
sample_generators,
&rhs[..exponents.len()],
);
for (coefficient, exponent) in coefficients.into_iter().zip(exponents) {
if !error.ring().is_zero(&coefficient) {
sparse_delta.append_monomial(coefficient, exponent);
}
}
}
let residual = error - &(&sparse_delta * &prods[sparse_factor]);
// Exact division reconstructs the other delta and verifies that the
// interpolated sparse correction satisfies the Diophantine identity.
let Some(dense_delta) = residual.try_div_owned(&prods[dense_factor]) else {
context.clear_two_factor_images();
continue;
};
let mut deltas = vec![error.zero(), error.zero()];
deltas[sparse_factor] = sparse_delta;
deltas[dense_factor] = dense_delta;
return Some(deltas);
}
None
}
fn sparse_multivariate_diophantine_by_sampling(
factors: &[Self],
prods: &[Self],
error: &Self,
skeletons: &[Self],
order: &[usize],
) -> Option<Vec<Self>> {
if factors.len() != skeletons.len() || factors.len() != prods.len() || order.len() < 2 {
return None;
}
let main_var = order[0];
let sample_vars = &order[1..];
let mut groups = Vec::with_capacity(skeletons.len());
let mut samples_needed = 0usize;
let mut total_terms = 0usize;
for skeleton in skeletons {
let mut factor_groups: Vec<(E, Vec<Vec<E>>, Vec<F::Element>)> = Vec::new();
for exponent in skeleton.exponents.chunks(skeleton.nvars()) {
total_terms += 1;
let degree = exponent[main_var];
if let Some((_, exponents, _)) =
factor_groups.iter_mut().find(|(d, _, _)| *d == degree)
{
exponents.push(exponent.to_vec());
} else {
factor_groups.push((degree, vec![exponent.to_vec()], Vec::new()));
}
}
for (_, exponents, _) in &factor_groups {
samples_needed = samples_needed.max(exponents.len());
}
groups.push(factor_groups);
}
if samples_needed == 0 || total_terms > 512 {
return None;
}
let mut rng = rng();
'sample_base: for _ in 0..SPARSE_MDP_SAMPLE_BASE_ATTEMPTS {
let base_points = Self::sparse_interpolation_base_points(error, sample_vars, &mut rng);
for factor_groups in &mut groups {
for (_, exponents, sample_generators) in factor_groups {
sample_generators.clear();
for exponent in exponents {
let generator =
Self::evaluate_monomial_exponent(&error.ring(), exponent, &base_points);
if !Self::sparse_interpolation_generator_is_usable(
error,
&generator,
sample_generators,
) {
continue 'sample_base;
}
sample_generators.push(generator);
}
}
}
let mut rhs: Vec<Vec<Vec<F::Element>>> = groups
.iter()
.map(|factor_groups| {
factor_groups
.iter()
.map(|_| Vec::with_capacity(samples_needed))
.collect()
})
.collect();
let mut cache = Self::sample_cache(error, factors, prods);
let error_base = error.evaluate_exponents(&base_points, &mut cache);
let factors_base = factors
.iter()
.map(|f| f.evaluate_exponents(&base_points, &mut cache))
.collect::<Vec<_>>();
let mut error_current = Cow::Borrowed(&error_base);
let mut factors_current = factors_base.iter().map(Cow::Borrowed).collect::<Vec<_>>();
let mut error_image =
error.zero_with_capacity(error.degree(main_var).to_u32() as usize + 1);
let mut factor_images = factors
.iter()
.map(|f| f.zero_with_capacity(f.degree(main_var).to_u32() as usize + 1))
.collect::<Vec<_>>();
for sample_index in 0..samples_needed {
if sample_index > 0 {
for (current, base) in error_current.to_mut().iter_mut().zip(&error_base) {
error.ring().mul_assign(current, base);
}
for (current, base) in factors_current.iter_mut().zip(&factors_base) {
for (c, b) in current.to_mut().iter_mut().zip(base) {
error.ring().mul_assign(c, b);
}
}
}
error.evaluate_using_exponents_univariate_grouped(
&error_current,
main_var,
&mut error_image,
);
for ((factor, current), image) in
factors.iter().zip(&factors_current).zip(&mut factor_images)
{
factor.evaluate_using_exponents_univariate_grouped(current, main_var, image);
}
let Some(deltas_image) =
Self::try_univariate_diophantine(&mut factor_images, &error_image)
else {
continue 'sample_base;
};
for (factor_index, factor_groups) in groups.iter().enumerate() {
for (group_index, (degree, _, _)) in factor_groups.iter().enumerate() {
rhs[factor_index][group_index].push(Self::univariate_coefficient(
&deltas_image[factor_index],
main_var,
*degree,
));
}
}
}
let mut deltas: Vec<_> = skeletons.iter().map(|s| s.zero()).collect();
for (factor_index, factor_groups) in groups.iter().enumerate() {
for (group_index, (_, exponents, sample_generators)) in
factor_groups.iter().enumerate()
{
let coefficients = error.solve_shifted_transposed_vandermonde(
sample_generators,
&rhs[factor_index][group_index][..exponents.len()],
);
for (coefficient, exponent) in coefficients.into_iter().zip(exponents) {
if !error.ring().is_zero(&coefficient) {
deltas[factor_index].append_monomial(coefficient, exponent);
}
}
}
}
let mut check = error.zero();
for (delta, prod) in deltas.iter().zip(prods) {
check = check + delta * prod;
}
if &check == error {
return Some(deltas);
}
}
None
}
fn sparse_interpolation_base_points(
poly: &Self,
sample_vars: &[usize],
rng: &mut impl rand::RngCore,
) -> Vec<(usize, F::Element)> {
let upper = match poly.ring().characteristic().to_i64() {
Some(characteristic) if characteristic > 0 => characteristic - 1,
_ => MAX_RNG_PREFACTOR as i64 - 1,
};
let policy = 0..=upper;
sample_vars
.iter()
.map(|v| {
let mut value = poly.ring().sample(rng, &policy);
let mut attempts = 0;
while poly.ring().is_zero(&value) && attempts < 8 {
value = poly.ring().sample(rng, &policy);
attempts += 1;
}
(*v, value)
})
.collect()
}
fn sparse_interpolation_generator_is_usable(
poly: &Self,
generator: &F::Element,
previous_generators: &[F::Element],
) -> bool {
if poly.ring().is_zero(generator) {
return false;
}
for prev in previous_generators {
let diff = poly.ring().sub(generator, prev);
if poly.ring().is_zero(&diff) {
return false;
}
}
true
}
fn evaluate_monomial_exponent(
ring: &F,
exponent: &[E],
sample_points: &[(usize, F::Element)],
) -> F::Element {
let mut value = ring.one();
for (var, sample) in sample_points {
let e = exponent[*var].to_u32();
if e > 0 {
ring.mul_assign(&mut value, &ring.pow(sample, e as u64));
}
}
value
}
fn sample_cache(f: &Self, factors: &[Self], prods: &[Self]) -> Vec<Vec<F::Element>> {
let mut degrees = (0..f.nvars())
.map(|var| f.degree(var).to_u32() as usize)
.collect::<Vec<_>>();
for p in factors.iter().chain(prods) {
for (var, degree) in degrees.iter_mut().enumerate() {
*degree = (*degree).max(p.degree(var).to_u32() as usize);
}
}
degrees
.into_iter()
.map(|degree| vec![f.ring().zero(); (degree + 1).min(POW_CACHE_SIZE)])
.collect()
}
fn univariate_coefficient(poly: &Self, var: usize, degree: E) -> F::Element {
let mut exponent = vec![E::zero(); poly.nvars()];
exponent[var] = degree;
poly.coefficient(&exponent)
.unwrap_or_else(|| poly.ring().zero())
}
/// Solve the two-factor univariate Diophantine equation for one requested
/// correction. For `f0 * s + f1 * t = 1`, correction zero is
/// `t * rhs mod f0` and correction one is `s * rhs mod f1`.
fn try_two_factor_univariate_correction(
factors: &mut [Self],
rhs: &Self,
requested_factor: usize,
context: &mut SparseDiophantineContext<Self, F::Element>,
) -> Option<Self> {
if factors.len() != 2
|| requested_factor >= 2
|| factors
.iter()
.any(|factor| factor.ring().is_zero(&factor.lcoeff()))
{
return None;
}
let key = (factors[0].clone(), factors[1].clone());
let bezout = context
.two_factor_bezout
.entry(key)
.or_insert_with(|| {
let (g, s, t) = factors[0].eea_univariate(&factors[1]);
g.is_one().then_some(TwoFactorImageBezout { s, t })
})
.as_ref()?;
if requested_factor == 0 {
Some((&bezout.t * rhs).quot_rem_univariate(&mut factors[0]).1)
} else {
Some((&bezout.s * rhs).quot_rem_univariate(&mut factors[1]).1)
}
}
fn try_univariate_diophantine(factors: &mut [Self], rhs: &Self) -> Option<Vec<Self>> {
if factors
.iter()
.any(|factor| factor.ring().is_zero(&factor.lcoeff()))
{
return None;
}
let mut cur = factors.last()?.clone();
let mut products = vec![cur.clone()];
for factor in factors[1..].iter().rev().skip(1) {
cur = cur * factor;
products.push(cur.clone());
}
products.reverse();
let mut deltas = Vec::with_capacity(factors.len());
let mut cur_s = rhs.clone();
for (factor, product) in factors.iter_mut().zip(&mut products) {
let (g, s, t) = factor.eea_univariate(product);
if !g.is_one() {
return None;
}
let new_s = (t * &cur_s).quot_rem_univariate(factor).1;
deltas.push(new_s);
cur_s = (s * &cur_s).quot_rem_univariate(product).1;
}
deltas.push(cur_s);
Some(deltas)
}
fn impose_true_lcoeffs_on_factors(
&self,
factors: &[Self],
true_lcoeffs: &[Self],
sample_points: &[(usize, F::Element)],
order: &[usize],
current_index: usize,
) -> Vec<Self> {
let mut factors_with_true_lcoeff = Vec::with_capacity(factors.len());
for (factor, true_lcoeff) in factors.iter().zip(true_lcoeffs) {
let mut lcoeff = true_lcoeff.clone();
for &var in &order[current_index + 1..] {
if let Some((_, sample)) = sample_points.iter().find(|(v, _)| *v == var) {
lcoeff = lcoeff.replace(var, sample);
}
}
let mut coefficients = factor.to_univariate_polynomial_list(order[0]);
coefficients.last_mut().unwrap().0 = lcoeff;
let mut fixed_factor = self.zero();
let mut exp = vec![E::zero(); self.nvars()];
for (coefficient, degree) in coefficients {
exp[order[0]] = degree;
fixed_factor = fixed_factor + coefficient.mul_exp(&exp);
}
factors_with_true_lcoeff.push(fixed_factor);
}
factors_with_true_lcoeff
}
/// Lift two factors through the final variable using dense bivariate
/// geometric images. Sparse interpolation reconstructs both corrections at
/// each lifted coefficient. When `verify_product` is true, the completed
/// factors are also certified against the target before they are returned.
fn try_multivariate_hensel_step_two_factor_evaluated(
&self,
factors: &[Self],
order: &[usize],
last_degree: usize,
verify_product: bool,
) -> Option<Vec<Self>> {
if factors.len() != 2 || order.len() <= 2 {
return None;
}
let characteristic = self.ring().characteristic();
if !characteristic.is_zero() && !characteristic.is_prime(0) {
return None;
}
let x = order[0];
let y = *order.last()?;
if x == y {
return None;
}
let y_rows = last_degree.checked_add(1)?;
if y_rows > MAX_EVALUATED_HENSEL_Y_ROWS {
return None;
}
let sample_vars = &order[1..order.len() - 1];
let mut u = Vec::with_capacity(2);
for factor in factors {
let mut coefficients = vec![self.zero(); y_rows];
for (coefficient, degree) in factor.to_univariate_polynomial_list(y) {
let degree = degree.to_u32() as usize;
if degree > last_degree {
return None;
}
coefficients[degree] = coefficient;
}
u.push(coefficients);
}
let expected_factor_x_degrees = [
u[0][0].degree(x).to_u32() as usize,
u[1][0].degree(x).to_u32() as usize,
];
if u.iter().any(|factor| factor[0].is_zero()) {
return None;
}
let mut rng = rng();
let base_points = Self::sparse_interpolation_base_points(self, sample_vars, &mut rng);
if base_points
.iter()
.any(|(_, point)| self.ring().is_zero(point))
{
return None;
}
let mut image_cache = Self::sample_cache(self, factors, &[]);
let mut target_images = Vec::new();
let mut factor_images = vec![Vec::new(), Vec::new()];
let mut factor_correction_contexts = Vec::new();
let mut sample_count = 0usize;
for k in 1..=last_degree {
let mut sampled_errors = Vec::with_capacity(sample_count);
if sample_count > 0 {
let mut sampled_error_is_zero = true;
for sample_index in 0..sample_count {
let error = self.dense_bivariate_product_error_row(
&target_images[sample_index],
&factor_images[0][sample_index],
&factor_images[1][sample_index],
k,
);
let error_is_zero = error
.iter()
.all(|coefficient| self.ring().is_zero(coefficient));
sampled_errors.push(error);
if !error_is_zero {
sampled_error_is_zero = false;
break;
}
}
if sampled_error_is_zero {
continue;
}
}
let skeletons = [&u[0][k - 1], &u[1][k - 1]];
if skeletons.iter().any(|skeleton| skeleton.nterms() > 512) {
return None;
}
let mut groups: Vec<Vec<(E, Vec<Vec<E>>, Vec<F::Element>)>> = Vec::with_capacity(2);
let mut samples_needed = 0usize;
for skeleton in skeletons {
let mut factor_groups: Vec<(E, Vec<Vec<E>>, Vec<F::Element>)> = Vec::new();
for exponent in skeleton.exponents.chunks(skeleton.nvars()) {
if exponent[y] != E::zero() {
return None;
}
let x_degree = exponent[x];
if let Some((_, exponents, _)) = factor_groups
.iter_mut()
.find(|(degree, _, _)| *degree == x_degree)
{
exponents.push(exponent.to_vec());
} else {
factor_groups.push((x_degree, vec![exponent.to_vec()], Vec::new()));
}
}
for (_, exponents, generators) in &mut factor_groups {
samples_needed = samples_needed.max(exponents.len());
for exponent in exponents {
let generator =
Self::evaluate_monomial_exponent(&self.ring(), exponent, &base_points);
if !Self::sparse_interpolation_generator_is_usable(
self, &generator, generators,
) {
return None;
}
generators.push(generator);
}
}
groups.push(factor_groups);
}
if samples_needed == 0 {
return None;
}
if samples_needed > sample_count {
let current_factors = u
.iter()
.map(|coefficients| self.from_dense_y_coefficients(coefficients, y))
.collect::<Vec<_>>();
let mut retained_image_cells = 0usize;
let mut term_steps = 0usize;
for polynomial in std::iter::once(self).chain(¤t_factors) {
if polynomial.nterms() > MAX_EVALUATED_HENSEL_GROUPED_TERMS {
return None;
}
let x_len = (polynomial.degree(x).to_u32() as usize).checked_add(1)?;
let image_cells = x_len.checked_mul(y_rows)?;
retained_image_cells = retained_image_cells
.checked_add(image_cells.checked_mul(samples_needed)?)?;
term_steps =
term_steps.checked_add(polynomial.nterms().checked_mul(samples_needed)?)?;
if retained_image_cells > MAX_EVALUATED_HENSEL_IMAGE_CELLS
|| term_steps > MAX_EVALUATED_HENSEL_TERM_STEPS
{
return None;
}
}
target_images = self.evaluate_geometric_bivariate_images(
x,
y,
&base_points,
samples_needed,
&mut image_cache,
);
factor_images = current_factors
.iter()
.map(|factor| {
factor.evaluate_geometric_bivariate_images(
x,
y,
&base_points,
samples_needed,
&mut image_cache,
)
})
.collect();
for image in target_images
.iter_mut()
.chain(factor_images.iter_mut().flatten())
{
if image.y_len > y_rows {
return None;
}
let image_len = image.x_len.checked_mul(y_rows)?;
image.coefficients.resize(image_len, self.ring().zero());
image.y_len = y_rows;
}
for sample_index in factor_correction_contexts.len()..samples_needed {
let images = [
self.dense_bivariate_x_row_to_univariate(
&factor_images[0][sample_index],
x,
0,
),
self.dense_bivariate_x_row_to_univariate(
&factor_images[1][sample_index],
x,
0,
),
];
for (factor_index, image) in images.iter().enumerate() {
if image.is_zero()
|| image.degree(x).to_u32() as usize
!= expected_factor_x_degrees[factor_index]
{
return None;
}
}
let (gcd, s, t) = images[0].eea_univariate(&images[1]);
if !gcd.is_one() {
return None;
}
factor_correction_contexts.push(DenseTwoFactorCorrectionContext {
multipliers: [
self.dense_indexed_univariate(&t, x)?,
self.dense_indexed_univariate(&s, x)?,
],
moduli: [
self.dense_monic_modulus(&factor_images[0][sample_index])?,
self.dense_monic_modulus(&factor_images[1][sample_index])?,
],
});
}
sample_count = samples_needed;
}
let supported_x_degrees = groups
.iter()
.enumerate()
.map(|(factor_index, factor_groups)| {
let x_len = factor_images[factor_index][0].x_len;
let mut supported = vec![false; x_len];
for (degree, _, _) in factor_groups {
let degree = degree.to_u32() as usize;
if degree >= x_len {
return None;
}
supported[degree] = true;
}
Some(supported)
})
.collect::<Option<Vec<_>>>()?;
let mut rhs: Vec<Vec<Vec<F::Element>>> = groups
.iter()
.map(|factor_groups| {
factor_groups
.iter()
.map(|_| Vec::with_capacity(samples_needed))
.collect()
})
.collect();
sampled_errors.extend((sampled_errors.len()..samples_needed).map(|sample_index| {
self.dense_bivariate_product_error_row(
&target_images[sample_index],
&factor_images[0][sample_index],
&factor_images[1][sample_index],
k,
)
}));
for (sample_index, error_coefficients) in
sampled_errors.iter().take(samples_needed).enumerate()
{
let correction_coefficients = self.dense_two_factor_corrections(
error_coefficients,
&factor_correction_contexts[sample_index],
[
factor_images[0][sample_index].x_len,
factor_images[1][sample_index].x_len,
],
);
let correction_coefficients = correction_coefficients?;
for factor_index in 0..2 {
if correction_coefficients[factor_index]
.iter()
.enumerate()
.any(|(degree, coefficient)| {
!self.ring().is_zero(coefficient)
&& !supported_x_degrees[factor_index]
.get(degree)
.copied()
.unwrap_or(false)
})
{
return None;
}
}
#[cfg(debug_assertions)]
{
if !self.dense_two_factor_correction_matches(
error_coefficients,
&factor_images[0][sample_index],
&factor_images[1][sample_index],
&correction_coefficients,
) {
return None;
}
}
for (factor_index, correction_coefficients) in
correction_coefficients.into_iter().enumerate()
{
for (group_index, (degree, _, _)) in groups[factor_index].iter().enumerate() {
rhs[factor_index][group_index]
.push(correction_coefficients[degree.to_u32() as usize].clone());
}
}
}
let mut deltas = [self.zero(), self.zero()];
let geometric_sequence_kernels = self.ring().kernels().geometric_sequences();
for factor_index in 0..2 {
for (group_index, (degree, exponents, generators)) in
groups[factor_index].iter().enumerate()
{
let coefficients = self.solve_shifted_transposed_vandermonde(
generators,
&rhs[factor_index][group_index][..exponents.len()],
);
let mut current = coefficients
.iter()
.zip(generators)
.map(|(coefficient, generator)| self.ring().mul(coefficient, generator))
.collect::<Vec<_>>();
let x_degree = degree.to_u32() as usize;
for sample_index in 0..sample_count {
let reconstructed = (sample_index + 1 < sample_count)
.then(|| geometric_sequence_kernels)
.flatten()
.and_then(|kernels| {
kernels.try_sum_and_advance_geometric_sequences(
GeometricSequenceStepRequest {
current: &mut current,
ratios: generators,
},
)
})
.unwrap_or_else(|| {
let mut reconstructed = self.ring().zero();
for (current, generator) in current.iter_mut().zip(generators) {
self.ring().add_assign(&mut reconstructed, &*current);
if sample_index + 1 < sample_count {
self.ring().mul_assign(current, generator);
}
}
reconstructed
});
if sample_index < samples_needed {
let expected = &rhs[factor_index][group_index][sample_index];
let difference = self.ring().sub(&reconstructed, expected);
if !self.ring().is_zero(&difference) {
return None;
}
}
if !self.ring().is_zero(&reconstructed) {
let image = &mut factor_images[factor_index][sample_index];
let index = image.index(x_degree, k);
self.ring()
.add_assign(&mut image.coefficients[index], reconstructed);
}
}
for (coefficient, exponent) in coefficients.into_iter().zip(exponents) {
if !self.ring().is_zero(&coefficient) {
deltas[factor_index].append_monomial(coefficient, exponent);
}
}
}
}
for factor_index in 0..2 {
u[factor_index][k] = &u[factor_index][k] + &deltas[factor_index];
}
}
let lifted = u
.iter()
.map(|coefficients| self.from_dense_y_coefficients(coefficients, y))
.collect::<Vec<_>>();
if verify_product {
let product_matches =
Self::packed_product_accumulation_is_preferred(&lifted[0], &lifted[1])
.then(|| self.product_matches_by_packed_accumulation(&lifted[0], &lifted[1]))
.flatten()
.unwrap_or_else(|| {
let product = &lifted[0] * &lifted[1];
&product == self
});
if !product_matches {
return None;
}
}
Some(lifted)
}
fn multivariate_hensel_lifting(
&self,
factors: &[Self],
univariate_factors: &mut [Self],
univariate_deltas: &[Self],
sample_points: &[(usize, F::Element)],
true_lcoeffs: Option<&[Self]>,
order: &[usize],
context: MultivariateHenselContext,
) -> Result<Vec<Self>, MultivariateHenselError> {
debug!("Hensel lift {} with order {:?}", self, order);
let mut degrees: Vec<_> = order
.iter()
.map(|v| self.degree(*v).to_u32() as usize)
.collect();
// Build each nested specialization once. Stage v keeps order[..=v]
// symbolic and evaluates every later variable at its sample point.
let mut specialized_targets: Vec<Option<Self>> =
std::iter::repeat_with(|| None).take(order.len()).collect();
let mut specialized_target = self.clone();
for v in (context.start_index..order.len()).rev() {
specialized_targets[v] = Some(specialized_target.clone());
if v > context.start_index {
let variable = order[v];
let sample = &sample_points
.iter()
.find(|(sample_var, _)| *sample_var == variable)
.unwrap()
.1;
specialized_target = specialized_target.replace(variable, sample);
}
}
let mut reconstructed_factors = factors.to_vec();
let mut used_evaluated_lift = false;
for v in context.start_index..order.len() {
// Replace the leading coefficient in x0 before this lift step.
let mut factors_with_true_lcoeff = if let Some(true_lcoeffs) = true_lcoeffs {
self.impose_true_lcoeffs_on_factors(
&reconstructed_factors,
true_lcoeffs,
sample_points,
order,
v,
)
} else {
reconstructed_factors
};
let mut f = specialized_targets[v]
.take()
.expect("every Hensel stage must have a specialized target");
// shift the polynomial such that the evaluation point is now at 0
// so that we can use a convolution for fast error computation
let shift = &sample_points.iter().find(|s| s.0 == order[v]).unwrap().1;
if !self.ring().is_zero(shift) {
f = f.shift_var_cached(order[v], shift);
for f in &mut factors_with_true_lcoeff {
*f = f.shift_var_cached(order[v], shift);
}
}
reconstructed_factors = f.multivariate_hensel_step(
univariate_deltas,
univariate_factors,
sample_points,
&mut factors_with_true_lcoeff,
&order[..=v],
&mut degrees[..=v],
context,
&mut used_evaluated_lift,
)?;
if !self.ring().is_zero(shift) {
for f in &mut reconstructed_factors {
*f = f.shift_var_cached(order[v], &self.ring().neg(shift));
}
}
for f in &reconstructed_factors {
debug!("Reconstructed factor {}", f);
}
}
// Evaluated stages reconstruct from sampled sparse supports. Certify the
// completed, unshifted factors once after all nested stages have finished.
if used_evaluated_lift {
let product_matches = (reconstructed_factors.len() == 2
&& Self::packed_product_accumulation_is_preferred(
&reconstructed_factors[0],
&reconstructed_factors[1],
))
.then(|| {
self.product_matches_by_packed_accumulation(
&reconstructed_factors[0],
&reconstructed_factors[1],
)
})
.flatten()
.unwrap_or_else(|| {
let product = reconstructed_factors
.iter()
.fold(self.one(), |product, factor| &product * factor);
&product == self
});
if !product_matches {
return Err(MultivariateHenselError::SparseDiophantineFailed);
}
}
Ok(reconstructed_factors)
}
fn multivariate_hensel_step(
&self,
univariate_deltas: &[Self],
univariate_factors: &mut [Self],
sample_points: &[(usize, F::Element)],
factors: &mut [Self],
order: &[usize],
degrees: &mut [usize],
context: MultivariateHenselContext,
used_evaluated_lift: &mut bool,
) -> Result<Vec<Self>, MultivariateHenselError> {
let last_var = *order.last().unwrap();
let last_degree = *degrees.last().unwrap();
let defer_product_verification =
context.sparse_diophantine_fallback == SparseDiophantineFallback::RetrySample;
if let Some(lifted) = self.try_multivariate_hensel_step_two_factor_evaluated(
factors,
order,
last_degree,
!defer_product_verification,
) {
*used_evaluated_lift |= defer_product_verification;
return Ok(lifted);
}
// Before a generic stage consumes speculative factors, verify their
// constant row in the new lifting variable against the target row.
if *used_evaluated_lift {
let zero = self.ring().zero();
let target_at_zero = self.replace(last_var, &zero);
let product_at_zero = factors.iter().fold(self.one(), |product, factor| {
&product * &factor.replace(last_var, &zero)
});
if product_at_zero != target_at_zero {
return Err(MultivariateHenselError::SparseDiophantineFailed);
}
}
let y_poly = self.to_univariate_polynomial_list(last_var);
// extract coefficients in last_var
let mut u: Vec<_> = factors
.iter()
.map(|f| {
let mut dense = vec![self.zero(); last_degree + 1];
for (p, e) in f.to_univariate_polynomial_list(last_var) {
dense[e.to_u32() as usize] = p;
}
dense
})
.collect();
// TODO: do entire initialization here?
// the loop below cannot be cut short anyway, so it's not extra work to do it all here
let mut p = u.clone();
let mut cur_p = p[0][0].clone();
for x in &mut p.iter_mut().skip(1) {
for j in &mut *x {
*j = &*j * &cur_p;
}
cur_p = x[0].clone();
}
let factors_mod = u
.iter()
.map(|factor_coefficients| factor_coefficients[0].clone())
.collect::<Vec<_>>();
let prod_mod = if factors_mod.len() == 2 {
vec![factors_mod[1].clone(), factors_mod[0].clone()]
} else {
(0..factors_mod.len())
.map(|excluded| {
factors_mod
.iter()
.enumerate()
.filter(|(index, _)| *index != excluded)
.fold(self.one(), |product, (_, factor)| product * factor)
})
.collect()
};
debug!("in shift {}", self);
debug!("deg {:?}", degrees);
let mut sparse_diophantine_context = SparseDiophantineContext::new();
// create the polynomials (x_i-shift_i)^deg used for modding during Hensel lifting
let mut mod_vars = Vec::with_capacity(order.len() - 2);
let mut exp = vec![E::zero(); self.nvars()];
for r in order[1..order.len() - 1]
.iter()
.zip(°rees[1..order.len() - 1])
{
let shift = &sample_points.iter().find(|s| s.0 == *r.0).unwrap().1;
exp[*r.0] = E::one();
let var_pow = self
.monomial(self.ring().one(), exp.clone())
.shift_var(*r.0, &self.ring().neg(shift))
.pow(r.1 + 1);
exp[*r.0] = E::zero();
mod_vars.push(var_pow);
}
for k in 1..=last_degree {
// extract the coefficient required to compute the error in y^k
// computed using a convolution
for i in 1..factors.len() {
for j in 0..k {
if p[i - 1][k - j].is_zero() || u[i][j].is_zero() {
continue;
}
p[i][k] = &p[i][k] + &(&p[i - 1][k - j] * &u[i][j]);
}
}
// find the kth power of y in f
// since we compute the error per power of y, we cannot stop on a 0 error
let e = if let Some((v, _)) = y_poly.iter().find(|e| e.1.to_u32() as usize == k) {
v - &p.last().unwrap()[k]
} else {
-p.last().unwrap()[k].clone()
};
debug!("hensel e[x{}^{}] = {}", last_var, k, e);
if e.is_zero() {
continue;
}
let skeletons: Vec<_> = u.iter().map(|ui| ui[k - 1].clone()).collect();
let sparse_delta = Self::sparse_multivariate_diophantine_from_skeleton(
&factors_mod,
&prod_mod,
&e,
&skeletons,
&order[..order.len() - 1],
&mut sparse_diophantine_context,
);
let new_delta = match sparse_delta {
Some(delta) => delta,
None if order.len() > 2
&& context.sparse_diophantine_fallback
== SparseDiophantineFallback::RetrySample =>
{
debug!(
"Sparse Diophantine correction failed after {} lifted variables; retrying the evaluation sample",
order.len() - 1
);
return Err(MultivariateHenselError::SparseDiophantineFailed);
}
None => Self::multivariate_diophantine(
univariate_deltas,
univariate_factors,
&prod_mod,
&e,
&order[..order.len() - 1],
sample_points,
°rees[..order.len() - 1],
&mod_vars,
),
};
// update the coefficients with the new y^k contributions
let mut t = self.zero();
for (i, (du, d)) in u.iter_mut().zip(&new_delta).enumerate() {
debug!("hensel d[x{}^{}] = {}", last_var, k, d);
du[k] = &du[k] + d;
if i > 0 {
t = &du[0] * &t + d * &p[i - 1][0];
} else {
t = &t + d;
}
p[i][k] = &p[i][k] + &t;
}
}
// convert dense polynomials to multivariate polynomials
let lifted = u
.into_iter()
.map(|ts| {
let mut new_poly = self.zero_with_capacity(ts.len());
for (i, mut f) in ts.into_iter().enumerate() {
for x in f.exponents_iter_mut() {
debug_assert_eq!(x[last_var], E::zero());
x[last_var] = E::from_u32(i as u32);
}
new_poly = new_poly + f;
}
new_poly
})
.collect::<Vec<_>>();
Ok(lifted)
}
}
impl<E: PositiveExponent> MultivariatePolynomial<IntegerRing, E, LexOrder> {
fn integer_factor_start_auto_decision(&self, order: &[usize]) -> (bool, f64) {
let bivariate_box_density = integer_factor_bivariate_box_density(
self.nterms(),
self.degree(order[0]).to_u32() as usize,
self.degree(order[1]).to_u32() as usize,
);
let use_bivariate =
bivariate_box_density <= INTEGER_FACTOR_BIVARIATE_SPARSE_BOX_DENSITY_THRESHOLD;
(use_bivariate, bivariate_box_density)
}
/// Hensel lift a solution of `self = u * w mod p` to `self = u * w mod max_p`
/// where `max_p` is a power of `p`.
///
/// If the lifting is successful, i.e. the error is 0 at some stage,
/// it will return `Ok((u,w))` where `u` and `w` are the true factors over
/// the integers. If a true factorization is not possible, it returns
/// `Err((u,w))` where `u` and `w` are monic.
pub fn hensel_lift<UField: FiniteFieldWorkspace>(
&self,
u: MultivariatePolynomial<FiniteField<UField>, E, LexOrder>,
w: MultivariatePolynomial<FiniteField<UField>, E, LexOrder>,
gamma: Option<Integer>,
max_p: &Integer,
) -> Result<(Self, Self), (Self, Self)>
where
FiniteField<UField>: Field + PolynomialGCD<E> + FiniteFieldCore<UField>,
Integer: ToFiniteField<UField>,
{
self.hensel_lift_with_strategy(u, w, gamma, max_p, true)
}
fn hensel_lift_with_strategy<UField: FiniteFieldWorkspace>(
&self,
mut u: MultivariatePolynomial<FiniteField<UField>, E, LexOrder>,
mut w: MultivariatePolynomial<FiniteField<UField>, E, LexOrder>,
gamma: Option<Integer>,
max_p: &Integer,
quadratic_lift_allowed: bool,
) -> Result<(Self, Self), (Self, Self)>
where
FiniteField<UField>: Field + PolynomialGCD<E> + FiniteFieldCore<UField>,
Integer: ToFiniteField<UField>,
{
let lcoeff = self.lcoeff(); // lcoeff % p != 0
let mut gamma = gamma.unwrap_or(lcoeff.clone());
let lcoeff_p = lcoeff.to_finite_field(&u.ring());
let gamma_p = gamma.to_finite_field(&u.ring());
let field = u.ring().clone();
let p = field.get_prime().to_integer();
let a = self.clone().mul_coeff(gamma.clone());
u = u.make_monic().mul_coeff(gamma_p.clone());
w = w.make_monic().mul_coeff(lcoeff_p.clone());
let (_, s, t) = u.eea_univariate(&w);
debug_assert!((&s * &u + &t * &w).is_one());
let mut u_i = u.map_coeff(|c| field.to_symmetric_integer(c), Z);
let mut w_i = w.map_coeff(|c| field.to_symmetric_integer(c), Z);
// only replace the leading coefficient
*u_i.coefficients.last_mut().unwrap() = gamma.clone();
*w_i.coefficients.last_mut().unwrap() = lcoeff;
let mut m = p.clone();
let use_quadratic_lift = if !quadratic_lift_allowed || p == Integer::Single(2) {
false
} else {
let mut threshold_power = p.clone();
let mut reaches_threshold = true;
for _ in 1..MIN_QUADRATIC_HENSEL_DIGITS {
if &threshold_power >= max_p {
reaches_threshold = false;
break;
}
threshold_power *= &p;
}
reaches_threshold
};
let lift_is_exact = if use_quadratic_lift {
#[cfg(test)]
QUADRATIC_HENSEL_LIFT_CALLS.with(|calls| calls.set(calls.get() + 1));
let variable = self
.last_exponents()
.iter()
.position(|exponent| !exponent.is_zero())
.expect("a Hensel factor must be nonconstant");
let dense_length = self.degree(variable).to_u32() as usize + 1;
let intermediate_length = dense_length
.checked_mul(2)
.and_then(|length| length.checked_sub(1))
.expect("dense Hensel intermediate length overflow");
let dense_indices = (0..intermediate_length)
.map(|index| index as u32)
.collect::<Vec<_>>();
let conversion_context =
DenseIntegerModularUnivariateContext::new(&p, variable, self, &dense_indices);
let target = conversion_context.dense_coefficients(&a);
let mut u_dense = conversion_context.dense_coefficients(&u_i);
let mut w_dense = conversion_context.dense_coefficients(&w_i);
let mut s_dense = conversion_context.dense_coefficients(
&s.map_coeff(|coefficient| field.to_symmetric_integer(coefficient), Z),
);
let mut t_dense = conversion_context.dense_coefficients(
&t.map_coeff(|coefficient| field.to_symmetric_integer(coefficient), Z),
);
let mut residual =
conversion_context.exact_product_residual(&target, &u_dense, &w_dense);
while !residual.is_empty() && &m < max_p {
// A full round doubles the known p-adic precision. The last round can use the
// remaining divisor of m so that the resulting modulus is exactly max_p.
debug_assert!((max_p % &m).is_zero());
let squared_modulus = &m * &m;
let step_modulus = if &squared_modulus <= max_p {
m.clone()
} else {
max_p / &m
};
debug_assert!((&m % &step_modulus).is_zero());
let next_modulus = &m * &step_modulus;
let modular_context = DenseIntegerModularUnivariateContext::new(
&step_modulus,
variable,
self,
&dense_indices,
);
let error_mod =
modular_context.exact_scalar_quotient_mod(std::mem::take(&mut residual), &m);
let u_mod = modular_context.reduce(&u_dense);
let w_mod = modular_context.reduce(&w_dense);
let s_mod = modular_context.reduce(&s_dense);
let t_mod = modular_context.reduce(&t_dense);
let (u_divisor, w_divisor) =
modular_context.prepare_unit_leading_divisor_pair(u_mod, w_mod);
// Since s*u+t*w=1 modulo the correction modulus, reducing E*s modulo w and
// E*t modulo u gives degree-bounded corrections with E=tau*w+r*u, where
// E=(target-u*w)/m.
let mut r =
modular_context.multiply_remainder(error_mod.clone(), &s_mod, &w_divisor);
let mut tau = modular_context.multiply_remainder(error_mod, &t_mod, &u_divisor);
modular_context.symmetrize_in_place(&mut r);
modular_context.symmetrize_in_place(&mut tau);
u_dense = modular_context.add_scaled_exact(u_dense, &tau, &m);
w_dense = modular_context.add_scaled_exact(w_dense, &r, &m);
residual = modular_context.exact_product_residual(&target, &u_dense, &w_dense);
if residual.is_empty() || &next_modulus >= max_p {
m = next_modulus;
break;
}
// Lift the Bezout cofactors to the same doubled modulus. For
// b=(1-s*u-t*w)/m, solve ds*u+dt*w=b modulo m and set
// s'=s+m*ds, t'=t+m*dt.
let bezout_mod = modular_context
.exact_bezout_residual_quotient_mod(&s_dense, &u_dense, &t_dense, &w_dense, &m);
// The lifted factors differ from the prepared divisors by multiples of m, which
// vanish modulo the correction modulus because that modulus divides m.
let mut delta_s =
modular_context.multiply_remainder(bezout_mod.clone(), &s_mod, &w_divisor);
let mut delta_t =
modular_context.multiply_remainder(bezout_mod, &t_mod, &u_divisor);
modular_context.symmetrize_in_place(&mut delta_s);
modular_context.symmetrize_in_place(&mut delta_t);
s_dense = modular_context.add_scaled_exact(s_dense, &delta_s, &m);
t_dense = modular_context.add_scaled_exact(t_dense, &delta_t, &m);
m = next_modulus;
}
let exact = residual.is_empty();
u_i = conversion_context.from_dense_coefficients(u_dense);
w_i = conversion_context.from_dense_coefficients(w_dense);
exact
} else {
let e = &a - &(&u_i * &w_i);
// At precision m, error_quotient is exactly
// (a - u_i * w_i) / m. Updating it from the p-adic corrections
// avoids multiplying the two increasingly large lifted factors.
let divide_by_prime = |mut residual: Self| {
for coefficient in &mut residual.coefficients {
debug_assert!((&*coefficient % &p).is_zero());
*coefficient /= &p;
}
residual
};
let mut error_quotient = divide_by_prime(e);
while !error_quotient.is_zero() && &m < max_p {
let e_p = error_quotient.map_coeff(|c| c.to_finite_field(&field), field.clone());
let (q, r) = (&e_p * &s).quot_rem_univariate(&mut w);
let tau = &e_p * &t + q * &u;
let tau_i = tau.map_coeff(|c| field.to_symmetric_integer(c), Z);
let r_i = r.map_coeff(|c| field.to_symmetric_integer(c), Z);
// For u' = u + m*tau and w' = w + m*r,
// (a - u'*w') / m = error_quotient - tau*w - r*u'. Here w is
// the old w_i and u' is the updated u_i. The right-hand side
// is divisible coefficient-wise by p.
let tau_times_w = &tau_i * &w_i;
u_i = u_i + tau_i.mul_coeff(m.clone());
let r_times_u = &r_i * &u_i;
error_quotient = divide_by_prime(error_quotient - tau_times_w - r_times_u);
w_i = w_i + r_i.mul_coeff(m.clone());
m = &m * &p;
debug_assert_eq!(
error_quotient.clone().mul_coeff(m.clone()),
&a - &(&u_i * &w_i)
);
}
error_quotient.is_zero()
};
if lift_is_exact {
let content = u_i.content();
if !content.is_one() {
u_i = u_i.div_coeff(&content);
gamma = &gamma / &content;
}
if !gamma.is_one() {
w_i = w_i.div_coeff(&gamma); // true division is possible in this case
}
Ok((u_i, w_i))
} else {
if use_quadratic_lift
&& ((u_i.lcoeff() % &p).is_zero() || (w_i.lcoeff() % &p).is_zero())
{
#[cfg(test)]
QUADRATIC_HENSEL_NONUNIT_RETRIES.with(|retries| retries.set(retries.get() + 1));
// An unsuccessful quadratic lift can add a higher-degree correction whose
// leading coefficient is divisible by p. It cannot be normalized modulo p^k,
// so recompute the approximation with the degree-preserving linear lift.
return self.hensel_lift_with_strategy(u, w, Some(gamma), max_p, false);
}
if !u_i.lcoeff().is_one() {
let inv = u_i.lcoeff().mod_inverse(&m);
u_i = u_i.map_coeff(|c| (c * &inv).symmetric_mod(&m), Z);
}
if !w_i.lcoeff().is_one() {
let inv = w_i.lcoeff().mod_inverse(&m);
w_i = w_i.map_coeff(|c| (c * &inv).symmetric_mod(&m), Z);
}
Err((u_i, w_i))
}
}
/// Lift every leaf of a modular factor tree to the same prime-power modulus.
///
/// `self` may only be a modular approximation to an exact integer factor. The returned
/// leaves therefore remain modular approximations and must be recombined against a known
/// exact polynomial before they can be used with a different modulus.
fn lift_modular_factor_tree<UField: FiniteFieldWorkspace>(
&self,
hs: &[MultivariatePolynomial<FiniteField<UField>, E, LexOrder>],
max_p: &Integer,
quadratic_lift_allowed: bool,
) -> Vec<Self>
where
FiniteField<UField>: Field + PolynomialGCD<E> + FiniteFieldCore<UField>,
Integer: ToFiniteField<UField>,
{
if hs.len() == 1 {
if self.lcoeff().is_one() {
return vec![self.clone()];
} else {
let inv = self.lcoeff().mod_inverse(max_p);
let r = self.map_coeff(|c| (c * &inv).symmetric_mod(max_p), Z);
return vec![r];
}
}
let (gs, hs) = hs.split_at(hs.len() / 2);
let mut g = gs[0].one();
for x in gs {
g = g * x;
}
let mut h = hs[0].one();
for x in hs {
h = h * x;
}
let (g_i, h_i) = self
.hensel_lift_with_strategy(g, h, None, max_p, quadratic_lift_allowed)
.unwrap_or_else(|e| e);
let mut factors = g_i.lift_modular_factor_tree(gs, max_p, quadratic_lift_allowed);
factors.extend(h_i.lift_modular_factor_tree(hs, max_p, quadratic_lift_allowed));
factors
}
/// Reconstructs one primitive exact factor from a lifted monic product and
/// verifies it by exact division of this polynomial.
fn try_reconstruct_lifted_factor(
&self,
lifted: DenseIntegerUnivariatePolynomial,
context: &DenseIntegerModularUnivariateContext<E>,
modulus: &Integer,
variable: usize,
) -> Option<(Self, Self)> {
let lifted = context.from_dense_coefficients(lifted);
let mut candidate = (&self.constant(self.lcoeff()) * &lifted)
.map_coeff(|coefficient| coefficient.clone().symmetric_mod(modulus), Z);
let content = candidate.content();
if content.is_zero() {
return None;
}
candidate = candidate.div_coeff(&content);
if candidate.is_constant() || candidate.degree(variable) >= self.degree(variable) {
return None;
}
let (quotient, remainder) = self.quot_rem(&candidate, true);
if !remainder.is_zero() {
return None;
}
let quotient_content = quotient.content();
if quotient_content.is_zero() {
return None;
}
Some((candidate, quotient.div_coeff("ient_content)))
}
/// Certifies an exact factor represented by two lifted leaves and records
/// the complementary modular leaves and the local reconstruction bounds.
fn try_reconstruct_balanced_leaf_pair(
&self,
lift: &UnivariateHenselProductTreeLiftContext,
leaf_indices: [usize; 2],
context: &DenseIntegerModularUnivariateContext<E>,
modulus: &Integer,
prime: u32,
variable: usize,
) -> Option<UnivariateHenselExactPartition<Self>> {
debug_assert!(leaf_indices[0] < leaf_indices[1]);
debug_assert!(leaf_indices[1] < lift.leaves.len());
let mut lifted_product =
context.multiply_raw(&lift.leaves[leaf_indices[0]], &lift.leaves[leaf_indices[1]]);
context.reduce_in_place(&mut lifted_product);
let (factor, complement) =
self.try_reconstruct_lifted_factor(lifted_product, context, modulus, variable)?;
let factor_degree = lift.topology.leaf_degrees[leaf_indices[0]]
.checked_add(lift.topology.leaf_degrees[leaf_indices[1]])
.expect("Hensel factor degrees overflow");
if factor.degree(variable).to_u32() as usize != factor_degree {
return None;
}
let total_degree = self.degree(variable).to_u32() as usize;
if complement.degree(variable).to_u32() as usize != total_degree - factor_degree {
return None;
}
let complement_indices = (0..lift.leaves.len())
.filter(|index| !leaf_indices.contains(index))
.collect::<Vec<_>>();
let factor_bound = factor.coefficient_bound();
let complement_bound = complement.coefficient_bound();
let required_exponent = Self::linear_hensel_modulus(&factor_bound, prime)
.0
.max(Self::linear_hensel_modulus(&complement_bound, prime).0);
Some(UnivariateHenselExactPartition {
exact_parts: [factor, complement],
leaf_indices: [leaf_indices.to_vec(), complement_indices],
coefficient_bounds: [factor_bound, complement_bound],
required_exponent,
})
}
/// Recombines both sides of a certified exact partition from their current
/// lifted leaves using the coefficient bound local to each side.
fn recombine_exact_product_tree_partition(
partition: UnivariateHenselExactPartition<Self>,
lift: &UnivariateHenselProductTreeLiftContext,
context: &DenseIntegerModularUnivariateContext<E>,
modulus: &Integer,
variable: usize,
) -> Vec<Self> {
let UnivariateHenselExactPartition {
exact_parts,
leaf_indices,
coefficient_bounds,
..
} = partition;
let mut reconstructed = Vec::new();
for ((part, indices), bound) in exact_parts
.into_iter()
.zip(leaf_indices)
.zip(coefficient_bounds)
{
let lifted_factors = indices
.into_iter()
.map(|index| {
let mut coefficients = lift.leaves[index].clone();
context.symmetrize_in_place(&mut coefficients);
context.from_dense_coefficients(coefficients)
})
.collect();
reconstructed.extend(part.recombine_lifted_factors(
lifted_factors,
modulus,
variable,
&bound,
));
}
reconstructed
}
/// Certifies a root split whose one child is a single irreducible modular
/// factor, then recombines the other child once its local coefficient
/// bound is covered by the current modulus.
fn try_reconstruct_product_tree_root(
&self,
lift: &UnivariateHenselProductTreeLiftContext,
root_index: usize,
context: &DenseIntegerModularUnivariateContext<E>,
modulus: &Integer,
prime: u32,
variable: usize,
) -> Option<Vec<Self>> {
let root = lift.topology.nodes[root_index];
let (leaf_index, leaf_link, complement_link) = match root.children {
[
leaf @ UnivariateHenselProductTreeLink::Leaf(index),
complement,
] => (index, leaf, complement),
[
complement,
leaf @ UnivariateHenselProductTreeLink::Leaf(index),
] => (index, leaf, complement),
_ => return None,
};
let (leaf_factor, complement) = self.try_reconstruct_lifted_factor(
lift.value(leaf_link).to_vec(),
context,
modulus,
variable,
)?;
if leaf_factor.degree(variable).to_u32() as usize != lift.topology.leaf_degrees[leaf_index]
{
return None;
}
if let UnivariateHenselProductTreeLink::Leaf(index) = complement_link {
if complement.degree(variable).to_u32() as usize != lift.topology.leaf_degrees[index] {
return None;
}
return Some(vec![leaf_factor, complement]);
}
let complement_bound = complement.coefficient_bound();
let (_, complement_modulus) = Self::linear_hensel_modulus(&complement_bound, prime);
if modulus < &complement_modulus {
return None;
}
let lifted_factors = lift
.topology
.leaf_indices_below(complement_link)
.into_iter()
.map(|index| {
let mut coefficients = lift.leaves[index].clone();
context.symmetrize_in_place(&mut coefficients);
context.from_dense_coefficients(coefficients)
})
.collect();
let mut factors = complement.recombine_lifted_factors(
lifted_factors,
modulus,
variable,
&complement_bound,
);
factors.push(leaf_factor);
Some(factors)
}
/// Lifts all modular factors through one synchronized degree-greedy
/// product tree.
///
/// At every stage all node products and Bezout relations advance from
/// `p^old` to `p^new`. The correction ring is `Z/(p^(new-old))`, whose
/// modulus divides `p^old`; this permits a ceiling-halving schedule and
/// skips the final, no-longer-needed Bezout update. Internal factors and
/// cofactors stay in `[0, p^old)`, so adding a correction in
/// `p^old * [0, p^(new-old))` produces a canonical coefficient at the new
/// precision without another modular reduction.
fn lift_modular_factor_product_tree<UField: FiniteFieldWorkspace>(
&self,
hs: &[MultivariatePolynomial<FiniteField<UField>, E, LexOrder>],
max_p: &Integer,
) -> UnivariateHenselProductTreeLiftResult<Self>
where
FiniteField<UField>: Field + PolynomialGCD<E> + FiniteFieldCore<UField>,
Integer: ToFiniteField<UField>,
{
#[cfg(test)]
PRODUCT_TREE_HENSEL_LIFT_CALLS.with(|calls| calls.set(calls.get() + 1));
assert!(hs.len() >= 2);
let prime_workspace = hs[0].ring().get_prime();
let prime = prime_workspace.to_integer();
let prime_u32 = u32::try_from(
prime_workspace
.to_u64()
.expect("a synchronized integer Hensel prime must fit in u64"),
)
.expect("a synchronized integer Hensel prime must fit in u32");
let mut target_digits = 1usize;
let mut reconstructed_modulus = prime.clone();
while &reconstructed_modulus < max_p {
reconstructed_modulus *= ′
target_digits += 1;
}
assert_eq!(
&reconstructed_modulus, max_p,
"the Hensel target modulus must be a power of the base prime"
);
let variable = self
.last_exponents()
.iter()
.position(|exponent| !exponent.is_zero())
.expect("a Hensel factor must be nonconstant");
let dense_length = self.degree(variable).to_u32() as usize + 1;
let intermediate_length = dense_length
.checked_mul(2)
.and_then(|length| length.checked_sub(1))
.expect("dense Hensel intermediate length overflow");
let dense_indices = (0..intermediate_length)
.map(|index| index as u32)
.collect::<Vec<_>>();
let target_context =
DenseIntegerModularUnivariateContext::new(max_p, variable, self, &dense_indices);
let leading_inverse = self.lcoeff().mod_inverse(max_p);
let mut normalized_target = target_context.dense_coefficients(self);
for coefficient in &mut normalized_target {
*coefficient *= &leading_inverse;
}
target_context.reduce_in_place(&mut normalized_target);
debug_assert!(normalized_target.last().is_some_and(Integer::is_one));
let mut schedule = univariate_hensel_precision_schedule(target_digits);
let mut pending_exact_partition: Option<UnivariateHenselExactPartition<Self>> = None;
let mut lift = UnivariateHenselProductTreeLiftContext::new(hs);
let root = lift
.topology
.root
.expect("a multi-factor Hensel product tree has a root");
let root_index = match root {
UnivariateHenselProductTreeLink::Internal(index) => index,
UnivariateHenselProductTreeLink::Leaf(_) => {
unreachable!("a multi-factor Hensel product tree has an internal root")
}
};
#[cfg(debug_assertions)]
{
let base_context =
DenseIntegerModularUnivariateContext::new(&prime, variable, self, &dense_indices);
let base_target = base_context.reduce(&normalized_target);
let root_product = lift.value(root);
let length = base_target.len().max(root_product.len());
debug_assert!((0..length).all(|index| {
let target_coefficient = base_target
.get(index)
.cloned()
.unwrap_or_else(Integer::zero);
let product_coefficient = root_product
.get(index)
.cloned()
.unwrap_or_else(Integer::zero);
((target_coefficient - product_coefficient) % &prime).is_zero()
}));
}
let mut modulus = prime.clone();
let mut stage_index = 0;
while stage_index + 1 < schedule.len() {
let old_exponent = schedule[stage_index];
let new_exponent = schedule[stage_index + 1];
let correction_exponent = new_exponent - old_exponent;
debug_assert!(correction_exponent <= old_exponent);
let mut correction_modulus = Integer::one();
for _ in 0..correction_exponent {
correction_modulus *= ′
}
debug_assert!((&modulus % &correction_modulus).is_zero());
let next_modulus = &modulus * &correction_modulus;
let correction_context = DenseIntegerModularUnivariateContext::new(
&correction_modulus,
variable,
self,
&dense_indices,
);
let next_context = DenseIntegerModularUnivariateContext::new(
&next_modulus,
variable,
self,
&dense_indices,
);
struct DeferredBezoutUpdate {
node_index: usize,
u_mod: Vec<Integer>,
w_mod: Vec<Integer>,
s_mod: Vec<Integer>,
t_mod: Vec<Integer>,
}
let final_stage = stage_index + 2 == schedule.len();
let mut deferred_bezout_updates = Vec::with_capacity(if final_stage {
0
} else {
lift.topology.nodes.len()
});
for node_index in (0..lift.topology.nodes.len()).rev() {
let node = lift.topology.nodes[node_index];
let u_link = node.children[0];
let w_link = node.children[1];
let (u_mod, w_mod, s_mod, t_mod, du, dw) = {
let target = if node_index == root_index {
normalized_target.as_slice()
} else {
&lift.internal_products[node_index]
};
let u = lift.value(u_link);
let w = lift.value(w_link);
let cofactors = &lift.bezout_cofactors[node_index];
let s = &cofactors[0];
let t = &cofactors[1];
// E=(target-u*w)/m is integral because the child product
// is already correct at the old precision. Reducing E
// modulo the correction modulus determines both factor
// corrections.
let error = correction_context
.exact_product_residual_quotient_mod(target, u, w, &modulus);
let u_mod = correction_context.reduce(u);
let w_mod = correction_context.reduce(w);
let s_mod = correction_context.reduce(s);
let t_mod = correction_context.reduce(t);
// du=(E*t) rem u and dw=(E*s) rem w. Reducing E before
// each multiplication keeps the intermediate product
// below the degree of the corresponding child factor.
let du =
correction_context.multiply_remainder_monic(error.clone(), &t_mod, &u_mod);
let dw = correction_context.multiply_remainder_monic(error, &s_mod, &w_mod);
(u_mod, w_mod, s_mod, t_mod, du, dw)
};
// The old child buffers are dead after this node advances, so
// the factor corrections reuse their owned storage.
let u = lift.take_value(u_link);
let w = lift.take_value(w_link);
let lifted_u = next_context.lift_correction(u, &du, &modulus);
let lifted_w = next_context.lift_correction(w, &dw, &modulus);
debug_assert!(lifted_u.last().is_some_and(Integer::is_one));
debug_assert!(lifted_w.last().is_some_and(Integer::is_one));
#[cfg(debug_assertions)]
{
let target = if node_index == root_index {
normalized_target.as_slice()
} else {
&lift.internal_products[node_index]
};
debug_assert!(next_context.product_matches(target, &lifted_u, &lifted_w));
}
lift.set_value(u_link, lifted_u);
lift.set_value(w_link, lifted_w);
if !final_stage {
deferred_bezout_updates.push(DeferredBezoutUpdate {
node_index,
u_mod,
w_mod,
s_mod,
t_mod,
});
}
}
if let Some(partition) = pending_exact_partition.take() {
debug_assert_eq!(partition.required_exponent, new_exponent);
let factors = Self::recombine_exact_product_tree_partition(
partition,
&lift,
&next_context,
&next_modulus,
variable,
);
return UnivariateHenselProductTreeLiftResult::Exact(factors);
}
const MIN_EARLY_PRODUCT_TREE_RECONSTRUCTION_BITS: u64 = 128;
if next_modulus.significant_bits() >= MIN_EARLY_PRODUCT_TREE_RECONSTRUCTION_BITS {
#[cfg(test)]
{
PRODUCT_TREE_EARLY_RECONSTRUCTION_ATTEMPTS
.with(|attempts| attempts.set(attempts.get() + 1));
PRODUCT_TREE_EARLY_RECONSTRUCTION_EXPONENT
.with(|exponent| exponent.set(new_exponent));
}
if let Some(factors) = self.try_reconstruct_product_tree_root(
&lift,
root_index,
&next_context,
&next_modulus,
prime_u32,
variable,
) {
#[cfg(test)]
PRODUCT_TREE_EARLY_RECONSTRUCTION_SUCCESSES
.with(|successes| successes.set(successes.get() + 1));
return UnivariateHenselProductTreeLiftResult::Exact(factors);
}
let at_penultimate_global_precision = stage_index + 3 == schedule.len();
if at_penultimate_global_precision
&& let Some(leaf_indices) = lift.topology.balanced_leaf_pair_improving_root()
{
#[cfg(test)]
PRODUCT_TREE_BALANCED_PAIR_ATTEMPTS
.with(|attempts| attempts.set(attempts.get() + 1));
if let Some(partition) = self.try_reconstruct_balanced_leaf_pair(
&lift,
leaf_indices,
&next_context,
&next_modulus,
prime_u32,
variable,
) {
#[cfg(test)]
{
PRODUCT_TREE_BALANCED_PAIR_CERTIFICATES
.with(|certificates| certificates.set(certificates.get() + 1));
PRODUCT_TREE_BALANCED_PAIR_TARGET_EXPONENT
.with(|exponent| exponent.set(partition.required_exponent));
}
if partition.required_exponent <= new_exponent {
let factors = Self::recombine_exact_product_tree_partition(
partition,
&lift,
&next_context,
&next_modulus,
variable,
);
return UnivariateHenselProductTreeLiftResult::Exact(factors);
}
if let Some(shortened_target) = univariate_hensel_shortened_target(
new_exponent,
partition.required_exponent,
target_digits,
) {
debug_assert!(shortened_target - new_exponent <= new_exponent);
schedule.truncate(stage_index + 2);
schedule.push(shortened_target);
pending_exact_partition = Some(partition);
}
}
}
}
// A successful exact reconstruction makes the next Hensel stage
// unnecessary. Update the Bezout relations only after every
// available certificate has failed and another stage is needed.
for update in deferred_bezout_updates {
let node = lift.topology.nodes[update.node_index];
let [s, t] = std::mem::take(&mut lift.bezout_cofactors[update.node_index]);
let lifted_u = lift.value(node.children[0]);
let lifted_w = lift.value(node.children[1]);
// B=(1-s*U-t*W)/m gives the correction to the Bezout
// relation after U and W have been lifted.
let bezout_error = correction_context
.exact_bezout_residual_quotient_mod(&s, lifted_u, &t, lifted_w, &modulus);
// The correction modulus divides `modulus`, so adding
// `modulus*du` and `modulus*dw` does not change these
// canonical images used by the Bezout correction.
// ds=(B*s) rem W and dt=(B*t) rem U preserve the degree
// bounds of the two Bezout cofactors.
let delta_s = correction_context.multiply_remainder_monic(
bezout_error.clone(),
&update.s_mod,
&update.w_mod,
);
let delta_t = correction_context.multiply_remainder_monic(
bezout_error,
&update.t_mod,
&update.u_mod,
);
let lifted_s = next_context.lift_correction(s, &delta_s, &modulus);
let lifted_t = next_context.lift_correction(t, &delta_t, &modulus);
#[cfg(debug_assertions)]
{
debug_assert!(
next_context
.bezout_identity_matches(&lifted_s, lifted_u, &lifted_t, lifted_w,)
);
}
lift.bezout_cofactors[update.node_index] = [lifted_s, lifted_t];
}
#[cfg(test)]
if !final_stage {
PRODUCT_TREE_LAST_BEZOUT_UPDATE_EXPONENT
.with(|exponent| exponent.set(new_exponent));
}
modulus = next_modulus;
stage_index += 1;
}
debug_assert_eq!(&modulus, max_p);
debug_assert_eq!(
lift.topology.leaf_indices_in_input_order(),
(0..hs.len()).collect::<Vec<_>>()
);
UnivariateHenselProductTreeLiftResult::Lifted(
lift.leaves
.into_iter()
.map(|mut factor| {
target_context.symmetrize_in_place(&mut factor);
target_context.from_dense_coefficients(factor)
})
.collect(),
)
}
/// Continue factoring one exact Hensel child with a coefficient bound local to that child.
fn factor_exact_hensel_child(
self,
hs: &[MultivariatePolynomial<Zp, E, LexOrder>],
prime: u32,
var: usize,
quadratic_lift_allowed: bool,
) -> Vec<Self> {
if hs.len() == 1 {
return vec![self];
}
let bound = self.coefficient_bound();
let (_, max_p) = Self::linear_hensel_modulus(&bound, prime);
self.factor_hensel_subtree(hs, &max_p, &bound, var, quadratic_lift_allowed)
}
/// Factor a known exact integer polynomial through its retained modular-factor subtree.
///
/// An exact two-factor split lets each exact child use a modulus derived from its own
/// coefficient bound. If the split is only a congruence, all descendants retain this node's
/// modulus and are recombined locally before exact factors are returned to the parent.
fn factor_hensel_subtree(
&self,
hs: &[MultivariatePolynomial<Zp, E, LexOrder>],
max_p: &Integer,
bound: &Integer,
var: usize,
quadratic_lift_allowed: bool,
) -> Vec<Self> {
if hs.len() == 1 {
return vec![self.clone()];
}
#[cfg(test)]
EXACT_HENSEL_SUBTREE_MODULUS_BITS
.with(|bits| bits.borrow_mut().push(max_p.significant_bits()));
let (gs, hs) = hs.split_at(hs.len() / 2);
let mut g = gs[0].one();
for factor in gs {
g = g * factor;
}
let mut h = hs[0].one();
for factor in hs {
h = h * factor;
}
match self.hensel_lift_with_strategy(g, h, None, max_p, quadratic_lift_allowed) {
Ok((g_i, h_i)) => {
#[cfg(test)]
EXACT_HENSEL_SUBTREE_SPLITS.with(|splits| splits.set(splits.get() + 1));
let prime = gs[0].ring().get_prime();
let mut factors =
g_i.factor_exact_hensel_child(gs, prime, var, quadratic_lift_allowed);
factors.extend(h_i.factor_exact_hensel_child(
hs,
prime,
var,
quadratic_lift_allowed,
));
factors
}
Err((g_i, h_i)) => {
#[cfg(test)]
LOCAL_HENSEL_RECOMBINATION_NODES.with(|nodes| nodes.set(nodes.get() + 1));
let mut factors = g_i.lift_modular_factor_tree(gs, max_p, quadratic_lift_allowed);
factors.extend(h_i.lift_modular_factor_tree(hs, max_p, quadratic_lift_allowed));
#[cfg(debug_assertions)]
for (factor, factor_p) in factors.iter().zip(gs.iter().chain(hs)) {
let field = factor_p.ring().clone();
let lifted_mod_p = factor
.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
)
.make_monic();
debug_assert_eq!(&lifted_mod_p, factor_p);
}
self.recombine_lifted_factors(factors, max_p, var, bound)
}
}
}
/// Compute distinct-degree data for a suitable monomial-free univariate image modulo `prime`.
///
/// Equal-degree factorization is deferred until the prime selector retains this candidate.
/// A candidate whose proven factor-count lower bound exceeds the inclusive limit is reported
/// separately from a prime that changes the degree, introduces a zero constant coefficient,
/// or destroys square-freeness. Rejecting a zero constant avoids a modular factor `x` that is
/// absent from the exact monomial-free polynomial.
fn screen_univariate_mod_prime(
&self,
var: usize,
prime: u32,
max_factor_count: Option<usize>,
) -> Option<ModularPrimeScreen<E>> {
let (Some(constant), Some(leading)) = (self.coefficients.first(), self.coefficients.last())
else {
return None;
};
let modulus = Integer::Single(prime as i64);
if !self.exponents(0).iter().all(|exponent| exponent.is_zero())
|| (constant % &modulus).is_zero()
|| (leading % &modulus).is_zero()
{
return None;
}
let field = Zp::new(prime);
let image = self.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
);
if image.degree(var) != self.degree(var)
|| !image.univariate_gcd(&image.derivative(var)).is_one()
{
return None;
}
let image = image.make_monic();
let distinct_degree = if let Some(context) = DenseZpDistinctDegreeContext::new(&image, var)
{
#[cfg(test)]
DENSE_ZP_DDF_SCREENS.with(|screens| screens.set(screens.get() + 1));
context.factor(max_factor_count)
} else {
image.distinct_degree_factorization_bounded(max_factor_count)
};
match distinct_degree {
Ok(distinct_degree) => {
debug!(
"Prime {prime} yields {} modular factors",
distinct_degree.factor_count
);
Some(ModularPrimeScreen::Candidate(ModularIntegerFactorization {
field,
variable: var,
distinct_degree,
}))
}
Err(lower_bound) => {
debug!(
"Prime {prime} exceeds the modular factor limit with lower bound {lower_bound}"
);
Some(ModularPrimeScreen::FactorLimitExceeded { lower_bound })
}
}
}
/// Screen the first suitable prime in the dense-u64 range.
///
/// At the degrees where this is used, the prime bound ensures that dense modular
/// multiplication accumulates every output coefficient in one `u64` before reduction.
fn screen_dense_u64_prime(
&self,
var: usize,
maximum_prime: u64,
max_factor_count: usize,
) -> Option<ModularPrimeScreen<E>> {
let mut primes = PrimeIteratorU64::new(65_000_000);
loop {
let prime = primes.next()?;
if prime > maximum_prime {
return None;
}
if let Some(screen) =
self.screen_univariate_mod_prime(var, prime as u32, Some(max_factor_count))
{
return Some(screen);
}
}
}
/// Split the retained distinct-degree blocks into monic irreducible factors.
fn complete_equal_degree_factorization(
candidate: ModularIntegerFactorization<E>,
) -> (Zp, Vec<MultivariatePolynomial<Zp, E, LexOrder>>) {
#[cfg(test)]
{
MODULAR_INTEGER_EDF_CALLS.with(|calls| calls.set(calls.get() + 1));
LAST_MODULAR_INTEGER_EDF_PRIME.with(|prime| prime.set(candidate.field.get_prime()));
}
let factor_count = candidate.distinct_degree.factor_count;
let mut factors = Vec::with_capacity(factor_count);
for (degree, block) in candidate.distinct_degree.blocks {
debug!("DDF {} {}", block, degree);
let block_factors =
DenseZpEqualDegreeContext::factor(&block, candidate.variable, degree)
.unwrap_or_else(|| block.equal_degree_factorization(degree));
for factor in block_factors {
debug!("EDF {}", factor);
factors.push(factor);
}
}
debug_assert_eq!(factors.len(), factor_count);
(candidate.field, factors)
}
/// Return the number of base-prime digits and the prime power needed to
/// exceed an integer factor coefficient bound.
fn linear_hensel_modulus(bound: &Integer, prime: u32) -> (usize, Integer) {
let prime = Integer::from(prime);
let mut modulus = prime.clone();
let mut digits = 1usize;
while &modulus < bound {
modulus *= ′
digits += 1;
}
(digits, modulus)
}
/// Estimate the number of full correction rounds performed by the binary
/// tree of linear two-factor Hensel lifts.
fn linear_hensel_work(factor_count: usize, digits: usize) -> usize {
factor_count.saturating_sub(1).saturating_mul(digits)
}
/// Returns whether a modular factorization exceeds the degree, height,
/// factor-count, and precision thresholds for high-pressure Hensel paths.
fn has_high_linear_hensel_pressure(
degree: u32,
bound: &Integer,
factor_count: usize,
digits: usize,
) -> bool {
degree <= 64
&& bound.significant_bits() >= 256
&& factor_count >= 3
&& digits >= 64
&& Self::linear_hensel_work(factor_count, digits) >= 256
}
/// Return the estimated lift work when a dense-u64 image should replace another image.
///
/// The candidate must reduce the number of full Hensel corrections by the
/// factor-count-dependent margin used by the modular-prime selector.
fn preferred_dense_u64_factorization_work(
best_factor_count: usize,
best_digits: usize,
candidate_factor_count: usize,
candidate_digits: usize,
) -> Option<(usize, usize)> {
let best_work = Self::linear_hensel_work(best_factor_count, best_digits);
let candidate_work = Self::linear_hensel_work(candidate_factor_count, candidate_digits);
let same_or_fewer_factors = candidate_factor_count <= best_factor_count
&& candidate_work.saturating_mul(2) <= best_work;
let one_extra_factor = candidate_factor_count == best_factor_count + 1
&& best_factor_count < 10
&& candidate_work.saturating_mul(4) <= best_work;
(same_or_fewer_factors || one_extra_factor).then_some((best_work, candidate_work))
}
/// Return the largest factor count for which the next small-prime image could
/// still prevent the already-screened dense-u64 image from being selected.
///
/// A bounded distinct-degree factorization may stop once its proven lower
/// bound exceeds this value. Counts at or below the returned limit are completed,
/// preserving irreducibility proofs and unusually favorable small images.
fn competitive_small_prime_factor_limit(
degree: usize,
best_factor_count: usize,
best_digits: usize,
direct_factor_count: usize,
direct_digits: usize,
small_prime_digits: usize,
) -> usize {
let best_work = Self::linear_hensel_work(best_factor_count, best_digits);
let largest_improving_count =
((best_work.saturating_sub(1) / small_prime_digits).saturating_add(1)).min(degree);
let mut competitive_limit = 0;
for small_factor_count in 1..=largest_improving_count {
let small_work = Self::linear_hensel_work(small_factor_count, small_prime_digits);
if small_work < best_work
&& Self::preferred_dense_u64_factorization_work(
small_factor_count,
small_prime_digits,
direct_factor_count,
direct_digits,
)
.is_none()
{
competitive_limit = small_factor_count;
}
}
competitive_limit
}
fn dense_coefficients_mod(&self, var: usize, modulus: &Integer) -> Vec<Integer> {
let degree = self.degree(var).to_u32() as usize;
let mut exponents = vec![E::zero(); self.nvars()];
(0..=degree)
.map(|power| {
exponents[var] = E::from_u32(power as u32);
self.coefficient(&exponents)
.unwrap_or_else(Integer::zero)
.symmetric_mod(modulus)
})
.collect()
}
/// Compute the coefficient vector of `self * factor' / factor` modulo `modulus`.
fn logarithmic_derivative_coefficients(
&self,
factor: &Self,
var: usize,
modulus: &Integer,
) -> Vec<Integer> {
let dividend = self.dense_coefficients_mod(var, modulus);
let divisor = factor.dense_coefficients_mod(var, modulus);
let dividend_degree = dividend.len() - 1;
let divisor_degree = divisor.len() - 1;
let leading_inverse = divisor[divisor_degree].mod_inverse(modulus);
let mut remainder = dividend;
let mut quotient = vec![Integer::zero(); dividend_degree - divisor_degree + 1];
for power in (divisor_degree..=dividend_degree).rev() {
let coefficient = (&remainder[power] * &leading_inverse).symmetric_mod(modulus);
quotient[power - divisor_degree] = coefficient.clone();
for (offset, divisor_coefficient) in divisor.iter().enumerate() {
remainder[power - divisor_degree + offset] = (&remainder
[power - divisor_degree + offset]
- &coefficient * divisor_coefficient)
.symmetric_mod(modulus);
}
}
debug_assert!(
remainder
.iter()
.all(|coefficient| coefficient.clone().symmetric_mod(modulus).is_zero())
);
let derivative = divisor
.iter()
.enumerate()
.skip(1)
.map(|(power, coefficient)| {
(coefficient * &Integer::from(power)).symmetric_mod(modulus)
})
.collect::<Vec<_>>();
let mut result = vec![Integer::zero(); dividend_degree];
for (left_power, left) in quotient.iter().enumerate() {
for (right_power, right) in derivative.iter().enumerate() {
let index = left_power + right_power;
result[index] = (&result[index] + left * right).symmetric_mod(modulus);
}
}
result
}
/// Use a van-Hoeij-style coefficient-of-logarithmic-derivative lattice to partition lifted
/// modular factors. Every partition is verified by exact division before it is accepted.
fn lll_factor_recombination(
&self,
factors: &[Self],
modulus: &Integer,
var: usize,
) -> Option<Vec<Self>> {
const MAX_DATA_COLUMNS: usize = 8;
let factor_count = factors.len();
let degree = self.degree(var).to_u32() as usize;
if factor_count <= 10 || degree < 2 {
return None;
}
let scale_exponent = (usize::BITS - factor_count.max(20).leading_zeros()) as usize;
let scale = Integer::Single(2).pow(scale_exponent as u64);
let retained_bits = factor_count * 3 / 2 + scale_exponent;
let shift = modulus
.significant_bits()
.saturating_sub(retained_bits as u64);
if shift == 0 {
return None;
}
let divisor = Integer::Single(2).pow(shift);
let truncated_modulus = modulus / &divisor;
let logarithmic_derivatives = factors
.iter()
.map(|factor| self.logarithmic_derivative_coefficients(factor, var, modulus))
.collect::<Vec<_>>();
let data_columns = (0..degree.min(MAX_DATA_COLUMNS))
.map(|index| {
if index % 2 == 0 {
index / 2
} else {
degree - 1 - index / 2
}
})
.collect::<Vec<_>>();
let lattice_dimension = factor_count + data_columns.len();
let mut lattice = Vec::with_capacity(lattice_dimension);
for factor_index in 0..factor_count {
let mut row = vec![Integer::zero(); lattice_dimension];
row[factor_index] = scale.clone();
for (column, coefficient_index) in data_columns.iter().enumerate() {
row[factor_count + column] =
&logarithmic_derivatives[factor_index][*coefficient_index] / &divisor;
}
lattice.push(Vector::new(row, Z));
}
for column in 0..data_columns.len() {
let mut row = vec![Integer::zero(); lattice_dimension];
row[factor_count + column] = truncated_modulus.clone();
lattice.push(Vector::new(row, Z));
}
let reduced = Vector::basis_reduction_approximate(&lattice, 0.75)?;
let scale_squared = &scale * &scale;
let carry_bound = Integer::from(factor_count.div_ceil(2));
let mut short_bound = &scale_squared * &Integer::from(factor_count + 1);
short_bound += &Integer::from(data_columns.len()) * &(&carry_bound * &carry_bound);
short_bound *= 4;
let mut projected_rows = vec![];
for row in reduced {
if row.norm_squared() > short_bound {
continue;
}
let values = row.into_vec();
if values[..factor_count].iter().all(Integer::is_zero)
|| values[..factor_count]
.iter()
.any(|value| !(value % &scale).is_zero())
{
continue;
}
projected_rows.push(
values[..factor_count]
.iter()
.map(|value| value / &scale)
.collect::<Vec<_>>(),
);
}
if projected_rows.is_empty() {
return None;
}
let mut groups: Vec<(Vec<Integer>, Vec<usize>)> = vec![];
for factor_index in 0..factor_count {
let signature = projected_rows
.iter()
.map(|row| row[factor_index].clone())
.collect::<Vec<_>>();
if let Some((_, indices)) = groups
.iter_mut()
.find(|(existing, _)| existing == &signature)
{
indices.push(factor_index);
} else {
groups.push((signature, vec![factor_index]));
}
}
if groups.len() < 2 {
return None;
}
groups.sort_by_key(|(_, indices)| {
indices
.iter()
.map(|index| factors[*index].degree(var).to_u32())
.sum::<u32>()
});
let mut reconstructed = vec![];
let mut rest = self.clone();
for (_, indices) in groups.iter().take(groups.len() - 1) {
let mut candidate = rest.constant(rest.lcoeff());
for index in indices {
candidate = (&candidate * &factors[*index])
.map_coeff(|coefficient| coefficient.clone().symmetric_mod(modulus), Z);
}
let content = candidate.content();
candidate = candidate.div_coeff(&content);
let (quotient, remainder) = rest.quot_rem(&candidate, true);
if !remainder.is_zero() {
return None;
}
reconstructed.push(candidate);
let content = quotient.content();
rest = quotient.div_coeff(&content);
}
reconstructed.push(rest);
debug!(
"LLL recombination partitioned {} modular factors into {} exact factors",
factor_count,
reconstructed.len()
);
let mut result = vec![];
for factor in reconstructed {
if factor.degree(var) >= self.degree(var) {
return None;
}
result.extend(factor.factor_reconstruct());
}
#[cfg(test)]
LLL_RECOMBINATION_SUCCESSES.with(|successes| successes.set(successes.get() + 1));
Some(result)
}
/// Recombine modularly lifted leaves into exact factors of `self`.
///
/// Every candidate is verified by exact division. The leaves may therefore be returned only
/// to this exact target; they are not valid inputs to a recombination at another modulus.
fn recombine_lifted_factors(
&self,
mut factors: Vec<Self>,
modulus: &Integer,
var: usize,
bound: &Integer,
) -> Vec<Self> {
if factors.len() > 10
&& let Some(recombined) = self.lll_factor_recombination(&factors, modulus, var)
{
return recombined;
}
let mut reconstructed = vec![];
let mut subset_size = 1;
let mut rest = self.clone();
'subset_size: while 2 * subset_size <= factors.len() {
let mut subsets = CombinationIterator::new(factors.len(), subset_size);
while let Some(indices) = subsets.next() {
if rest.exponents[..rest.nvars()]
.iter()
.all(|exponent| *exponent == E::zero())
{
let mut selected_constant = rest.lcoeff();
let mut complement_constant = rest.lcoeff();
for (index, factor) in factors.iter().enumerate() {
if factor.exponents[..rest.nvars()]
.iter()
.all(|exponent| *exponent == E::zero())
{
if indices.contains(&index) {
selected_constant = (&selected_constant * &factor.coefficients[0])
.symmetric_mod(modulus);
} else {
complement_constant = (&complement_constant
* &factor.coefficients[0])
.symmetric_mod(modulus);
}
}
}
// TODO: improve check
// for monic factors we can do selected_constant * complement_constant !=
// rest.lcoeff() * rest.coefficients[0]
if &(&selected_constant * &complement_constant).abs() > bound {
continue;
}
}
let mut candidate = rest.constant(rest.lcoeff());
for (index, factor) in factors.iter().enumerate() {
if indices.contains(&index) {
candidate = (&candidate * factor)
.map_coeff(|coefficient| coefficient.clone().symmetric_mod(modulus), Z);
}
}
let content = candidate.content();
candidate = candidate.div_coeff(&content);
let (quotient, remainder) = rest.quot_rem(&candidate, true);
if remainder.is_zero() {
reconstructed.push(candidate);
for index in indices.iter().rev() {
factors.remove(*index);
}
let content = quotient.content();
rest = quotient.div_coeff(&content);
continue 'subset_size;
}
}
subset_size += 1;
}
reconstructed.push(rest);
reconstructed
}
/// Remove the greatest common power of `var` from every term.
///
/// The returned exponent is the multiplicity of the exact variable factor,
/// and the returned polynomial is its cofactor. Coefficients and exponents
/// of all other variables are unchanged.
fn remove_univariate_monomial_factor(&self, var: usize) -> Option<(E, Self)> {
let (power, _) = self.degree_bounds(var);
if power == E::zero() {
return None;
}
let mut cofactor = self.clone();
for exponents in cofactor.exponents_iter_mut() {
debug_assert!(exponents[var] >= power);
exponents[var] = exponents[var] - power;
}
Some((power, cofactor))
}
/// Factor a square-free univariate polynomial over the integers by Hensel lifting factors computed over
/// a finite field image of the polynomial.
fn factor_reconstruct(&self) -> Vec<Self> {
let Some(var) = self.last_exponents().iter().position(|x| *x > E::zero()) else {
return vec![self.clone()]; // constant polynomial
};
let d = self.degree(var).to_u32();
if d == 1 {
return vec![self.clone()];
}
if let Some((power, cofactor)) = self.remove_univariate_monomial_factor(var) {
let mut variable_exponents = vec![E::zero(); self.nvars()];
variable_exponents[var] = E::one();
let variable = self.monomial(self.ring().one(), variable_exponents);
let power = power.to_u32() as usize;
let mut cofactor_factors = cofactor.factor_reconstruct();
let mut factors = Vec::with_capacity(power + cofactor_factors.len());
for _ in 0..power {
factors.push(variable.clone());
}
factors.append(&mut cofactor_factors);
return factors;
}
// Select a suitable prime. The number of modular factors controls the
// exponential recombination step, so try several small primes and
// retain the factorization with the fewest factors.
let prime_trials = if d >= 128 {
10
} else if d >= 32 {
5
} else {
1
};
let mut best_factorization: Option<ModularIntegerFactorization<E>> = None;
let mut suitable_primes = 0;
let mut pi = PrimeIteratorU64::new(2);
while suitable_primes < prime_trials {
let p = pi.next().unwrap();
if p > u32::MAX as u64 {
panic!("Ran out of primes during factorization of {self}");
}
let max_factor_count = best_factorization
.as_ref()
.map(|best| best.distinct_degree.factor_count.saturating_sub(1));
let Some(screen) = self.screen_univariate_mod_prime(var, p as u32, max_factor_count)
else {
continue;
};
suitable_primes += 1;
let candidate = match screen {
ModularPrimeScreen::Candidate(candidate) => candidate,
ModularPrimeScreen::FactorLimitExceeded { .. } => continue,
};
if candidate.distinct_degree.factor_count == 1 {
// Irreducibility modulo one prime proves irreducibility over Z.
return vec![self.clone()];
}
let replace_best = best_factorization
.as_ref()
.map(|best| {
candidate.distinct_degree.factor_count < best.distinct_degree.factor_count
})
.unwrap_or(true);
if replace_best {
best_factorization = Some(candidate);
}
if best_factorization
.as_ref()
.is_some_and(|best| best.distinct_degree.factor_count <= 12)
{
break;
}
}
let bound = self.coefficient_bound();
// A wider machine prime reduces the number of full correction rounds in
// the current linear p-adic lift. Restrict it to low-degree, high-height
// images where that saving clearly outweighs the more expensive finite-
// field factorization.
let (initial_factor_count, initial_digits) = {
let candidate = best_factorization.as_ref().unwrap();
let (digits, _) = Self::linear_hensel_modulus(&bound, candidate.field.get_prime());
(candidate.distinct_degree.factor_count, digits)
};
let high_linear_lift_pressure =
Self::has_high_linear_hensel_pressure(d, &bound, initial_factor_count, initial_digits);
if high_linear_lift_pressure {
// High-degree product-tree lifts compare the first suitable image with a prime at
// least one bit wider. Intervening primes provide nearly the same p-adic precision per
// digit while requiring another complete distinct-degree factorization. If the wider
// image does not improve the estimated lift work, the skipped range is still searched.
let geometric_small_prime_trial = d >= 48 && initial_factor_count > 4;
let first_small_prime = best_factorization.as_ref().unwrap().field.get_prime();
// Keep dense products of two degree-d images in the u64 accumulator kernel. The
// bound p * (d + 1) <= u32::MAX proves both that all (d + 1)^2 products fit in u64
// and that one Montgomery reduction is sufficient for each output coefficient.
let maximum_direct_prime = u64::from(u32::MAX) / (u64::from(d) + 1);
// Once two suitable small-prime images have exposed at least ten factors, a wide
// image is screened before the third image. The third distinct-degree factorization
// is then bounded at the largest factor count that could still change the final
// selection, so favorable or irreducible small images are retained without completing
// a factorization that cannot win.
let can_probe_direct_first = geometric_small_prime_trial
&& suitable_primes == 2
&& initial_factor_count >= MIN_DENSE_U64_PRIME_FACTOR_COUNT;
let mut direct_candidate = None;
let mut direct_rejection_lower_bound = None;
let mut direct_search_exhausted = false;
if can_probe_direct_first {
match self.screen_dense_u64_prime(var, maximum_direct_prime, initial_factor_count) {
Some(ModularPrimeScreen::Candidate(candidate)) => {
let digits =
Self::linear_hensel_modulus(&bound, candidate.field.get_prime()).0;
direct_candidate = Some((candidate, digits));
}
Some(ModularPrimeScreen::FactorLimitExceeded { lower_bound }) => {
direct_rejection_lower_bound = Some(lower_bound);
}
None => direct_search_exhausted = true,
}
}
if direct_candidate
.as_ref()
.is_some_and(|(candidate, _)| candidate.distinct_degree.factor_count == 1)
{
return vec![self.clone()];
}
let direct_can_bound_third_image =
direct_candidate
.as_ref()
.is_some_and(|(candidate, candidate_digits)| {
Self::preferred_dense_u64_factorization_work(
initial_factor_count,
initial_digits,
candidate.distinct_degree.factor_count,
*candidate_digits,
)
.is_some()
&& !Self::has_high_linear_hensel_pressure(
d,
&bound,
candidate.distinct_degree.factor_count,
*candidate_digits,
)
});
if geometric_small_prime_trial {
pi = PrimeIteratorU64::new(
u64::from(first_small_prime)
.saturating_mul(2)
.saturating_add(1),
);
}
while suitable_primes < 3 {
let p = pi.next().unwrap();
if p > u32::MAX as u64 {
panic!("Ran out of primes during factorization of {self}");
}
let candidate_digits = Self::linear_hensel_modulus(&bound, p as u32).0;
let best = best_factorization.as_ref().unwrap();
let best_digits = Self::linear_hensel_modulus(&bound, best.field.get_prime()).0;
let best_factor_count = best.distinct_degree.factor_count;
let best_work = Self::linear_hensel_work(best_factor_count, best_digits);
let ordinary_factor_limit =
(best_work.saturating_sub(1) / candidate_digits).saturating_add(1);
let geometric_probe = geometric_small_prime_trial && suitable_primes == 1;
let competitive_factor_limit = direct_can_bound_third_image.then(|| {
let (direct, direct_digits) = direct_candidate.as_ref().unwrap();
Self::competitive_small_prime_factor_limit(
d as usize,
best_factor_count,
best_digits,
direct.distinct_degree.factor_count,
*direct_digits,
candidate_digits,
)
});
let max_factor_count = competitive_factor_limit
.map(|limit| limit.min(ordinary_factor_limit))
.unwrap_or(ordinary_factor_limit)
.min(if geometric_probe {
best_factor_count
} else {
usize::MAX
});
let Some(screen) =
self.screen_univariate_mod_prime(var, p as u32, Some(max_factor_count))
else {
continue;
};
suitable_primes += 1;
match screen {
ModularPrimeScreen::Candidate(candidate) => {
if candidate.distinct_degree.factor_count == 1 {
return vec![self.clone()];
}
let candidate_work = Self::linear_hensel_work(
candidate.distinct_degree.factor_count,
candidate_digits,
);
let factor_count_did_not_increase =
candidate.distinct_degree.factor_count <= best_factor_count;
if candidate_work < best_work
&& (!geometric_probe || factor_count_did_not_increase)
{
best_factorization = Some(candidate);
}
}
ModularPrimeScreen::FactorLimitExceeded { lower_bound } => {
if let Some(limit) = competitive_factor_limit
&& limit < ordinary_factor_limit
{
debug_assert!(lower_bound > limit);
#[cfg(test)]
COMPETITIVE_SMALL_PRIME_DDF_REJECTIONS
.with(|rejections| rejections.set(rejections.get() + 1));
}
}
}
if geometric_small_prime_trial && suitable_primes == 2 {
if best_factorization.as_ref().unwrap().field.get_prime() != first_small_prime {
break;
}
#[cfg(test)]
GEOMETRIC_SMALL_PRIME_BACKFILLS
.with(|backfills| backfills.set(backfills.get() + 1));
pi = PrimeIteratorU64::new(u64::from(first_small_prime).saturating_add(1));
}
}
if direct_candidate.is_none() && !direct_search_exhausted {
let best_factor_count = best_factorization
.as_ref()
.unwrap()
.distinct_degree
.factor_count;
let direct_factor_limit = if best_factor_count < 10 {
best_factor_count + 1
} else {
best_factor_count
};
let should_screen = best_factor_count >= MIN_DENSE_U64_PRIME_FACTOR_COUNT
&& (!can_probe_direct_first
|| direct_rejection_lower_bound
.is_some_and(|lower_bound| lower_bound <= direct_factor_limit));
if should_screen {
match self.screen_dense_u64_prime(
var,
maximum_direct_prime,
direct_factor_limit,
) {
Some(ModularPrimeScreen::Candidate(candidate)) => {
let digits =
Self::linear_hensel_modulus(&bound, candidate.field.get_prime()).0;
direct_candidate = Some((candidate, digits));
}
Some(ModularPrimeScreen::FactorLimitExceeded { lower_bound }) => {
debug!(
"Rejected the first suitable dense-u64 prime at modular factor lower bound {lower_bound}"
);
}
None => {}
}
}
}
if direct_candidate
.as_ref()
.is_some_and(|(candidate, _)| candidate.distinct_degree.factor_count == 1)
{
return vec![self.clone()];
}
if let Some((candidate, candidate_digits)) = direct_candidate {
let best = best_factorization.as_ref().unwrap();
let best_digits = Self::linear_hensel_modulus(&bound, best.field.get_prime()).0;
if let Some((best_work, candidate_work)) =
Self::preferred_dense_u64_factorization_work(
best.distinct_degree.factor_count,
best_digits,
candidate.distinct_degree.factor_count,
candidate_digits,
)
{
debug!(
"Selected a dense-u64 modular prime: estimated linear Hensel work {best_work} -> {candidate_work}"
);
best_factorization = Some(candidate);
}
}
}
let best_factorization = best_factorization.unwrap();
debug!(
"Selected modular factorization with {} factors",
best_factorization.distinct_degree.factor_count
);
let (field, mut hs) = Self::complete_equal_degree_factorization(best_factorization);
let (final_digits, max_p) = Self::linear_hensel_modulus(&bound, field.get_prime());
let product_tree_lift_pressure =
Self::has_high_linear_hensel_pressure(d, &bound, hs.len(), final_digits);
if product_tree_lift_pressure && hs.len() == 3 {
balance_three_factor_hensel_root(&mut hs, var);
}
let quadratic_lift_allowed = hs.len() <= 4;
if product_tree_lift_pressure && hs.len() > 4 {
return match self.lift_modular_factor_product_tree(&hs, &max_p) {
UnivariateHenselProductTreeLiftResult::Lifted(lifted) => {
self.recombine_lifted_factors(lifted, &max_p, var, &bound)
}
UnivariateHenselProductTreeLiftResult::Exact(factors) => factors,
};
}
self.factor_hensel_subtree(&hs, &max_p, &bound, var, quadratic_lift_allowed)
}
/// Lift a solution of `poly ≡ lcoeff * univariate_factors mod y mod p^k`
/// to `mod y^iterations mod p^k`.
///
/// Univariate factors must be monic and `lcoeff_y=0` should be as well.
fn bivariate_hensel_lift_bernardin(
poly: &MultivariatePolynomial<FiniteField<Integer>, E, LexOrder>,
interpolation_var: usize,
lcoeff: &MultivariatePolynomial<FiniteField<Integer>, E, LexOrder>,
univariate_factors: &[MultivariatePolynomial<FiniteField<Integer>, E, LexOrder>],
iterations: usize,
p: u32,
k: usize,
) -> Vec<MultivariatePolynomial<FiniteField<Integer>, E, LexOrder>> {
let finite_field = Zp::new(p);
// add the leading coefficient as a first factor
let mut factors = vec![lcoeff.replace(interpolation_var, &poly.ring().zero())];
for f in univariate_factors {
factors.push(f.clone());
}
let delta = Self::lift_diophantine_univariate(
&mut factors,
&poly.constant(poly.ring().one()),
finite_field.get_prime(),
k,
);
let y_poly = poly.to_univariate_polynomial_list(interpolation_var);
// extract coefficients in y
let mut u: Vec<_> = factors
.iter()
.map(|f| {
let mut dense = vec![poly.zero(); iterations + 1];
dense[0] = f.clone();
dense
})
.collect();
// update the first polynomial as it may contain y, since it's lcoeff
let y_lcoeff = lcoeff.to_univariate_polynomial_list(interpolation_var);
for (p, e) in y_lcoeff {
u[0][e.to_u32() as usize] = p;
}
let mut p = u.clone();
let mut cur_p = p[0][0].clone();
for x in &mut p.iter_mut().skip(1) {
cur_p = cur_p * &x[0];
x[0] = cur_p.clone();
}
for k in 1..iterations {
// extract the coefficient required to compute the error in y^k
// computed using a convolution
p[0][k] = u[0][k].clone();
for i in 1..factors.len() {
for j in 0..k {
p[i][k] = &p[i][k] + &(&p[i - 1][k - j] * &u[i][j]);
}
}
// find the kth power of y in f
// since we compute the error per power of y, we cannot stop on a 0 error
let e = if let Some((v, _)) = y_poly.iter().find(|e| e.1.to_u32() as usize == k) {
v - &p.last().unwrap()[k]
} else {
-p.last().unwrap()[k].clone()
};
if e.is_zero() {
continue;
}
for ((dp, f), d) in u.iter_mut().zip(factors.iter()).zip(&delta) {
dp[k] = &dp[k] + &(d * &e).quot_rem_univariate_monic(f).1;
}
// update the coefficients with the new y^k contributions
// note that the lcoeff[k] contribution is not new
let mut t = poly.zero();
for i in 1..factors.len() {
t = &u[i][0] * &t + &u[i][k] * &p[i - 1][0];
p[i][k] = &p[i][k] + &t;
}
}
// convert dense polynomials to multivariate polynomials
u.into_iter()
.map(|ts| {
let mut new_poly = poly.zero_with_capacity(ts.len());
for (i, mut f) in ts.into_iter().enumerate() {
for x in f.exponents_iter_mut() {
x[interpolation_var] = E::from_u32(i as u32);
}
new_poly = new_poly + f;
}
new_poly
})
.collect()
}
/// Factor a square-free bivariate polynomial over the integers.
fn bivariate_factor_reconstruct(&self, main_var: usize, interpolation_var: usize) -> Vec<Self> {
if self.bivariate_irreducibility_test() {
return vec![self.clone()];
}
let d2 = self.degree(interpolation_var).to_u32();
// select a suitable evaluation point, as small as possible as to not change the coefficient bound
let mut sample_point;
let mut uni_f;
let mut i = 0u64;
loop {
sample_point = i.into();
uni_f = self.replace(interpolation_var, &sample_point);
if self.degree(main_var) == uni_f.degree(main_var)
&& uni_f.gcd(&uni_f.derivative(main_var)).is_constant()
{
break;
}
i += 1;
}
// The sample is square-free by the GCD check above. Remove its integer content and use
// the reconstruction stage directly instead of repeating square-free decomposition.
let mut factor_target = uni_f.clone().make_primitive();
if factor_target.lcoeff().is_negative() {
factor_target = -factor_target;
}
let uni_fs = factor_target.factor_reconstruct();
let shifted_poly = if !sample_point.is_zero() {
self.shift_var(interpolation_var, &sample_point)
} else {
self.clone()
};
// The factor bound determines how many linear p-adic correction rounds are needed.
let bound = shifted_poly.coefficient_bound();
// Select a suitable prime. High-height two-factor images first try a few wide primes whose
// convolution coefficients still fit the direct u64 Montgomery-reduction bound.
let main_degree = self.degree(main_var).to_u32();
let use_wide_prime = uni_fs.len() == 2
&& main_degree <= BIVARIATE_WIDE_PRIME_MAX_DEGREE
&& bound.significant_bits() >= BIVARIATE_WIDE_PRIME_MIN_BOUND_BITS;
let maximum_direct_prime = u64::from(u32::MAX) / (u64::from(main_degree).saturating_add(1));
let mut wide_primes = use_wide_prime.then(|| {
PrimeIteratorU64::new(BIVARIATE_WIDE_PRIME_START).take(BIVARIATE_WIDE_PRIME_ATTEMPTS)
});
let mut small_primes = PrimeIteratorU64::new(101);
let mut field;
'new_prime: loop {
let p = match wide_primes.as_mut().and_then(Iterator::next) {
Some(p) if p <= maximum_direct_prime => p,
Some(_) | None => {
wide_primes = None;
small_primes.next().unwrap()
}
};
if p > u32::MAX as u64 {
panic!("Ran out of primes during factorization of {self}");
}
let p = p as u32;
if (&uni_f.lcoeff() % &Integer::Single(p as i64)).is_zero() {
continue;
}
field = Zp::new(p);
// make sure the factors stay coprime
let fs_p: Vec<_> = uni_fs
.iter()
.map(|f| f.map_coeff(|c| c.to_finite_field(&field), field.clone()))
.collect();
for (j, f) in fs_p.iter().enumerate() {
for g in &fs_p[j + 1..] {
if !f.gcd(g).is_one() {
continue 'new_prime;
}
}
}
break;
}
#[cfg(test)]
LAST_BIVARIATE_RECONSTRUCTION_PRIME.with(|prime| prime.set(field.get_prime()));
let p = field.get_prime().to_integer();
let mut max_p = p.clone();
let mut k = 1;
while &max_p * 2 < bound {
max_p = &max_p * &p;
k += 1;
}
let mod_field = FiniteField::<Integer>::new_non_prime(max_p.clone());
// make all factors monic, this is possible since the lcoeff is invertible mod p^k
let uni_fs_mod: Vec<_> = uni_fs
.iter()
.map(|f| {
let f1 = f.map_coeff(|c| mod_field.to_element(c.clone()), mod_field.clone());
f1.make_monic()
})
.collect();
let mut f_mod =
shifted_poly.map_coeff(|c| mod_field.to_element(c.clone()), mod_field.clone());
// make sure the lcoeff is monic at y=0
let inv_coeff = mod_field.inv(&mod_field.to_element(uni_f.lcoeff().clone()));
let f_mod_monic = f_mod.clone().mul_coeff(inv_coeff);
let lcoeff_monic = f_mod_monic.lcoeff_last_varorder(&[main_var, interpolation_var]);
let mut factors = Self::bivariate_hensel_lift_bernardin(
&f_mod_monic,
interpolation_var,
&lcoeff_monic,
&uni_fs_mod,
(d2 + 1) as usize,
field.get_prime(),
k,
);
factors.swap_remove(0); // remove the lcoeff
let mut rec_factors = vec![];
// factor recombination
let mut s = 1;
let mut lcoeff = f_mod.lcoeff_last_varorder(&[main_var, interpolation_var]);
let mut rest = shifted_poly;
'len: while 2 * s <= factors.len() {
let mut fs = CombinationIterator::new(factors.len(), s);
while let Some(cs) = fs.next() {
let mut g = lcoeff.clone();
for (i, f) in factors.iter().enumerate() {
if cs.contains(&i) {
g = (&g * f).mod_var(interpolation_var, E::from_u32(d2 + 1));
}
}
// convert to integer
let mut g_int = g.map_coeff(|c| mod_field.to_symmetric_integer(c), Z);
let content = g_int.univariate_content(main_var);
g_int = &g_int / &content;
let (h, r) = rest.quot_rem(&g_int, true);
if r.is_zero() {
rec_factors.push(g_int);
for i in cs.iter().rev() {
factors.remove(*i);
}
rest = h;
f_mod = rest.map_coeff(|c| mod_field.to_element(c.clone()), mod_field.clone());
lcoeff = f_mod.lcoeff_last_varorder(&[main_var, interpolation_var]);
continue 'len;
}
}
s += 1;
}
rec_factors.push(rest);
if !sample_point.is_zero() {
for x in &mut rec_factors {
// shift the polynomial to y - sample
*x = x.shift_var(interpolation_var, &self.ring().neg(&sample_point));
}
}
rec_factors
}
/// Solve a Diophantine equation over the ring `Z_p^k` using Newton iteration.
/// All factors must be monic.
fn lift_diophantine_univariate(
factors: &mut [MultivariatePolynomial<FiniteField<Integer>, E, LexOrder>],
rhs: &MultivariatePolynomial<FiniteField<Integer>, E, LexOrder>,
p: u32,
k: usize,
) -> Vec<MultivariatePolynomial<FiniteField<Integer>, E, LexOrder>> {
let field = Zp::new(p);
let prime: Integer = (p as u64).into();
let mut f_p: Vec<_> = factors
.iter()
.map(|f| {
f.map_coeff(
|c| rhs.ring().to_symmetric_integer(c).to_finite_field(&field),
field.clone(),
)
})
.collect();
let rhs_p = rhs.map_coeff(
|c| rhs.ring().to_symmetric_integer(c).to_finite_field(&field),
field.clone(),
);
// TODO: recycle from finite field computation that must have happened earlier
let mut delta =
MultivariatePolynomial::<Zp, E, LexOrder>::diophantine_univariate(&mut f_p, &rhs_p);
let mut deltas: Vec<_> = delta
.iter()
.map(|s| {
s.map_coeff(
|c| field.to_symmetric_integer(c).to_finite_field(&rhs.ring()),
rhs.ring().clone(),
)
})
.collect();
if k == 1 {
return deltas;
}
let mut tot = rhs.constant(rhs.ring().one());
for f in factors.iter() {
tot = &tot * f;
}
let pi = factors
.iter_mut()
.map(|f| tot.quot_rem_univariate(f).0)
.collect::<Vec<_>>();
let mut m = prime.clone();
for _ in 1..k {
// TODO: is breaking on e=0 safe?
let mut e = rhs.clone();
for (dd, pp) in deltas.iter_mut().zip(&pi) {
e = &e - &(&*dd * pp);
}
let e_m = e.map_coeff(
|c| (&rhs.ring().to_symmetric_integer(c) / &m).to_finite_field(&field),
field.clone(),
);
for ((p, d_m), d) in f_p.iter_mut().zip(&mut delta).zip(deltas.iter_mut()) {
let new_delta = (&e_m * &*d_m).quot_rem_univariate(p).1;
*d = &*d
+ &new_delta.map_coeff(
|c| (&field.to_symmetric_integer(c) * &m).to_finite_field(&rhs.ring()),
rhs.ring().clone(),
);
}
m = &m * ′
}
deltas
}
/// Compute the Gelfond bound for the coefficients magnitude of every factor of this polynomial
fn coefficient_bound(&self) -> Integer {
let max_norm = self.coefficients.iter().map(|x| x.abs()).max().unwrap();
let mut bound = Integer::one();
let mut total_degree = 0;
let mut non_zero_vars = 0;
for v in 0..self.nvars() {
let d = self.degree(v).to_u32() as u64;
if d > 0 {
non_zero_vars += 1;
total_degree += d;
bound *= &Integer::from(d + 1);
}
}
// move the 2^n into the sqrt to prevent precision loss when converting the sqrt
// to an integer
bound = &bound * &Integer::Single(2).pow((total_degree * 2).saturating_sub(non_zero_vars));
let root_bound = match &bound {
Integer::Single(b) => Integer::Single((*b as f64).sqrt() as i64),
Integer::Double(_) | Integer::Large(_) => bound.root(2),
};
bound = &root_bound + &1i64.into();
&bound * &(&max_norm * &self.lcoeff().abs())
}
/// Sort the bivariate factors based on their univariate image so that they are
/// aligned between the different vars.
// TODO: merge with the implementation for finite fields as the implementation
// is almost identical
fn canonical_sort(
biv_polys: &[Self],
replace_var: usize,
sample_points: &[(usize, Integer)],
) -> Vec<(Self, Integer, Self)> {
let mut univariate_factors = biv_polys
.iter()
.map(|f| {
let mut u = f.clone();
for (v, p) in sample_points {
if *v == replace_var {
u = u.replace(*v, p);
}
}
// make sure the representative is unique
let mut uni = u.clone().make_primitive();
if uni.lcoeff().is_negative() {
uni = -uni;
}
(f.clone(), u.lcoeff(), uni)
})
.collect::<Vec<_>>();
univariate_factors.sort_by(|(_, _, a), (_, _, b)| {
a.exponents
.cmp(&b.exponents)
.then(a.coefficients.partial_cmp(&b.coefficients).unwrap())
});
univariate_factors
}
/// Return whether `factor` is a unit multiple of one polynomial variable.
fn is_primitive_variable_factor(factor: &Self) -> bool {
factor.nterms() == 1
&& factor.lcoeff().abs().is_one()
&& factor
.exponents
.iter()
.filter(|exponent| **exponent != E::zero())
.count()
== 1
&& factor
.exponents
.iter()
.any(|exponent| *exponent == E::one())
}
/// Assign the irreducible factors of `lcoeff` to sampled polynomial
/// factors and return their exact multivariate leading coefficients. The
/// returned integer is the remaining unit relating their product to
/// `lcoeff` after `univariate_content` is restored.
fn reconstruct_lcoeffs_from_univariate_sample_up_to_unit(
&self,
lcoeff: &Self,
lcoeff_factorization: &[(Self, usize)],
univariate_factors: &[Self],
sample_points: &[(usize, Integer)],
univariate_content: &Integer,
) -> Option<(Integer, Vec<Self>)> {
let lcoeff_content = lcoeff.content().abs();
let mut lcoeff_factors = Vec::new();
for (f, pow) in lcoeff_factorization {
if f.is_constant() {
continue;
}
let mut eval = f.clone();
for (v, p) in sample_points {
eval = eval.replace(*v, p);
}
if !eval.is_constant() {
return None;
}
let eval = eval.get_constant().abs();
if eval <= 1 {
return None;
}
if Self::is_primitive_variable_factor(f) && !eval.gcd(&lcoeff_content).is_one() {
return None;
}
lcoeff_factors.push((f, pow, eval));
}
for (i, (_, _, a)) in lcoeff_factors.iter().enumerate() {
for (_, _, b) in &lcoeff_factors[i + 1..] {
if !a.gcd(b).is_one() {
return None;
}
}
}
let mut true_lcoeffs = vec![self.one(); univariate_factors.len()];
let mut used = vec![0usize; lcoeff_factors.len()];
let mut residual_lcs: Vec<_> = univariate_factors.iter().map(|f| f.lcoeff()).collect();
for (i, residual) in residual_lcs.iter_mut().enumerate() {
for (j, (factor, multiplicity, eval)) in lcoeff_factors.iter().enumerate() {
while used[j] < **multiplicity && (&residual.abs() % eval).is_zero() {
true_lcoeffs[i] = &true_lcoeffs[i] * factor;
*residual = &*residual / eval;
used[j] += 1;
}
}
}
if used
.iter()
.zip(&lcoeff_factors)
.any(|(used, (_, multiplicity, _))| *used != **multiplicity)
{
return None;
}
for (lcoeff_i, factor) in true_lcoeffs.iter_mut().zip(univariate_factors) {
let mut eval = lcoeff_i.clone();
for (v, p) in sample_points {
eval = eval.replace(*v, p);
}
if !eval.is_constant() {
return None;
}
let eval = eval.get_constant();
if eval.is_zero() {
return None;
}
let univariate_lcoeff = factor.lcoeff();
if (&univariate_lcoeff % &eval) != 0 {
return None;
}
*lcoeff_i = lcoeff_i.clone().mul_coeff(&univariate_lcoeff / &eval);
}
let mut product = self.one();
for l in &true_lcoeffs {
product = &product * l;
}
let reconstructed = product.mul_coeff(univariate_content.clone());
if reconstructed == *lcoeff {
Some((Integer::one(), true_lcoeffs))
} else if -reconstructed == *lcoeff {
Some((Integer::from(-1), true_lcoeffs))
} else {
None
}
}
/// Reconstruct sampled factor leading coefficients when their product has
/// exactly the requested sign.
fn reconstruct_lcoeffs_from_univariate_sample(
&self,
lcoeff: &Self,
lcoeff_factorization: &[(Self, usize)],
univariate_factors: &[Self],
sample_points: &[(usize, Integer)],
univariate_content: &Integer,
) -> Option<Vec<Self>> {
let (unit, true_lcoeffs) = self.reconstruct_lcoeffs_from_univariate_sample_up_to_unit(
lcoeff,
lcoeff_factorization,
univariate_factors,
sample_points,
univariate_content,
)?;
unit.is_one().then_some(true_lcoeffs)
}
/// Reconstruct and align factor leading coefficients from one admissible
/// bivariate Wang image, rescaling the image factors for Hensel lifting.
fn wang_lcoeff_precomputation(
&self,
bivariate_factors: &[Self],
sample_points: &[(usize, Integer)],
order: &[usize],
) -> Result<(Integer, Vec<Self>, Vec<Self>), usize> {
let lcoeff = self.univariate_lcoeff(order[0]);
let normalized_lcoeff = if lcoeff.lcoeff().is_negative() {
-lcoeff.clone()
} else {
lcoeff.clone()
};
let lcoeff_factorization = normalized_lcoeff.factor();
let sorted_biv_factors = Self::canonical_sort(bivariate_factors, order[1], sample_points)
.into_iter()
.map(|(f, _, _)| f)
.collect::<Vec<_>>();
let univariate_factor_images = sorted_biv_factors
.iter()
.map(|factor| {
let mut image = factor.clone();
for (v, p) in sample_points {
image = image.replace(*v, p);
}
image
})
.collect::<Vec<_>>();
let Some((lcoeff_unit, mut true_lcoeffs)) = self
.reconstruct_lcoeffs_from_univariate_sample_up_to_unit(
&lcoeff,
&lcoeff_factorization,
&univariate_factor_images,
sample_points,
&Integer::one(),
)
else {
return Err(sorted_biv_factors.len());
};
let mut sorted_biv_factors = sorted_biv_factors;
let mut lcoeff_left = self.constant(lcoeff_unit);
for (f, b) in true_lcoeffs.iter_mut().zip(&mut sorted_biv_factors) {
let mut b_eval = b.clone();
for (v, p) in sample_points {
b_eval = b_eval.replace(*v, p);
}
let b_lc = b_eval.lcoeff();
let mut f_eval = f.clone();
for (v, p) in sample_points {
f_eval = f_eval.replace(*v, p);
}
let f_lc = f_eval.lcoeff();
debug_assert_eq!(b_lc, f_lc);
let lcm = b_lc.lcm(&f_lc);
let b_cor = &lcm / &b_lc;
let f_cor = lcm / &f_lc;
debug_assert!(b_cor.abs().is_one());
debug_assert!(f_cor.abs().is_one());
*b = b.clone().mul_coeff(b_cor);
lcoeff_left = lcoeff_left.div_coeff(&f_cor);
*f = f.clone().mul_coeff(f_cor);
}
Ok((lcoeff_left.get_constant(), sorted_biv_factors, true_lcoeffs))
}
/// Precompute the leading coefficients of the polynomial factors, using an
/// adapted version of Kaltofen's algorithm that has modifications of Martin Lee and Stanislav Poslavsky.
///
// TODO: merge with the implementation for finite fields as the implementation
// is almost identical
#[allow(dead_code)]
fn lcoeff_precomputation(
&self,
bivariate_factors: &[Self],
sample_points: &[(usize, Integer)],
order: &[usize],
bound: Integer,
p: u32,
k: usize,
) -> Result<(Integer, Vec<Self>, Vec<Self>), usize> {
let lcoeff = self.univariate_lcoeff(order[0]);
let sqf = lcoeff.square_free_factorization();
let mut lcoeff_square_free = self.one();
for (f, _) in &sqf {
lcoeff_square_free = &lcoeff_square_free * f;
}
let sorted_main_factors = Self::canonical_sort(bivariate_factors, order[1], sample_points);
let mut true_lcoeffs: Vec<_> = bivariate_factors.iter().map(|_| self.one()).collect();
let mut lcoeff_left = lcoeff.clone();
let mut main_bivariate_factors: Vec<_> =
sorted_main_factors.into_iter().map(|(f, _, _)| f).collect();
// TODO: smarter ordering
for (i, &var) in order[1..].iter().enumerate() {
if lcoeff_left.is_one() {
break;
}
if lcoeff_left.degree(var).is_zero() {
continue;
}
// only construct factors that depend on var and remove integer content and unit
let c = lcoeff_square_free.univariate_content(var);
let mut lcoeff_square_free_pp = &lcoeff_square_free / &c;
// check if the evaluated leading coefficient remains square free
let mut poly_eval = lcoeff_square_free_pp.clone();
for (v, p) in sample_points {
if *v != var {
poly_eval = poly_eval.replace(*v, p);
}
}
if poly_eval.lcoeff().is_negative() {
lcoeff_square_free_pp = -lcoeff_square_free_pp;
poly_eval = -poly_eval;
}
debug!("Content-free lcsqf {}", lcoeff_square_free_pp);
let sqf = poly_eval.square_free_factorization();
if sqf.len() != 1 || sqf[0].1 != 1 {
debug!("Polynomial is not square free: {}", poly_eval);
return Err(main_bivariate_factors.len());
}
let bivariate_factors = if var == order[1] {
main_bivariate_factors.to_vec()
} else {
let mut poly_eval = self.clone();
for (v, p) in sample_points {
if *v != var {
poly_eval = poly_eval.replace(*v, p);
}
}
if poly_eval.degree(order[0]) != self.degree(order[0])
|| poly_eval.degree(var) != self.degree(var)
|| poly_eval.univariate_lcoeff(order[0]).degree(var) != lcoeff.degree(var)
{
debug!("Bad sample for reconstructing lcoeff: degrees do not match");
return Err(main_bivariate_factors.len());
}
let bivariate_factors: Vec<_> = poly_eval
.factor()
.into_iter()
.map(|(f, _)| f)
// remove spurious content caused by particular evaluation point
.filter(|f| !f.is_constant())
.collect();
if bivariate_factors.len() != main_bivariate_factors.len() {
return Err(bivariate_factors.len().min(main_bivariate_factors.len()));
}
Self::canonical_sort(&bivariate_factors, var, sample_points)
.into_iter()
.map(|(f, _, _)| f)
.collect()
};
let square_free_lc_biv_factors: Vec<_> = bivariate_factors
.iter()
.map(|f| {
let mut sff = f.univariate_lcoeff(order[0]).square_free_factorization();
// make sure every bivariate factor has positive lcoeff such that the product
// of the basis elements equals the evaluated lcoeff_square_free_pp
for (b, _) in &mut sff {
if b.lcoeff().is_negative() {
*b = -b.clone();
}
}
sff
})
.collect();
let basis = Self::gcd_free_basis(
square_free_lc_biv_factors
.iter()
.flatten()
.map(|x| x.0.clone())
.filter(|x| !x.is_constant())
.collect(),
);
if basis.is_empty() {
continue;
}
let lifted = if basis.len() == 1 {
vec![lcoeff_square_free_pp.clone()]
} else {
let mut new_order = order.to_vec();
new_order.swap(1, i + 1);
new_order.remove(0);
lcoeff_square_free_pp.multivariate_hensel_lift_with_auto_lcoeff_fixing(
&basis,
sample_points,
&new_order,
bound.clone(),
p,
k,
)
};
for (l, fac) in true_lcoeffs.iter_mut().zip(&square_free_lc_biv_factors) {
let mut contrib = self.one();
for (full, b) in lifted.iter().zip(&basis) {
// check if a GCD-free basis element is a factor of the leading coefficient of this bivariate factor
if let Some((_, m)) = fac.iter().find(|(f, _)| f == b || f.try_div(b).is_some())
{
for _ in 0..*m {
contrib = &contrib * full;
}
}
}
let g = contrib.gcd(l);
let new = (contrib / &g).make_primitive();
*l = (&*l * &new).make_primitive();
let (q, r) = lcoeff_left.quot_rem(&new, true);
if !r.is_zero() {
panic!(
"Problem with bivariate factor scaling in factorization of {self}: order={order:?}, samples={sample_points:?}"
);
}
lcoeff_left = q;
}
}
if !lcoeff_left.is_constant() {
panic!(
"Could not reconstruct leading coefficient of {self}: order={order:?}, samples={sample_points:?} Rest = {lcoeff_left}"
);
}
// rescale the leading coefficient factors to recover the missing content and sign
for (f, b) in true_lcoeffs.iter_mut().zip(&mut main_bivariate_factors) {
let mut b_eval = b.clone();
for (v, p) in sample_points {
b_eval = b_eval.replace(*v, p);
}
let b_lc = b_eval.lcoeff();
let mut f_eval = f.clone();
for (v, p) in sample_points {
f_eval = f_eval.replace(*v, p);
}
let f_lc = f_eval.lcoeff();
let lcm = b_lc.lcm(&f_lc);
let b_cor = &lcm / &b_lc;
let f_cor = lcm / &f_lc;
*b = b.clone().mul_coeff(b_cor);
lcoeff_left = lcoeff_left.div_coeff(&f_cor);
*f = f.clone().mul_coeff(f_cor);
}
Ok((
lcoeff_left.get_constant(),
main_bivariate_factors,
true_lcoeffs,
))
}
#[allow(dead_code)]
fn multivariate_hensel_lift_with_auto_lcoeff_fixing(
&self,
factors: &[Self],
sample_points: &[(usize, Integer)],
order: &[usize],
bound: Integer,
p: u32,
k: usize,
) -> Vec<Self> {
let modulus = FiniteField::<Integer>::new(bound);
let ff = self.map_coeff(|c| modulus.to_element(c.clone()), modulus.clone());
let factors_ff: Vec<_> = factors
.iter()
.map(|f| f.map_coeff(|c| modulus.to_element(c.clone()), modulus.clone()))
.collect();
let sample_points_ff: Vec<_> = sample_points
.iter()
.map(|(v, p)| (*v, modulus.to_element(p.clone())))
.collect();
let lcoeff = ff.univariate_lcoeff(order[0]);
if lcoeff.is_constant() {
// the factors should be properly normalized
let (mut uni, delta) = MultivariatePolynomial::get_univariate_factors_and_deltas(
&factors_ff,
order,
sample_points,
p,
k,
);
let h = ff
.multivariate_hensel_lifting(
&factors_ff,
&mut uni,
&delta,
&sample_points_ff,
None,
order,
MultivariateHenselContext::new(1),
)
.unwrap();
return h
.into_iter()
.map(|f| f.map_coeff(|c| modulus.to_symmetric_integer(c), Z))
.collect();
}
// repeat the leading coefficient for every factor so that the leading coefficient is known
let padded_lcoeffs = vec![lcoeff.clone(); factors.len()];
let mut self_adjusted = ff;
for _ in 1..factors_ff.len() {
self_adjusted = &self_adjusted * &lcoeff;
}
// set the proper lc
let mut lc_var_eval = lcoeff.clone();
for (v, p) in sample_points {
if *v != order[0] {
lc_var_eval = lc_var_eval.replace(*v, &lc_var_eval.ring().to_element(p.clone()));
}
}
let adjusted_factors: Vec<_> = factors_ff
.into_iter()
.map(|f| f.make_monic() * &lc_var_eval)
.collect();
let (mut uni, delta) = MultivariatePolynomial::get_univariate_factors_and_deltas(
&adjusted_factors,
order,
sample_points,
p,
k,
);
let h = self_adjusted
.multivariate_hensel_lifting(
&adjusted_factors,
&mut uni,
&delta,
&sample_points_ff,
Some(&padded_lcoeffs),
order,
MultivariateHenselContext::new(1),
)
.unwrap();
h.into_iter()
.map(|f| {
let f_i = f.map_coeff(|c| modulus.to_symmetric_integer(c), Z);
let c = f_i.univariate_content(order[0]);
f_i / &c
})
.collect()
}
fn lcoeff_sample_preserves_square_free_images(
&self,
order: &[usize],
lcoeff: &Self,
lcoeff_square_free: &Self,
sample_points: &[(usize, Integer)],
) -> bool {
for &var in &order[1..] {
if lcoeff.degree(var).is_zero() {
continue;
}
let content = lcoeff_square_free.univariate_content(var);
let lcoeff_square_free_pp = lcoeff_square_free / &content;
let target_degree = lcoeff_square_free_pp.degree(var);
let mut poly_eval = lcoeff_square_free_pp;
for (v, p) in sample_points {
if *v != var {
poly_eval = poly_eval.replace(*v, p);
}
}
if poly_eval.lcoeff().is_negative() {
poly_eval = -poly_eval;
}
if poly_eval.degree(var) != target_degree {
return false;
}
let derivative = poly_eval.derivative(var);
if !poly_eval.gcd(&derivative).is_constant() {
return false; // not square-free
}
}
true
}
/// Check that evaluated leading-coefficient factors can be assigned uniquely
/// to the factors of the univariate image.
fn lcoeff_sample_supports_wang_reconstruction(
lcoeff_factorization: &[(Self, usize)],
lcoeff_content: &Integer,
sample_points: &[(usize, Integer)],
) -> bool {
let mut images = Vec::new();
for (factor, _) in lcoeff_factorization {
if factor.is_constant() {
continue;
}
let mut image = factor.clone();
for (v, p) in sample_points {
image = image.replace(*v, p);
}
if !image.is_constant() {
return false;
}
let image = image.get_constant().abs();
if image <= 1 {
return false;
}
if Self::is_primitive_variable_factor(factor) && !image.gcd(lcoeff_content).is_one() {
return false;
}
if images
.iter()
.any(|prev: &Integer| !prev.gcd(&image).is_one())
{
return false;
}
images.push(image);
}
true
}
/// Find an admissible bivariate factorization image. A successful
/// deterministic Wang sample also returns the factor leading coefficients
/// needed by the subsequent Hensel lift.
#[allow(dead_code)]
fn find_sample(
&self,
order: &mut [usize],
mut coefficient_upper_bound: i64,
mut max_factors_num: Option<usize>,
) -> (
Vec<Self>,
Vec<(usize, Integer)>,
i64,
Self,
Option<(Integer, Vec<Self>)>,
) {
debug!("Find sample for {} with order {:?}", self, order);
// select a suitable evaluation point, as small as possible as to not change the coefficient bound
let mut cur_sample_points: Vec<_> =
order[1..].iter().map(|i| (*i, Integer::zero())).collect();
let mut cur_uni_f;
let mut cur_biv_f;
let mut rng = rng();
let degree = self.degree(order[0]);
let mut bivariate_factors: Vec<_>;
let mut best: Option<(Integer, Vec<Self>, Vec<(usize, Integer)>, i64, Self)> = None;
let uni_lcoeff = self.univariate_lcoeff(order[0]);
let can_accept_first_primitive_image =
max_factors_num.is_none() && uni_lcoeff.is_constant();
let can_reconstruct_monomial_lcoeff = integer_factor_start_mode()
== IntegerFactorStart::Auto
&& max_factors_num.is_none()
&& order.len() <= INTEGER_FACTOR_BIVARIATE_WANG_MAX_VARIABLES
&& order[1..].iter().fold(0u32, |total, variable| {
total.saturating_add(self.degree(*variable).to_u32())
}) <= INTEGER_FACTOR_BIVARIATE_WANG_MAX_EVALUATED_TOTAL_DEGREE
&& integer_factor_bivariate_wang_density_supported(
self.nterms(),
self.degree(order[0]).to_u32() as usize,
self.degree(order[1]).to_u32() as usize,
)
&& uni_lcoeff.nterms() == 1
&& !uni_lcoeff.is_constant();
let lcoeff_content = uni_lcoeff.content().abs();
let lcoeff_factorization = can_reconstruct_monomial_lcoeff.then(|| uni_lcoeff.factor());
let mut wang_sample_attempts = 0;
let wang_sample_primes = if can_reconstruct_monomial_lcoeff {
let mut primes = PrimeIteratorU64::new(1);
(0..cur_sample_points.len())
.map(|_| {
loop {
let prime = Integer::from(primes.next().unwrap());
if !(&lcoeff_content % &prime).is_zero() {
break prime;
}
}
})
.collect::<Vec<_>>()
} else {
vec![]
};
let wang_sample_attempt_limit =
BIVARIATE_WANG_PRIME_SAMPLE_ROTATIONS.min(wang_sample_primes.len());
let initial_coefficient_upper_bound = coefficient_upper_bound;
let mut initialized_random_fallback = false;
let mut lcoeff_square_free = self.one();
for (f, _) in uni_lcoeff.square_free_factorization() {
lcoeff_square_free = &lcoeff_square_free * &f;
}
let mut content_try_count = 0;
let mut lcoeff_try_count = 0;
'new_sample: loop {
let use_wang_sample = can_reconstruct_monomial_lcoeff
&& max_factors_num.is_none()
&& wang_sample_attempts < wang_sample_attempt_limit;
if can_reconstruct_monomial_lcoeff
&& !use_wang_sample
&& max_factors_num.is_none()
&& wang_sample_attempts > 0
&& !initialized_random_fallback
{
coefficient_upper_bound = initial_coefficient_upper_bound;
content_try_count = 0;
lcoeff_try_count = 0;
initialized_random_fallback = true;
}
if use_wang_sample {
#[cfg(test)]
BIVARIATE_WANG_SAMPLE_TRIES.with(|count| count.set(count.get() + 1));
let rotation = wang_sample_attempts;
wang_sample_attempts += 1;
for (index, sample) in cur_sample_points.iter_mut().enumerate() {
sample.1 =
wang_sample_primes[(index + rotation) % wang_sample_primes.len()].clone();
}
if !Self::lcoeff_sample_supports_wang_reconstruction(
lcoeff_factorization.as_ref().unwrap(),
&lcoeff_content,
&cur_sample_points,
) {
continue;
}
} else {
for sample in &mut cur_sample_points {
sample.1 = Integer::Single(rng.random_range(0..=coefficient_upper_bound));
}
}
for sample in &cur_sample_points {
debug!("Sample x{} {}", sample.0, sample.1);
}
cur_biv_f = self.clone();
for ((v, s), rem_var) in cur_sample_points[1..].iter().zip(&order[1..]).rev() {
cur_biv_f = cur_biv_f.replace(*v, s);
if cur_biv_f.degree(*rem_var) != self.degree(*rem_var) {
coefficient_upper_bound += 10;
continue 'new_sample;
}
}
// requirement for leading coefficient precomputation
if cur_biv_f.univariate_lcoeff(order[0]).degree(order[1]) != uni_lcoeff.degree(order[1])
{
debug!(
"Degree of x{} in leading coefficient of bivariate image is wrong",
order[1]
);
coefficient_upper_bound += 10;
continue 'new_sample;
}
if !self.lcoeff_sample_preserves_square_free_images(
order,
&uni_lcoeff,
&lcoeff_square_free,
&cur_sample_points,
) {
debug!(
"Bad sample for reconstructing lcoeff: square-free lcoeff image is not square-free"
);
lcoeff_try_count += 1;
if lcoeff_try_count == 10 {
coefficient_upper_bound += 10;
lcoeff_try_count = 0;
}
continue 'new_sample;
}
lcoeff_try_count = 0;
if use_wang_sample && !cur_biv_f.content().abs().is_one() {
continue;
}
let biv_df = cur_biv_f.derivative(order[0]);
cur_uni_f = cur_biv_f.replace(cur_sample_points[0].0, &cur_sample_points[0].1);
let uni_df = cur_uni_f.derivative(order[0]);
if degree == cur_biv_f.degree(order[0])
&& degree == cur_uni_f.degree(order[0])
&& cur_biv_f.gcd(&biv_df).is_constant()
&& cur_uni_f.gcd(&uni_df).is_constant()
{
let c = cur_biv_f.univariate_content(order[0]);
if !c.is_constant() {
if use_wang_sample {
continue;
}
content_try_count += 1;
coefficient_upper_bound += 10;
debug!("Univariate content is not constant");
if content_try_count == 10 {
// it is likely that we will always find content for this variable ordering, so change the
// second variable
// TODO: is this guaranteed to work or should we also change the first variable?
let sec_var = order[1];
order.copy_within(2..order.len(), 1);
order[order.len() - 1] = sec_var;
for ((vs, _), v) in cur_sample_points.iter_mut().zip(&order[1..]) {
*vs = *v;
}
debug!("Changed the second variable to {}", order[1]);
content_try_count = 0;
}
continue;
}
if use_wang_sample && !c.get_constant().abs().is_one() {
continue;
}
#[cfg(test)]
BIVARIATE_SAMPLE_FACTORIZATIONS.with(|count| count.set(count.get() + 1));
let retained_variables_start_at_zero = [order[0], order[1]]
.into_iter()
.all(|variable| cur_biv_f.degree_bounds(variable).0 == E::zero());
bivariate_factors =
if c.get_constant().abs().is_one() && retained_variables_start_at_zero {
cur_biv_f.bivariate_factor_reconstruct(order[0], order[1])
} else {
let mut factors = cur_biv_f
.factor()
.into_iter()
.map(|factor| factor.0)
.collect::<Vec<_>>();
factors.retain(|factor| !factor.is_constant());
factors
};
// A one-factor admissible image certifies irreducibility. An
// initial primitive image with constant leading coefficient or
// a certified Wang leading-coefficient assignment is ready for
// reconstruction. Other images feed the bounded retries below.
let accepts_first_reconstruction = !bivariate_factors.is_empty()
&& can_accept_first_primitive_image
&& c.get_constant().abs().is_one();
let wang_lcoeffs = (bivariate_factors.len() > 1
&& use_wang_sample
&& c.get_constant().abs().is_one())
.then(|| {
self.wang_lcoeff_precomputation(&bivariate_factors, &cur_sample_points, order)
.ok()
})
.flatten();
let accepts_first_wang_reconstruction = wang_lcoeffs.is_some();
if use_wang_sample
&& bivariate_factors.len() > 1
&& !accepts_first_wang_reconstruction
{
continue;
}
if bivariate_factors.len() == 1
|| accepts_first_reconstruction
|| accepts_first_wang_reconstruction
{
#[cfg(test)]
if bivariate_factors.len() > 1 {
BIVARIATE_FIRST_SAMPLE_ACCEPTANCES.with(|count| count.set(count.get() + 1));
}
if let Some((leftover_lc, sorted_biv_factors, true_lcoeffs)) = wang_lcoeffs {
return (
sorted_biv_factors,
cur_sample_points,
coefficient_upper_bound,
cur_uni_f,
Some((leftover_lc, true_lcoeffs)),
);
}
return (
bivariate_factors,
cur_sample_points,
coefficient_upper_bound,
cur_uni_f,
None,
);
}
if max_factors_num.is_none() {
max_factors_num = Some(bivariate_factors.len());
}
// A valid specialization can split true multivariate factors, so the
// bivariate factor count is an upper bound on the final factor count.
// Keep the smallest admissible count; a larger count is only useful
// when the caller supplied a looser bound from an earlier failure.
if bivariate_factors.len() <= max_factors_num.unwrap() {
if bivariate_factors.len() < max_factors_num.unwrap() {
max_factors_num = Some(bivariate_factors.len());
content_try_count = 0;
best = None;
}
if best.is_none() || c.get_constant().abs() < best.as_ref().unwrap().0 {
best = Some((
c.get_constant().abs(),
bivariate_factors.clone(),
cur_sample_points.clone(),
coefficient_upper_bound,
cur_uni_f.clone(),
));
}
content_try_count += 1;
// try a few times to lower the chance of a costly Hensel lift
// with a wrong number of factors
if content_try_count > 2 {
break;
}
} else {
debug!(
"Number of factors is too large: {} vs {}",
bivariate_factors.len(),
max_factors_num.unwrap_or(bivariate_factors.len())
);
}
}
coefficient_upper_bound += 10;
debug!("Growing bound {}", coefficient_upper_bound);
}
let (_, bivariate_factors, cur_sample_points, coefficient_upper_bound, cur_uni_f) =
best.unwrap();
(
bivariate_factors,
cur_sample_points,
coefficient_upper_bound,
cur_uni_f,
None,
)
}
fn find_univariate_sample(
&self,
order: &mut [usize],
coefficient_upper_bound: i64,
max_factors_num: Option<usize>,
) -> Option<(
Vec<Self>,
Vec<Self>,
Vec<(usize, Integer)>,
i64,
Self,
Integer,
)> {
debug!("Find univariate sample for {} with order {:?}", self, order);
let mut cur_sample_points: Vec<_> =
order[1..].iter().map(|i| (*i, Integer::zero())).collect();
let degree = self.degree(order[0]);
let mut seed = 0x9e37_79b9_7f4a_7c15u64;
for (i, &v) in order.iter().enumerate() {
seed ^= ((v as u64) + 0x517c_c1b7_2722_0a95u64).rotate_left(((i * 13) % 64) as u32);
}
seed ^= (self.nterms() as u64).rotate_left(17);
seed ^= (degree.to_u32() as u64).rotate_left(31);
seed ^= (coefficient_upper_bound as u64).rotate_left(43);
let mut rng = StdRng::seed_from_u64(seed);
let uni_lcoeff = self.univariate_lcoeff(order[0]);
let lcoeff_content = uni_lcoeff.content().abs();
let lcoeff_factorization = uni_lcoeff.factor();
// Store powers for all evaluated variables so that each univariate image can
// be constructed in one pass over the input terms.
let mut power_cache = (0..self.nvars())
.map(|i| {
vec![Integer::zero(); (self.degree(i).to_u32() as usize + 1).min(POW_CACHE_SIZE)]
})
.collect::<Vec<_>>();
const SAMPLE_ATTEMPTS: usize = 512;
const ATTEMPTS_PER_BOUND: usize = 32;
const MAX_SAMPLE_BOUND: i64 = 4096;
let mut sample_bound = coefficient_upper_bound.clamp(10, MAX_SAMPLE_BOUND);
let prime_start = if coefficient_upper_bound <= 10 {
1
} else {
coefficient_upper_bound as u64
};
let mut primes = PrimeIteratorU64::new(prime_start);
for attempt in 0..SAMPLE_ATTEMPTS {
if attempt < WANG_PRIME_SAMPLE_ATTEMPTS {
// Each block uses fresh prime coordinates. Distinct variable factors
// then have pairwise-coprime images, while another block can replace
// an image that acquired content during specialization.
for sample in &mut cur_sample_points {
loop {
let prime = Integer::from(primes.next().unwrap());
if !(&lcoeff_content % &prime).is_zero() {
sample.1 = prime;
break;
}
}
}
} else {
let random_attempt = attempt - WANG_PRIME_SAMPLE_ATTEMPTS;
if random_attempt > 0 && random_attempt % ATTEMPTS_PER_BOUND == 0 {
sample_bound = sample_bound.saturating_mul(2).min(MAX_SAMPLE_BOUND);
}
for sample in &mut cur_sample_points {
sample.1 = Integer::Single(rng.random_range(2..=sample_bound));
}
}
for sample in &cur_sample_points {
debug!("Sample x{} {}", sample.0, sample.1);
}
if !Self::lcoeff_sample_supports_wang_reconstruction(
&lcoeff_factorization,
&lcoeff_content,
&cur_sample_points,
) {
continue;
}
for powers in &mut power_cache {
powers.fill(Integer::zero());
}
let cur_uni_f = self.replace_except(order[0], &cur_sample_points, &mut power_cache);
if degree != cur_uni_f.degree(order[0]) {
continue;
}
let univariate_content = cur_uni_f.univariate_content(order[0]);
if !univariate_content.is_constant() {
continue;
}
let univariate_content = univariate_content.get_constant();
// Primitiveization removes this content from the leading coefficient.
// If it contains the image of a variable factor, Wang reconstruction
// cannot assign that factor's full multiplicity to the primitive image.
if !Self::lcoeff_sample_supports_wang_reconstruction(
&lcoeff_factorization,
&univariate_content.abs(),
&cur_sample_points,
) {
continue;
}
let primitive_uni_f = cur_uni_f.clone().div_coeff(&univariate_content);
let uni_df = primitive_uni_f.derivative(order[0]);
if !primitive_uni_f.gcd(&uni_df).is_constant() {
continue;
}
// The image is primitive and has just been checked to be square-free,
// so factor it directly without repeating content and square-free GCDs.
let factor_target = if primitive_uni_f.lcoeff().is_negative() {
-primitive_uni_f.clone()
} else {
primitive_uni_f.clone()
};
let mut univariate_factors = factor_target.factor_reconstruct();
if univariate_factors.is_empty() {
continue;
}
let mut product = self.one();
for f in &univariate_factors {
product = &product * f;
}
if product != primitive_uni_f {
if -product == primitive_uni_f {
univariate_factors[0] =
univariate_factors[0].clone().mul_coeff(Integer::from(-1));
} else {
continue;
}
}
let Some(true_lcoeffs) = self.reconstruct_lcoeffs_from_univariate_sample(
&uni_lcoeff,
&lcoeff_factorization,
&univariate_factors,
&cur_sample_points,
&univariate_content,
) else {
continue;
};
if max_factors_num.is_none_or(|max| univariate_factors.len() <= max) {
return Some((
univariate_factors,
true_lcoeffs,
cur_sample_points,
coefficient_upper_bound.max(sample_bound),
cur_uni_f,
univariate_content,
));
}
}
None
}
fn impose_true_lcoeffs_on_integer_factors(
&self,
factors: &[Self],
true_lcoeffs: &[Self],
order: &[usize],
) -> Vec<Self> {
let mut factors_with_true_lcoeff = Vec::with_capacity(factors.len());
for (factor, true_lcoeff) in factors.iter().zip(true_lcoeffs) {
let mut coefficients = factor.to_univariate_polynomial_list(order[0]);
coefficients.last_mut().unwrap().0 = true_lcoeff.clone();
let mut fixed_factor = self.zero();
let mut exp = vec![E::zero(); self.nvars()];
for (coefficient, degree) in coefficients {
exp[order[0]] = degree;
fixed_factor = fixed_factor + coefficient.mul_exp(&exp);
}
factors_with_true_lcoeff.push(fixed_factor);
}
factors_with_true_lcoeff
}
/// Lift modular factor coefficients until their exact product equals the target.
///
/// A factorization that is already exact is returned immediately. `coefficient_bound` is
/// evaluated only when another p-adic coefficient-lifting step is required.
fn sparse_coefficient_hensel_lift_mod_prime<B>(
&self,
mut factorization: Vec<Self>,
true_lcoeffs: &[Self],
p: u32,
order: &[usize],
coefficient_bound: B,
) -> Option<Vec<Self>>
where
B: FnOnce() -> Integer,
{
factorization =
self.impose_true_lcoeffs_on_integer_factors(&factorization, true_lcoeffs, order);
let mut product = self.one();
for f in &factorization {
product = &product * f;
}
let mut error = self - &product;
if error.is_zero() {
return Some(factorization);
}
let field = Zp::new(p);
let p_int: Integer = (p as u64).into();
let factors_mod_p: Vec<_> = factorization
.iter()
.map(|f| f.map_coeff(|c| c.to_finite_field(&field), field.clone()))
.collect();
let skeletons: Vec<_> = factors_mod_p
.iter()
.map(|f| f.mod_var(order[0], f.degree(order[0])))
.collect();
let prods_mod_p: Vec<_> = (0..factors_mod_p.len())
.map(|i| {
let mut prod = factors_mod_p[i].one();
for (j, f) in factors_mod_p.iter().enumerate() {
if i != j {
prod = prod * f;
}
}
prod
})
.collect();
let bound = coefficient_bound();
let mut max_p = p_int.clone();
while &max_p * 2 < bound {
max_p = &max_p * &p_int;
}
let mut m = p_int.clone();
let mut sparse_diophantine_context = SparseDiophantineContext::new();
while !error.is_zero() && &m <= &max_p {
let mut error_mod_p = factors_mod_p[0].zero();
for term in &error {
if !(term.coefficient % &m).is_zero() {
return None;
}
let q = term.coefficient / &m;
error_mod_p.append_monomial(q.to_finite_field(&field), term.exponents);
}
let deltas = MultivariatePolynomial::sparse_multivariate_diophantine_from_skeleton(
&factors_mod_p,
&prods_mod_p,
&error_mod_p,
&skeletons,
order,
&mut sparse_diophantine_context,
)?;
for (factor, delta) in factorization.iter_mut().zip(deltas) {
*factor = &*factor
+ &delta
.map_coeff(|c| field.to_symmetric_integer(c), Z)
.mul_coeff(m.clone());
}
factorization =
self.impose_true_lcoeffs_on_integer_factors(&factorization, true_lcoeffs, order);
product = self.one();
for f in &factorization {
product = &product * f;
}
error = self - &product;
m = &m * &p_int;
}
Some(factorization)
}
/// Factor a square-free integer polynomial by factoring a bivariate
/// specialization, lifting those factors through the remaining variables,
/// and accepting only an exact reconstruction of the input. Failed samples
/// advance the deterministic coefficient bound and retry without growing
/// the call stack.
fn multivariate_factorization_bivariate_start(
&self,
order: &mut [usize],
mut coefficient_upper_bound: i64,
mut max_bivariate_factors: Option<usize>,
univariate_fallback_state: Option<UnivariateFactorFallbackState>,
) -> Vec<Self> {
let mut retry_context = BivariateFactorRetryContext::new(univariate_fallback_state);
'retry: loop {
if let Some(m) = max_bivariate_factors
&& m == 1
{
return vec![self.clone()];
}
let (bivariate_factors, sample_points, uni_f, wang_lcoeffs) = loop {
let (bivariate_factors, sample_points, coeff_b, uni_f, wang_lcoeffs) = self
.find_sample(
order,
coefficient_upper_bound.max(10),
max_bivariate_factors,
);
coefficient_upper_bound = coeff_b;
if bivariate_factors.len() == 1 {
return vec![self.clone()];
}
if let Some(max) = max_bivariate_factors {
if bivariate_factors.len() < max {
debug!(
"Updating bivariate factor bound to {}",
bivariate_factors.len()
);
max_bivariate_factors = Some(bivariate_factors.len());
}
} else {
debug!(
"Updating bivariate factor bound to {}",
bivariate_factors.len()
);
max_bivariate_factors = Some(bivariate_factors.len());
}
break (bivariate_factors, sample_points, uni_f, wang_lcoeffs);
};
let mut prime_iter = PrimeIteratorU64::new(1 << 31);
let mut field;
let mut p;
'new_prime: loop {
p = prime_iter
.next()
.expect("Ran out of primes during factorization");
if p > u32::MAX as u64 {
panic!("Ran out of primes during factorization of {self}");
}
if (&uni_f.lcoeff() % &p.into()).is_zero() {
continue;
}
field = Zp::new(p as u32);
let fs_p: Vec<_> = bivariate_factors
.iter()
.map(|f| f.map_coeff(|c| c.to_finite_field(&field), field.clone()))
.collect();
for (f_p, f_z) in fs_p.iter().zip(&bivariate_factors) {
if f_p.degree(order[0]) != f_z.degree(order[0])
|| f_p.degree(order[1]) != f_z.degree(order[1])
|| f_p.ring().try_inv(&f_p.lcoeff()).is_none()
{
continue 'new_prime;
}
}
for (j, f) in fs_p.iter().enumerate() {
for g in &fs_p[j + 1..] {
if !f.gcd(g).is_one() {
continue 'new_prime;
}
}
}
break;
}
let p32 = p as u32;
let wang_sample = wang_lcoeffs.is_some();
let lcoeff_precomputation = if let Some((leftover_lc, true_lcoeffs)) = wang_lcoeffs {
Ok((leftover_lc, bivariate_factors, true_lcoeffs))
} else {
let p_int = field.get_prime().to_integer();
let mut lcoeff_max_p = p_int.clone();
let mut k = 1;
let lcoeff_bound = self.coefficient_bound();
while &lcoeff_max_p * 2 < lcoeff_bound {
lcoeff_max_p = &lcoeff_max_p * &p_int;
k += 1;
}
self.lcoeff_precomputation(
&bivariate_factors,
&sample_points,
order,
lcoeff_max_p,
p32,
k,
)
};
let (leftover_lc, mut sorted_biv_factors, mut true_lcoeffs) =
match lcoeff_precomputation {
Ok((leftover_lc, sorted_biv_factors, true_lcoeffs)) => {
(leftover_lc, sorted_biv_factors, true_lcoeffs)
}
Err(max_biv) => {
max_bivariate_factors = Some(max_biv);
let univariate_state = retry_context.advance(&mut coefficient_upper_bound);
if max_biv != 1
&& let Some(univariate_state) = univariate_state
{
order.copy_from_slice(&univariate_state.order);
return self.multivariate_factorization_with_retries(
order,
univariate_state.coefficient_upper_bound,
max_bivariate_factors,
0,
None,
false,
IntegerFactorStart::Univariate,
);
}
continue 'retry;
}
};
let rescaled = if leftover_lc == -1 {
sorted_biv_factors[0] = -sorted_biv_factors[0].clone();
true_lcoeffs[0] = -true_lcoeffs[0].clone();
Cow::Borrowed(self)
} else if leftover_lc != 1 {
for (b, l) in sorted_biv_factors.iter_mut().zip(&mut true_lcoeffs) {
*b = b.clone().mul_coeff(leftover_lc.clone());
*l = l.clone().mul_coeff(leftover_lc.clone());
}
Cow::Owned(
self.clone()
.mul_coeff(leftover_lc.pow(sorted_biv_factors.len() as u64 - 1)),
)
} else {
Cow::Borrowed(self)
};
for (b, l) in sorted_biv_factors.iter().zip(&true_lcoeffs) {
debug!("Bivariate factor {} with true lcoeff {}", b, l);
}
let poly_p = rescaled.map_coeff(|c| c.to_finite_field(&field), field.clone());
let sample_points_p: Vec<_> = sample_points
.iter()
.map(|(v, p)| (*v, p.to_finite_field(&field)))
.collect();
let true_lcoeffs_p: Vec<_> = true_lcoeffs
.iter()
.map(|f| f.map_coeff(|c| c.to_finite_field(&field), field.clone()))
.collect();
let sorted_biv_factors_p: Vec<_> = sorted_biv_factors
.iter()
.map(|f| f.map_coeff(|c| c.to_finite_field(&field), field.clone()))
.collect();
let sorted_biv_factors_p = poly_p.impose_true_lcoeffs_on_factors(
&sorted_biv_factors_p,
&true_lcoeffs_p,
&sample_points_p,
order,
2,
);
let (mut uni, delta) = MultivariatePolynomial::univariate_diophantine_field(
&sorted_biv_factors_p,
order,
&sample_points_p,
);
let hensel_context = if wang_sample {
MultivariateHenselContext::new(2).retry_sample_on_sparse_failure()
} else {
MultivariateHenselContext::new(2)
};
let Ok(factorization_p) = poly_p.multivariate_hensel_lifting(
&sorted_biv_factors_p,
&mut uni,
&delta,
&sample_points_p,
Some(&true_lcoeffs_p),
order,
hensel_context,
) else {
if let Some(univariate_state) = retry_context.advance(&mut coefficient_upper_bound)
{
order.copy_from_slice(&univariate_state.order);
return self.multivariate_factorization_with_retries(
order,
univariate_state.coefficient_upper_bound,
max_bivariate_factors,
0,
None,
false,
IntegerFactorStart::Univariate,
);
}
continue 'retry;
};
let factorization_z: Vec<_> = factorization_p
.into_iter()
.map(|f| f.map_coeff(|c| field.to_symmetric_integer(c), Z))
.collect();
let Some(factorization_z) = rescaled.sparse_coefficient_hensel_lift_mod_prime(
factorization_z,
&true_lcoeffs,
p32,
order,
|| rescaled.coefficient_bound(),
) else {
if let Some(univariate_state) = retry_context.advance(&mut coefficient_upper_bound)
{
order.copy_from_slice(&univariate_state.order);
return self.multivariate_factorization_with_retries(
order,
univariate_state.coefficient_upper_bound,
max_bivariate_factors,
0,
None,
false,
IntegerFactorStart::Univariate,
);
}
continue 'retry;
};
let mut factorization: Vec<MultivariatePolynomial<IntegerRing, E>> = factorization_z
.into_iter()
.map(|f| f.make_primitive())
.collect();
let mut test = self.one();
for f in &factorization {
debug!("Factor = {}", f);
test = &test * f;
}
if self.lcoeff().is_negative() != test.lcoeff().is_negative() {
test = -test;
if let Some(neg_coeff) = factorization.iter_mut().find(|f| f.lcoeff().is_negative())
{
*neg_coeff = -neg_coeff.clone();
} else {
factorization[0] = factorization[0].clone().mul_coeff((-1).into());
}
}
if &test == self {
let mut negated_factors = 0usize;
for f in &mut factorization {
if f.lcoeff().is_negative() {
*f = -f.clone();
negated_factors += 1;
}
}
if negated_factors % 2 == 1 {
factorization[0] = -factorization[0].clone();
}
return factorization;
} else {
if let Some(univariate_state) = retry_context.advance(&mut coefficient_upper_bound)
{
order.copy_from_slice(&univariate_state.order);
return self.multivariate_factorization_with_retries(
order,
univariate_state.coefficient_upper_bound,
max_bivariate_factors,
0,
None,
false,
IntegerFactorStart::Univariate,
);
}
continue 'retry;
}
}
}
/// Perform multivariate factorization on a square-free polynomial.
fn multivariate_factorization(
&self,
order: &mut [usize],
coefficient_upper_bound: i64,
max_univariate_factors: Option<usize>,
bivariate_fallback_order: Option<&[usize]>,
prefer_univariate_start: bool,
) -> Vec<Self> {
self.multivariate_factorization_with_retries(
order,
coefficient_upper_bound,
max_univariate_factors,
0,
bivariate_fallback_order,
prefer_univariate_start,
integer_factor_start_mode(),
)
}
/// Start bivariate factorization after restoring the variable order that
/// preceded a speculative sparse-univariate ordering.
fn multivariate_factorization_bivariate_fallback(
&self,
order: &mut [usize],
bivariate_fallback_order: Option<&[usize]>,
coefficient_upper_bound: i64,
max_bivariate_factors: Option<usize>,
univariate_fallback_state: Option<UnivariateFactorFallbackState>,
) -> Vec<Self> {
if let Some(fallback_order) = bivariate_fallback_order {
debug_assert_eq!(order.len(), fallback_order.len());
order.copy_from_slice(fallback_order);
}
self.multivariate_factorization_bivariate_start(
order,
coefficient_upper_bound,
max_bivariate_factors,
univariate_fallback_state,
)
}
fn multivariate_factorization_with_retries(
&self,
order: &mut [usize],
mut coefficient_upper_bound: i64,
mut max_univariate_factors: Option<usize>,
mut univariate_retries: usize,
bivariate_fallback_order: Option<&[usize]>,
prefer_univariate_start: bool,
integer_start_mode: IntegerFactorStart,
) -> Vec<Self> {
'retry: loop {
if let Some(m) = max_univariate_factors
&& m == 1
{
return vec![self.clone()];
}
let use_bivariate_start = match integer_start_mode {
IntegerFactorStart::Auto => {
if univariate_retries >= INTEGER_FACTOR_UNIVARIATE_AUTO_RETRIES {
return self.multivariate_factorization_bivariate_fallback(
order,
bivariate_fallback_order,
coefficient_upper_bound,
max_univariate_factors,
None,
);
}
if prefer_univariate_start {
false
} else {
self.integer_factor_start_auto_decision(order).0
}
}
IntegerFactorStart::Univariate => false,
IntegerFactorStart::Bivariate => true,
IntegerFactorStart::Disabled => return vec![self.clone()],
};
if use_bivariate_start {
let univariate_fallback_state = (integer_start_mode == IntegerFactorStart::Auto
&& univariate_retries == 0)
.then(|| UnivariateFactorFallbackState {
order: order.to_vec(),
coefficient_upper_bound,
});
return self.multivariate_factorization_bivariate_fallback(
order,
bivariate_fallback_order,
coefficient_upper_bound,
max_univariate_factors,
univariate_fallback_state,
);
}
let (univariate_factors, lc_divs, sample_points, uni_f, univariate_content) = loop {
let Some((
univariate_factors,
lc_divs,
sample_points,
coeff_b,
uni_f,
univariate_content,
)) = self.find_univariate_sample(
order,
coefficient_upper_bound.max(10),
max_univariate_factors,
)
else {
if let Some(max) = max_univariate_factors {
debug!(
"No univariate image with at most {max} factors; relaxing the stale factor bound"
);
max_univariate_factors = None;
coefficient_upper_bound = coefficient_upper_bound.saturating_add(10);
continue;
}
match integer_start_mode {
IntegerFactorStart::Auto | IntegerFactorStart::Bivariate => {
return self.multivariate_factorization_bivariate_fallback(
order,
bivariate_fallback_order,
coefficient_upper_bound.saturating_add(10),
None,
None,
);
}
IntegerFactorStart::Univariate => {
// Start a new deterministic sampling block. Including this
// monotonically growing cursor in the RNG seed prevents a
// failed block from repeating the same candidate images.
coefficient_upper_bound = coefficient_upper_bound.saturating_add(10);
continue;
}
IntegerFactorStart::Disabled => return vec![self.clone()],
}
};
coefficient_upper_bound = coeff_b;
if univariate_factors.len() == 1 {
// the polynomial is irreducible
return vec![self.clone()];
}
if let Some(max) = max_univariate_factors {
if univariate_factors.len() < max {
debug!(
"Updating univariate factor bound to {}",
univariate_factors.len()
);
max_univariate_factors = Some(univariate_factors.len());
}
} else {
debug!(
"Updating univariate factor bound to {}",
univariate_factors.len()
);
max_univariate_factors = Some(univariate_factors.len());
}
break (
univariate_factors,
lc_divs,
sample_points,
uni_f,
univariate_content,
);
};
let univariate_factor_count = univariate_factors.len();
// select a suitable prime
let mut prime_iter = PrimeIteratorU64::new(1 << 31);
let mut field;
let mut p;
'new_prime: loop {
p = prime_iter
.next()
.expect("Ran out of primes during factorization");
if p > u32::MAX as u64 {
panic!("Ran out of primes during factorization of {self}");
}
if (&uni_f.lcoeff() % &p.into()).is_zero() {
continue;
}
field = Zp::new(p as u32);
// make sure the univariate factors stay square-free and coprime modulo p
let fs_p: Vec<_> = univariate_factors
.iter()
.map(|f| f.map_coeff(|c| c.to_finite_field(&field), field.clone()))
.collect();
for (f_p, f_z) in fs_p.iter().zip(&univariate_factors) {
if f_p.degree(order[0]) != f_z.degree(order[0])
|| f_p.ring().try_inv(&f_p.lcoeff()).is_none()
{
continue 'new_prime;
}
}
for (j, f) in fs_p.iter().enumerate() {
for g in &fs_p[j + 1..] {
if !f.gcd(g).is_one() {
continue 'new_prime;
}
}
}
break;
}
let p32 = p as u32;
let scaled_true_lcoeffs: Vec<_> = lc_divs
.iter()
.map(|l| l.clone().mul_coeff(univariate_content.clone()))
.collect();
let scaled_univariate_factors: Vec<_> = univariate_factors
.iter()
.map(|f| f.clone().mul_coeff(univariate_content.clone()))
.collect();
let mut scale_pow = Integer::one();
for _ in 1..univariate_factor_count {
scale_pow *= &univariate_content;
}
let scaled_self = self.clone().mul_coeff(scale_pow);
for (u, l) in univariate_factors.iter().zip(&lc_divs) {
debug!("Univariate factor {} with true lcoeff {}", u, l);
}
let poly_p = scaled_self.map_coeff(|c| c.to_finite_field(&field), field.clone());
let sample_points_p: Vec<_> = sample_points
.iter()
.map(|(v, p)| (*v, p.to_finite_field(&field)))
.collect();
let true_lcoeffs_p: Vec<_> = scaled_true_lcoeffs
.iter()
.map(|f| f.map_coeff(|c| c.to_finite_field(&field), field.clone()))
.collect();
let univariate_factors_p: Vec<_> = scaled_univariate_factors
.iter()
.map(|f| f.map_coeff(|c| c.to_finite_field(&field), field.clone()))
.collect();
let mut uni = univariate_factors_p.clone();
let Some(delta) = MultivariatePolynomial::try_univariate_diophantine(
&mut uni,
&univariate_factors_p[0].constant(field.one()),
) else {
coefficient_upper_bound += 10;
univariate_retries += 1;
continue 'retry;
};
let hensel_context = if integer_start_mode == IntegerFactorStart::Auto {
MultivariateHenselContext::new(1).retry_sample_on_sparse_failure()
} else {
MultivariateHenselContext::new(1)
};
let Ok(factorization_p) = poly_p.multivariate_hensel_lifting(
&univariate_factors_p,
&mut uni,
&delta,
&sample_points_p,
Some(&true_lcoeffs_p),
order,
hensel_context,
) else {
coefficient_upper_bound += 10;
univariate_retries += 1;
continue 'retry;
};
let factorization_z: Vec<_> = factorization_p
.into_iter()
.map(|f| f.map_coeff(|c| field.to_symmetric_integer(c), Z))
.collect();
let Some(factorization_z) = scaled_self.sparse_coefficient_hensel_lift_mod_prime(
factorization_z,
&scaled_true_lcoeffs,
p32,
order,
|| scaled_self.coefficient_bound(),
) else {
coefficient_upper_bound += 10;
univariate_retries += 1;
continue 'retry;
};
let mut factorization: Vec<MultivariatePolynomial<IntegerRing, E>> = factorization_z
.into_iter()
.map(|f| f.make_primitive())
.collect();
// test the factorization
let mut test = self.one();
for f in &factorization {
debug!("Factor = {}", f);
test = &test * f;
}
if self.lcoeff().is_negative() != test.lcoeff().is_negative() {
test = -test;
if let Some(neg_coeff) = factorization.iter_mut().find(|f| f.lcoeff().is_negative())
{
*neg_coeff = -neg_coeff.clone();
} else {
factorization[0] = factorization[0].clone().mul_coeff((-1).into());
}
}
if &test == self {
let mut negated_factors = 0usize;
for f in &mut factorization {
if f.lcoeff().is_negative() {
*f = -f.clone();
negated_factors += 1;
}
}
if negated_factors % 2 == 1 {
factorization[0] = -factorization[0].clone();
}
return factorization;
} else {
debug!(
"No immediate factorization of {} for sample points {:?}, retrying with factor bound {:?}",
self, sample_points, max_univariate_factors
);
coefficient_upper_bound += 10;
univariate_retries += 1;
continue 'retry;
}
}
}
}
impl<E: PositiveExponent> MultivariatePolynomial<FiniteField<Integer>, E, LexOrder> {
/// Compute a univariate diophantine equation in `Z_p^k` by Newton iteration.
fn get_univariate_factors_and_deltas(
factors: &[Self],
order: &[usize],
sample_points: &[(usize, Integer)],
p: u32,
k: usize,
) -> (Vec<Self>, Vec<Self>) {
// produce univariate factors and univariate delta
let mut univariate_factors = factors.to_vec();
for f in &mut univariate_factors {
for (v, s) in sample_points {
if order[0] != *v {
*f = f.replace(*v, &f.ring().nth(s.clone()));
}
}
}
let univariate_deltas = MultivariatePolynomial::lift_diophantine_univariate(
&mut univariate_factors,
&factors[0].constant(factors[0].ring().one()),
p,
k,
);
(univariate_factors, univariate_deltas)
}
}
#[cfg(test)]
mod test {
use std::sync::{Arc, Mutex, atomic::Ordering};
use super::{
BIVARIATE_FIRST_SAMPLE_ACCEPTANCES, BIVARIATE_SAMPLE_FACTORIZATIONS,
BIVARIATE_WANG_SAMPLE_TRIES, BIVARIATE_WIDE_PRIME_ATTEMPTS,
BIVARIATE_WIDE_PRIME_MIN_BOUND_BITS, BIVARIATE_WIDE_PRIME_START, BOUNDED_DDF_REJECTIONS,
BivariateFactorRetryContext, COMPETITIVE_SMALL_PRIME_DDF_REJECTIONS,
DENSE_INTEGER_I128_MULTIPLY_REMAINDERS, DENSE_ZP_DDF_MODULUS_UPDATES, DENSE_ZP_DDF_SCREENS,
DENSE_ZP_EDF_BLOCKS, DENSE_ZP_FROBENIUS_MAP_BUILDS, DenseBivariateImage,
DenseIntegerModularUnivariateContext, DenseTwoFactorCorrectionContext,
DenseZpAccumulationMode, DenseZpDistinctDegreeContext, DenseZpEqualDegreeContext,
DenseZpFrobeniusContext, DenseZpMulModWorkspace, EARLY_SEPARABLE_PAIR_PROBE_CALLS,
EARLY_SEPARABLE_PAIR_SPLITS, EXACT_HENSEL_SUBTREE_MODULUS_BITS,
EXACT_HENSEL_SUBTREE_SPLITS, EarlyFactorModularProbeContext,
EarlyQuadraticDiscriminantProbe, ExactPolynomialSquareRoot,
GEOMETRIC_SMALL_PRIME_BACKFILLS, INTEGER_FACTOR_BIVARIATE_WANG_MIN_BOX_DENSITY,
IntegerModularUnivariateContext, LAST_BIVARIATE_RECONSTRUCTION_PRIME,
LAST_BOUNDED_DDF_REJECTION_DEGREE, LAST_MODULAR_INTEGER_EDF_PRIME,
LINEAR_VARIABLE_IRREDUCIBILITY_CERTIFICATES, LLL_RECOMBINATION_SUCCESSES,
LOCAL_HENSEL_RECOMBINATION_NODES, MIN_EARLY_QUADRATIC_FACTOR_TERMS,
MODULAR_INTEGER_EDF_CALLS, ModularPrimeScreen, PRODUCT_TREE_BALANCED_PAIR_ATTEMPTS,
PRODUCT_TREE_BALANCED_PAIR_CERTIFICATES, PRODUCT_TREE_BALANCED_PAIR_TARGET_EXPONENT,
PRODUCT_TREE_EARLY_RECONSTRUCTION_ATTEMPTS, PRODUCT_TREE_EARLY_RECONSTRUCTION_EXPONENT,
PRODUCT_TREE_EARLY_RECONSTRUCTION_SUCCESSES, PRODUCT_TREE_HENSEL_LIFT_CALLS,
PRODUCT_TREE_LAST_BEZOUT_UPDATE_EXPONENT, PackedSparsePolynomialSquareContext,
QUADRATIC_HENSEL_LIFT_CALLS, QUADRATIC_HENSEL_NONUNIT_RETRIES, QuadraticFactorization,
SEPARABLE_CONTENT_NONTRIVIAL_MONOMIAL_FALLBACKS,
SEPARABLE_CONTENT_PAIR_MONOMIAL_CERTIFICATES, SEPARABLE_CONTENT_PAIR_PROBES,
SEPARABLE_CONTENT_PAIR_REPLACEMENTS, SEPARABLE_CONTENT_SINGLE_MONOMIAL_CERTIFICATES,
SeparableCoefficientContentContext, SparseDiophantineContext,
SparsePolynomialSquareRootContext, UNIVARIATE_IRREDUCIBILITY_CERTIFICATES,
UNIVARIATE_IRREDUCIBILITY_SCOUTS, UnivariateFactorFallbackState,
UnivariateHenselProductTreeBuildContext, UnivariateHenselProductTreeLiftContext,
UnivariateHenselProductTreeLiftResult, UnivariateHenselProductTreeLink,
UnivariateHenselProductTreeNode, UnivariateSpecializationFactorization,
balance_three_factor_hensel_root, integer_factor_bivariate_wang_density_supported,
reorder_integer_factor_variables_for_sparse_univariate,
univariate_hensel_precision_schedule, univariate_hensel_shortened_target,
};
use crate::{
GLOBAL_SETTINGS,
atom::AtomCore,
domains::{
InternalOrdering, Ring, RingOps,
algebraic::AlgebraicExtension,
finite_field::{
FiniteField, FiniteFieldCore, FiniteFieldElement, PrimeIteratorU64, ToFiniteField,
Z2, Zp,
},
integer::{Integer, IntegerRing, Z},
rational::Q,
},
parse,
poly::{MultivariatePolynomial, PositiveExponent, factor::Factorize},
symbol,
};
static GLOBAL_FACTOR_SETTINGS_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn separable_content_single_monomial_layer_certifies_constant_content() {
let variables = Some(Arc::new(vec![symbol!("x").into(), symbol!("y").into()]));
let polynomial = parse!("y^2+x*(1+y)")
.expand()
.to_polynomial::<_, u16>(&Z, variables);
let context = SeparableCoefficientContentContext::new(&polynomial);
SEPARABLE_CONTENT_SINGLE_MONOMIAL_CERTIFICATES.with(|count| count.set(0));
let layers = polynomial
.to_univariate_polynomial_list(0)
.into_iter()
.map(|(coefficient, _)| coefficient)
.collect();
assert!(context.nonconstant_content(layers).is_none());
SEPARABLE_CONTENT_SINGLE_MONOMIAL_CERTIFICATES.with(|count| assert_eq!(count.get(), 1));
}
#[test]
fn separable_content_pair_probe_uses_the_one_eighth_boundary() {
let variables = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("y").into(),
symbol!("z").into(),
]));
let at_boundary = parse!(
"y*(z+1)+x*y*(z+2)+x^2*(1+z+z^2+z^3+z^4+z^5+z^6+z^7+z^8+z^9+z^10+z^11+z^12+z^13)"
)
.expand()
.to_polynomial::<_, u16>(&Z, variables.clone());
let context = SeparableCoefficientContentContext::new(&at_boundary);
SEPARABLE_CONTENT_PAIR_PROBES.with(|count| count.set(0));
SEPARABLE_CONTENT_PAIR_MONOMIAL_CERTIFICATES.with(|count| count.set(0));
let layers = at_boundary
.to_univariate_polynomial_list(0)
.into_iter()
.map(|(coefficient, _)| coefficient)
.collect();
assert!(context.nonconstant_content(layers).is_none());
SEPARABLE_CONTENT_PAIR_PROBES.with(|count| assert_eq!(count.get(), 1));
SEPARABLE_CONTENT_PAIR_MONOMIAL_CERTIFICATES.with(|count| assert_eq!(count.get(), 1));
let below_boundary =
parse!("y*(z+1)+x*y*(z+2)+x^2*(1+z+z^2+z^3+z^4+z^5+z^6+z^7+z^8+z^9+z^10+z^11+z^12)")
.expand()
.to_polynomial::<_, u16>(&Z, variables);
let context = SeparableCoefficientContentContext::new(&below_boundary);
SEPARABLE_CONTENT_PAIR_PROBES.with(|count| count.set(0));
let layers = below_boundary
.to_univariate_polynomial_list(0)
.into_iter()
.map(|(coefficient, _)| coefficient)
.collect();
assert!(context.nonconstant_content(layers).is_none());
SEPARABLE_CONTENT_PAIR_PROBES.with(|count| assert_eq!(count.get(), 0));
}
#[test]
fn separable_content_nontrivial_common_monomial_uses_full_fallback() {
let variables = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("y").into(),
symbol!("z").into(),
]));
let polynomial = parse!(
"z*(y*(z+1)+x*y*(z+2)+x^2*(1+z+z^2+z^3+z^4+z^5+z^6+z^7+z^8+z^9+z^10+z^11+z^12+z^13))"
)
.expand()
.to_polynomial::<_, u16>(&Z, variables.clone());
let expected = parse!("z").to_polynomial::<_, u16>(&Z, variables);
let context = SeparableCoefficientContentContext::new(&polynomial);
SEPARABLE_CONTENT_NONTRIVIAL_MONOMIAL_FALLBACKS.with(|count| count.set(0));
SEPARABLE_CONTENT_PAIR_REPLACEMENTS.with(|count| count.set(0));
let layers = polynomial
.to_univariate_polynomial_list(0)
.into_iter()
.map(|(coefficient, _)| coefficient)
.collect();
let content = context.nonconstant_content(layers).unwrap();
assert_eq!(content, expected);
SEPARABLE_CONTENT_NONTRIVIAL_MONOMIAL_FALLBACKS.with(|count| assert_eq!(count.get(), 1));
SEPARABLE_CONTENT_PAIR_REPLACEMENTS.with(|count| assert_eq!(count.get(), 1));
}
#[test]
fn separable_content_pair_replacement_preserves_nonconstant_content() {
let variables = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("y").into(),
symbol!("z").into(),
]));
let polynomial = parse!(
"(y+1)*((z+1)+x*(z+2)+x^2*(1+z+z^2+z^3+z^4+z^5+z^6+z^7+z^8+z^9+z^10+z^11+z^12+z^13))"
)
.expand()
.to_polynomial::<_, u16>(&Z, variables.clone());
let expected = parse!("y+1").to_polynomial::<_, u16>(&Z, variables);
let context = SeparableCoefficientContentContext::new(&polynomial);
SEPARABLE_CONTENT_PAIR_REPLACEMENTS.with(|count| count.set(0));
let layers = polynomial
.to_univariate_polynomial_list(0)
.into_iter()
.map(|(coefficient, _)| coefficient)
.collect();
let content = context.nonconstant_content(layers).unwrap();
assert_eq!(content, expected);
SEPARABLE_CONTENT_PAIR_REPLACEMENTS.with(|count| assert_eq!(count.get(), 1));
}
#[test]
fn univariate_hensel_product_tree_is_degree_greedy_and_deterministic() {
use UnivariateHenselProductTreeLink::{Internal, Leaf};
let mut context = UnivariateHenselProductTreeBuildContext::default();
let topology = context.build(&[1, 1, 2, 10, 10, 10, 30]);
assert_eq!(
topology.nodes,
[
UnivariateHenselProductTreeNode {
children: [Leaf(0), Leaf(1)],
degree: 2,
},
UnivariateHenselProductTreeNode {
children: [Leaf(2), Internal(0)],
degree: 4,
},
UnivariateHenselProductTreeNode {
children: [Internal(1), Leaf(3)],
degree: 14,
},
UnivariateHenselProductTreeNode {
children: [Leaf(4), Leaf(5)],
degree: 20,
},
UnivariateHenselProductTreeNode {
children: [Internal(2), Internal(3)],
degree: 34,
},
UnivariateHenselProductTreeNode {
children: [Leaf(6), Internal(4)],
degree: 64,
},
]
);
assert_eq!(
topology
.nodes
.iter()
.map(|node| node.degree)
.collect::<Vec<_>>(),
[2, 4, 14, 20, 34, 64]
);
assert_eq!(
topology.nodes.iter().map(|node| node.degree).sum::<usize>(),
138
);
assert_eq!(topology.root, Some(Internal(5)));
assert_eq!(topology.degree(Internal(5)), 64);
assert_eq!(
topology
.internal_nodes_top_down()
.map(|(_, node)| node.degree)
.collect::<Vec<_>>(),
[64, 34, 20, 14, 4, 2]
);
for (parent_index, node) in topology.internal_nodes_top_down() {
for child in node.children {
if let Internal(child_index) = child {
assert!(child_index < parent_index);
}
}
}
assert_eq!(
topology.leaf_indices_in_input_order(),
[0, 1, 2, 3, 4, 5, 6]
);
assert_eq!(context.build(&[1, 1, 2, 10, 10, 10, 30]), topology);
}
#[test]
fn univariate_hensel_product_tree_handles_empty_and_singleton_inputs() {
let mut context = UnivariateHenselProductTreeBuildContext::default();
let empty = context.build(&[]);
assert!(empty.nodes.is_empty());
assert_eq!(empty.root, None);
assert!(empty.leaf_indices_in_input_order().is_empty());
let singleton = context.build(&[17]);
assert!(singleton.nodes.is_empty());
assert_eq!(
singleton.root,
Some(UnivariateHenselProductTreeLink::Leaf(0))
);
assert_eq!(
singleton.degree(UnivariateHenselProductTreeLink::Leaf(0)),
17
);
assert_eq!(singleton.leaf_indices_in_input_order(), [0]);
}
#[test]
fn univariate_hensel_balanced_leaf_pair_is_deterministic() {
let mut context = UnivariateHenselProductTreeBuildContext::default();
let degree_64 = context.build(&[1, 1, 10, 10, 10, 16, 16]);
assert_eq!(degree_64.most_balanced_leaf_pair(), Some([5, 6]));
assert_eq!(degree_64.balanced_leaf_pair_improving_root(), Some([5, 6]));
// This independent shape has a unique closest-to-half pair despite
// having neither equal leaf degrees nor an even total degree.
let independent = context.build(&[2, 3, 5, 7, 11, 13]);
assert_eq!(independent.most_balanced_leaf_pair(), Some([3, 5]));
assert_eq!(
independent.balanced_leaf_pair_improving_root(),
Some([3, 5])
);
// Both degree sums 12 and 13 have imbalance one. Input indices make
// the degree-12 pair the reproducible choice.
let tied = context.build(&[1, 2, 4, 5, 6, 7]);
assert_eq!(tied.most_balanced_leaf_pair(), Some([3, 5]));
}
#[test]
fn three_factor_hensel_root_is_degree_balanced() {
let variables = Some(Arc::new(vec![symbol!("x").into()]));
let field = Zp::new(5);
let mut factors = [
parse!("x^8+x+1").to_polynomial::<_, u8>(&field, variables.clone()),
parse!("x^8+2*x+1").to_polynomial::<_, u8>(&field, variables.clone()),
parse!("x^16+x+1").to_polynomial::<_, u8>(&field, variables),
];
balance_three_factor_hensel_root(&mut factors, 0);
assert_eq!(factors.map(|factor| factor.degree(0).to_u32()), [16, 8, 8]);
}
#[test]
fn univariate_hensel_precision_schedule_uses_ceiling_halves() {
assert_eq!(univariate_hensel_precision_schedule(1), [1]);
assert_eq!(univariate_hensel_precision_schedule(2), [1, 2]);
assert_eq!(univariate_hensel_precision_schedule(3), [1, 2, 3]);
assert_eq!(univariate_hensel_precision_schedule(5), [1, 2, 3, 5]);
assert_eq!(
univariate_hensel_precision_schedule(77),
[1, 2, 3, 5, 10, 20, 39, 77]
);
}
#[test]
fn univariate_hensel_shortened_target_requires_a_decisive_saving() {
assert_eq!(univariate_hensel_shortened_target(43, 51, 86), Some(51));
assert_eq!(univariate_hensel_shortened_target(43, 64, 86), Some(64));
assert_eq!(univariate_hensel_shortened_target(43, 65, 86), None);
assert_eq!(univariate_hensel_shortened_target(43, 43, 86), None);
assert_eq!(univariate_hensel_shortened_target(43, 86, 86), None);
}
#[test]
fn balanced_leaf_pair_rejects_a_spurious_modular_split_and_falls_back() {
let variables = Some(Arc::new(vec![symbol!("x").into()]));
let target = parse!("x^4+1").to_polynomial::<_, u8>(&Z, variables.clone());
let field = Zp::new(17);
let modular_target = target.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
);
let modular_factors = modular_target.equal_degree_factorization(1);
assert_eq!(modular_factors.len(), 4);
let lift = UnivariateHenselProductTreeLiftContext::new(&modular_factors);
let modulus = Integer::from(17);
let dense_indices = (0..9).collect::<Vec<u32>>();
let context =
DenseIntegerModularUnivariateContext::new(&modulus, 0, &target, &dense_indices);
let leaf_indices = lift.topology.most_balanced_leaf_pair().unwrap();
assert!(
target
.try_reconstruct_balanced_leaf_pair(&lift, leaf_indices, &context, &modulus, 17, 0,)
.is_none()
);
PRODUCT_TREE_LAST_BEZOUT_UPDATE_EXPONENT.with(|exponent| exponent.set(0));
let bound = target.coefficient_bound();
let max_p = Integer::from(17).pow(80);
let factors = match target.lift_modular_factor_product_tree(&modular_factors, &max_p) {
UnivariateHenselProductTreeLiftResult::Lifted(lifted) => {
target.recombine_lifted_factors(lifted, &max_p, 0, &bound)
}
UnivariateHenselProductTreeLiftResult::Exact(factors) => factors,
};
assert_eq!(factors, [target]);
PRODUCT_TREE_LAST_BEZOUT_UPDATE_EXPONENT.with(|exponent| assert_eq!(exponent.get(), 40));
}
#[test]
fn product_tree_hensel_lift_matches_binary_lift_at_binary_odd_precision() {
let variables = Some(Arc::new(vec![symbol!("x").into()]));
let integer_factors = ["3*x+2", "5*x+3", "x^2+x+1", "x^3+x+1", "x^3+x^2+1"]
.map(|factor| parse!(factor).to_polynomial::<_, u8>(&Z, variables.clone()));
let target = integer_factors
.iter()
.fold(integer_factors[0].one(), |product, factor| {
&product * factor
});
assert!(!target.lcoeff().is_one());
let field = Z2;
let modular_factors = integer_factors
.iter()
.map(|factor| {
factor
.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
)
.make_monic()
})
.collect::<Vec<_>>();
assert_eq!(
modular_factors
.iter()
.map(|factor| factor.degree(0))
.collect::<Vec<_>>(),
[1u8, 1, 2, 3, 3]
);
let max_p = Integer::from(2).pow(41);
let UnivariateHenselProductTreeLiftResult::Lifted(product_tree) =
target.lift_modular_factor_product_tree(&modular_factors, &max_p)
else {
panic!("the low-precision comparison must return modular leaves");
};
let binary = target.lift_modular_factor_tree(&modular_factors, &max_p, false);
let reduce = |factor: &MultivariatePolynomial<IntegerRing, u8>| {
factor.map_coeff(|coefficient| coefficient.clone().symmetric_mod(&max_p), Z)
};
for (product_tree_factor, binary_factor) in product_tree.iter().zip(&binary) {
assert_eq!(reduce(product_tree_factor), reduce(binary_factor));
}
let normalized_target = target.map_coeff(
|coefficient| (coefficient * target.lcoeff().mod_inverse(&max_p)).symmetric_mod(&max_p),
Z,
);
let lifted_product = product_tree
.iter()
.fold(target.one(), |product, factor| &product * factor);
let product_error = &normalized_target - &lifted_product;
assert!(
product_error
.coefficients
.iter()
.all(|coefficient| (coefficient % &max_p).is_zero())
);
let bound = target.coefficient_bound();
assert!(max_p > bound);
let mut reconstructed = target.recombine_lifted_factors(product_tree, &max_p, 0, &bound);
let mut expected = integer_factors.to_vec();
reconstructed.sort_by(|left, right| left.internal_cmp(right));
expected.sort_by(|left, right| left.internal_cmp(right));
assert_eq!(reconstructed, expected);
}
#[test]
fn product_tree_hensel_lift_matches_binary_lift_over_small_odd_primes() {
fn check(prime: u32, exponent: u64, factor_texts: &[&str]) {
let variables = Some(Arc::new(vec![symbol!("x").into()]));
let integer_factors = factor_texts
.iter()
.map(|&factor| parse!(factor).to_polynomial::<_, u8>(&Z, variables.clone()))
.collect::<Vec<_>>();
let target = integer_factors
.iter()
.fold(integer_factors[0].one(), |product, factor| {
&product * factor
});
let field = Zp::new(prime);
let modular_factors = integer_factors
.iter()
.map(|factor| {
factor
.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
)
.make_monic()
})
.collect::<Vec<_>>();
let max_p = Integer::from(prime).pow(exponent);
let UnivariateHenselProductTreeLiftResult::Lifted(product_tree) =
target.lift_modular_factor_product_tree(&modular_factors, &max_p)
else {
panic!("the low-precision comparison must return modular leaves");
};
let binary = target.lift_modular_factor_tree(&modular_factors, &max_p, false);
let symmetric = |factor: &MultivariatePolynomial<IntegerRing, u8>| {
factor.map_coeff(|coefficient| coefficient.clone().symmetric_mod(&max_p), Z)
};
for ((product_tree_factor, binary_factor), expected) in
product_tree.iter().zip(&binary).zip(&integer_factors)
{
assert_eq!(symmetric(product_tree_factor), symmetric(binary_factor));
assert_eq!(product_tree_factor, expected);
}
let lifted_product = product_tree
.iter()
.fold(target.one(), |product, factor| &product * factor);
assert_eq!(lifted_product, target);
}
check(5, 9, &["x", "x-1", "x-2", "x-3", "x-4"]);
check(17, 7, &["x", "x-1", "x-2", "x-3", "x-4"]);
}
fn multiply_dense_bivariate<R: Ring>(
ring: &R,
left: &DenseBivariateImage<R::Element>,
right: &DenseBivariateImage<R::Element>,
) -> DenseBivariateImage<R::Element> {
let x_len = left.x_len + right.x_len - 1;
let y_len = left.y_len + right.y_len - 1;
let mut product = DenseBivariateImage {
x_len,
y_len,
coefficients: vec![ring.zero(); x_len * y_len],
};
for left_y in 0..left.y_len {
for left_x in 0..left.x_len {
let left_coefficient = left.coefficient(left_x, left_y);
if ring.is_zero(left_coefficient) {
continue;
}
for right_y in 0..right.y_len {
for right_x in 0..right.x_len {
let right_coefficient = right.coefficient(right_x, right_y);
if !ring.is_zero(right_coefficient) {
let index = product.index(left_x + right_x, left_y + right_y);
ring.add_mul_assign(
&mut product.coefficients[index],
left_coefficient,
right_coefficient,
);
}
}
}
}
}
product
}
fn assert_dense_image_matches<R: Ring>(
expected: &MultivariatePolynomial<R, u8>,
image: &DenseBivariateImage<R::Element>,
x: usize,
y: usize,
) {
let mut exponent = vec![0u8; expected.nvars()];
for y_degree in 0..image.y_len {
exponent[y] = y_degree as u8;
for x_degree in 0..image.x_len {
exponent[x] = x_degree as u8;
let coefficient = expected
.coefficient(&exponent)
.unwrap_or_else(|| expected.ring().zero());
assert_eq!(image.coefficient(x_degree, y_degree), &coefficient);
}
exponent[x] = 0;
}
}
#[test]
fn geometric_bivariate_images_match_direct_specialization() {
let field = Zp::new(101);
// Retained variables have nonzero, nonadjacent indices so the test also
// checks that image coordinates use variable indices rather than positions.
let vars = Some(Arc::new(vec![
symbol!("z").into(),
symbol!("x").into(),
symbol!("w").into(),
symbol!("y").into(),
]));
let poly = parse!("3+5*x+7*y^3+11*x^2*y+3*z*x*y-2*w*x*y+17*z^2*w*x^3+19*z*w^2*x^2*y^2")
.to_polynomial::<_, u8>(&field, vars);
let x = 1;
let y = 3;
let base_points = vec![(0, field.nth(2.into())), (2, field.nth(3.into()))];
let mut cache = MultivariatePolynomial::sample_cache(&poly, &[], &[]);
let images = poly.evaluate_geometric_bivariate_images(x, y, &base_points, 4, &mut cache);
for (sample_index, image) in images.iter().enumerate() {
let mut expected = poly.clone();
for (variable, base) in &base_points {
let value = field.pow(base, (sample_index + 1) as u64);
expected = expected.replace(*variable, &value);
}
assert_dense_image_matches(&expected, image, x, y);
}
// At beta^1, 3*z*x*y - 2*w*x*y = 6*x*y - 6*x*y.
assert!(field.is_zero(images[0].coefficient(1, 1)));
}
#[test]
fn geometric_bivariate_images_preserve_products() {
let field = Zp::new(101);
let vars = Some(Arc::new(vec![
symbol!("z").into(),
symbol!("x").into(),
symbol!("w").into(),
symbol!("y").into(),
]));
let left = parse!("1+x+z*y+w*x^2*y").to_polynomial::<_, u8>(&field, vars.clone());
let right = parse!("2+y+w*x+z*x*y^2").to_polynomial::<_, u8>(&field, vars);
let product = &left * &right;
let base_points = vec![(0, field.nth(2.into())), (2, field.nth(3.into()))];
let factors = [left.clone(), right.clone()];
let mut cache = MultivariatePolynomial::sample_cache(&product, &factors, &[]);
let left_images =
left.evaluate_geometric_bivariate_images(1, 3, &base_points, 4, &mut cache);
let right_images =
right.evaluate_geometric_bivariate_images(1, 3, &base_points, 4, &mut cache);
let product_images =
product.evaluate_geometric_bivariate_images(1, 3, &base_points, 4, &mut cache);
for ((left_image, right_image), product_image) in
left_images.iter().zip(&right_images).zip(&product_images)
{
assert_eq!(
multiply_dense_bivariate(&field, left_image, right_image),
*product_image
);
}
}
#[test]
fn evaluated_two_factor_hensel_step_reconstructs_four_variable_factors() {
let field = Zp::new(1_000_003);
let vars = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("z").into(),
symbol!("w").into(),
symbol!("y").into(),
]));
let polynomial = |input| {
parse!(input)
.expand()
.to_polynomial::<_, u8>(&field, vars.clone())
};
let expected = vec![
polynomial("x^2+(z+w)*x+1+((2*z+3*w)*x+4)*y+((5*z+7*w)*x+8)*y^2"),
polynomial("x^2+(z+w)*x+2+((11*z+13*w)*x+14)*y+((17*z+19*w)*x+20)*y^2"),
];
let target = &expected[0] * &expected[1];
let initial = expected
.iter()
.map(|factor| factor.replace(3, &field.zero()))
.collect::<Vec<_>>();
let lifted = (0..8)
.find_map(|_| {
target.try_multivariate_hensel_step_two_factor_evaluated(
&initial,
&[0, 1, 2, 3],
target.degree(3) as usize,
true,
)
})
.expect("the evaluated two-factor lift should reconstruct both factors");
assert_eq!(lifted, expected);
assert_eq!(&lifted[0] * &lifted[1], target);
}
#[test]
fn evaluated_two_factor_hensel_step_verifies_unexamined_product_tail() {
let field = Zp::new(1_000_003);
let vars = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("z").into(),
symbol!("y").into(),
]));
let polynomial = |input| {
parse!(input)
.expand()
.to_polynomial::<_, u8>(&field, vars.clone())
};
let initial = vec![polynomial("x+z"), polynomial("x+2*z")];
let target = polynomial("(x+z)*(x+2*z)+(z*(x+2*z)+2*z*(x+z))*y");
let unchecked = (0..8)
.find_map(|_| {
target.try_multivariate_hensel_step_two_factor_evaluated(
&initial,
&[0, 1, 2],
1,
false,
)
})
.expect("the sampled corrections should reconstruct");
assert_ne!(&unchecked[0] * &unchecked[1], target);
assert!(
target
.try_multivariate_hensel_step_two_factor_evaluated(&initial, &[0, 1, 2], 1, true,)
.is_none()
);
}
#[test]
fn evaluated_two_factor_hensel_step_rejects_oversized_image_grid() {
let field = Zp::new(101);
let vars = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("z").into(),
symbol!("y").into(),
]));
let polynomial = |input| {
parse!(input)
.expand()
.to_polynomial::<_, u32>(&field, vars.clone())
};
let expected = vec![polynomial("x^5000000+z+z*y"), polynomial("x+2*z")];
let target = &expected[0] * &expected[1];
let initial = expected
.iter()
.map(|factor| factor.replace(2, &field.zero()))
.collect::<Vec<_>>();
assert!(
target
.try_multivariate_hensel_step_two_factor_evaluated(&initial, &[0, 1, 2], 1, true,)
.is_none()
);
}
#[test]
fn evaluated_two_factor_hensel_step_rejects_generator_collision() {
let field = Z2;
let vars = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("z").into(),
symbol!("w").into(),
symbol!("y").into(),
]));
let polynomial = |input| {
parse!(input)
.expand()
.to_polynomial::<_, u8>(&field, vars.clone())
};
let expected = vec![
polynomial("x^2+(z+w)*x+1+((z+w)*x+1)*y"),
polynomial("x^2+(z+w)*x+((z+w)*x+1)*y"),
];
let target = &expected[0] * &expected[1];
let initial = expected
.iter()
.map(|factor| factor.replace(3, &field.zero()))
.collect::<Vec<_>>();
assert!(
target
.try_multivariate_hensel_step_two_factor_evaluated(
&initial,
&[0, 1, 2, 3],
target.degree(3) as usize,
true,
)
.is_none()
);
}
#[test]
fn dense_two_factor_corrections_match_generic_univariate_solver() {
let field = Zp::new(101);
let vars = Some(Arc::new(vec![symbol!("x").into(), symbol!("y").into()]));
let polynomial = |input| {
parse!(input)
.expand()
.to_polynomial::<_, u8>(&field, vars.clone())
};
let factors = [polynomial("3*x^3+2*x+1"), polynomial("5*x^2+7*x+4")];
let mut cache = MultivariatePolynomial::sample_cache(&factors[0], &factors, &[]);
let factor_images = factors
.iter()
.map(|factor| {
factor
.evaluate_geometric_bivariate_images(0, 1, &[], 1, &mut cache)
.pop()
.unwrap()
})
.collect::<Vec<_>>();
let (gcd, s, t) = factors[0].eea_univariate(&factors[1]);
assert!(gcd.is_one());
let context = DenseTwoFactorCorrectionContext {
multipliers: [
factors[0].dense_indexed_univariate(&t, 0).unwrap(),
factors[0].dense_indexed_univariate(&s, 0).unwrap(),
],
moduli: [
factors[0].dense_monic_modulus(&factor_images[0]).unwrap(),
factors[0].dense_monic_modulus(&factor_images[1]).unwrap(),
],
};
for rhs in [
polynomial("0"),
polynomial("17"),
polynomial("11*x^4+13*x^3+17*x+19"),
] {
let rhs_image = rhs
.evaluate_geometric_bivariate_images(0, 1, &[], 1, &mut cache)
.pop()
.unwrap();
let actual = rhs
.dense_two_factor_corrections(
&rhs_image.coefficients,
&context,
[factor_images[0].x_len, factor_images[1].x_len],
)
.unwrap();
let expected =
MultivariatePolynomial::try_univariate_diophantine(&mut factors.to_vec(), &rhs)
.unwrap();
for factor_index in 0..2 {
let expected_image = expected[factor_index]
.evaluate_geometric_bivariate_images(0, 1, &[], 1, &mut cache)
.pop()
.unwrap();
let mut expected_coefficients = expected_image.coefficients;
expected_coefficients.resize(actual[factor_index].len(), field.zero());
assert_eq!(actual[factor_index], expected_coefficients);
}
}
}
#[test]
fn packed_product_accumulation_matches_exact_product() {
let field = Zp::new(101);
let vars = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("z").into(),
symbol!("w").into(),
symbol!("y").into(),
]));
let polynomial = |input| {
parse!(input)
.expand()
.to_polynomial::<_, u8>(&field, vars.clone())
};
let left = polynomial("1+x+z*x^2+w*y+x*z*y");
let right = polynomial("2+w*x+z*y+x^2*y+w*z*x");
let product = &left * &right;
assert_eq!(
product.product_matches_by_packed_accumulation(&left, &right),
Some(true)
);
let wrong_product = &product + &polynomial("1");
assert_eq!(
wrong_product.product_matches_by_packed_accumulation(&left, &right),
Some(false)
);
}
#[test]
fn packed_product_verification_preserves_total_degree_dense_dispatch() {
let field = Zp::new(101);
let vars = Some(Arc::new(vec![
symbol!("a").into(),
symbol!("b").into(),
symbol!("c").into(),
symbol!("d").into(),
symbol!("e").into(),
symbol!("f").into(),
symbol!("g").into(),
symbol!("h").into(),
]));
let dense = parse!("(1+a+b+c+d+e+f+g+h)^5")
.expand()
.to_polynomial::<_, u8>(&field, vars);
assert!(dense.total_degree_dense_mul_is_bounded(&dense));
assert!(!MultivariatePolynomial::packed_product_accumulation_is_preferred(&dense, &dense,));
}
#[test]
fn two_factor_univariate_correction_reuses_bezout_for_both_components() {
let field = Zp::new(101);
let vars = Some(Arc::new(vec![symbol!("x").into()]));
let factors = ["x^2+2", "x+3"]
.iter()
.map(|factor| {
parse!(factor)
.expand()
.to_polynomial::<_, u8>(&field, vars.clone())
})
.collect::<Vec<_>>();
let rhs = ["7+5*x+11*x^2", "13+17*x+19*x^2"]
.iter()
.map(|rhs| {
parse!(rhs)
.expand()
.to_polynomial::<_, u8>(&field, vars.clone())
})
.collect::<Vec<_>>();
let mut context = SparseDiophantineContext::new();
for rhs in &rhs {
let expected =
MultivariatePolynomial::try_univariate_diophantine(&mut factors.clone(), rhs)
.unwrap();
for requested_factor in 0..2 {
let correction = MultivariatePolynomial::try_two_factor_univariate_correction(
&mut factors.clone(),
rhs,
requested_factor,
&mut context,
)
.unwrap();
assert_eq!(correction, expected[requested_factor]);
let residual = rhs - &(&correction * &factors[1 - requested_factor]);
let remainder = residual
.quot_rem_univariate(&mut factors[requested_factor].clone())
.1;
assert!(remainder.is_zero());
assert_eq!(context.two_factor_bezout.len(), 1);
}
}
}
#[test]
fn two_factor_univariate_correction_caches_noncoprime_failure() {
let field = Zp::new(101);
let vars = Some(Arc::new(vec![symbol!("x").into()]));
let factors = ["(x+1)*(x+2)", "(x+1)*(x+3)"]
.iter()
.map(|factor| {
parse!(factor)
.expand()
.to_polynomial::<_, u8>(&field, vars.clone())
})
.collect::<Vec<_>>();
let rhs = parse!("1+x").to_polynomial::<_, u8>(&field, vars).clone();
let mut context = SparseDiophantineContext::new();
for requested_factor in 0..2 {
assert!(
MultivariatePolynomial::try_two_factor_univariate_correction(
&mut factors.clone(),
&rhs,
requested_factor,
&mut context,
)
.is_none()
);
assert_eq!(context.two_factor_bezout.len(), 1);
assert!(context.two_factor_bezout.values().next().unwrap().is_none());
}
}
#[test]
fn two_factor_sparse_correction_reuses_geometric_base() {
let field = Zp::new(101);
let vars = Some(Arc::new(vec![symbol!("x").into(), symbol!("z").into()]));
let polynomial = |input| {
parse!(input)
.expand()
.to_polynomial::<_, u8>(&field, vars.clone())
};
let factors = vec![polynomial("x+z"), polynomial("x+z+1")];
let prods = vec![factors[1].clone(), factors[0].clone()];
let skeletons = vec![polynomial("z"), polynomial("z")];
let corrections = [
vec![polynomial("2*z"), polynomial("3*z")],
vec![polynomial("5*z"), polynomial("7*z")],
];
let mut context = SparseDiophantineContext::new();
let mut first_base = None;
for expected in corrections {
let error = &(&expected[0] * &prods[0]) + &(&expected[1] * &prods[1]);
let actual = MultivariatePolynomial::sparse_multivariate_diophantine_from_skeleton(
&factors,
&prods,
&error,
&skeletons,
&[0, 1],
&mut context,
)
.unwrap();
assert_eq!(actual, expected);
assert_eq!(&(&actual[0] * &prods[0]) + &(&actual[1] * &prods[1]), error);
assert_eq!(context.two_factor_bezout.len(), 1);
if let Some(base) = &first_base {
assert_eq!(context.two_factor_base_points.as_ref(), Some(base));
} else {
first_base = context.two_factor_base_points.clone();
assert!(first_base.is_some());
}
}
}
struct FactorSettingsGuard {
use_univariate: bool,
use_bivariate: bool,
}
impl FactorSettingsGuard {
fn new() -> Self {
Self {
use_univariate: GLOBAL_SETTINGS
.use_univariate_factorization
.load(Ordering::Relaxed),
use_bivariate: GLOBAL_SETTINGS
.use_bivariate_factorization
.load(Ordering::Relaxed),
}
}
}
impl Drop for FactorSettingsGuard {
fn drop(&mut self) {
GLOBAL_SETTINGS
.use_univariate_factorization
.store(self.use_univariate, Ordering::Relaxed);
GLOBAL_SETTINGS
.use_bivariate_factorization
.store(self.use_bivariate, Ordering::Relaxed);
}
}
#[test]
fn linear_active_variable_certifies_post_separable_irreducibility() {
let _lock = GLOBAL_FACTOR_SETTINGS_LOCK.lock().unwrap();
let _guard = FactorSettingsGuard::new();
GLOBAL_SETTINGS
.use_univariate_factorization
.store(true, Ordering::Relaxed);
GLOBAL_SETTINGS
.use_bivariate_factorization
.store(true, Ordering::Relaxed);
let polynomial = parse!("x+y*z+z^2+1").to_polynomial::<_, u8>(&Z, None);
LINEAR_VARIABLE_IRREDUCIBILITY_CERTIFICATES.with(|count| count.set(0));
UNIVARIATE_IRREDUCIBILITY_SCOUTS.with(|count| count.set(0));
assert_eq!(polynomial.factor(), vec![(polynomial.clone(), 1)]);
LINEAR_VARIABLE_IRREDUCIBILITY_CERTIFICATES.with(|count| assert_eq!(count.get(), 1));
UNIVARIATE_IRREDUCIBILITY_SCOUTS.with(|count| assert_eq!(count.get(), 0));
}
#[test]
fn irreducible_univariate_image_certifies_multivariate_component() {
let _lock = GLOBAL_FACTOR_SETTINGS_LOCK.lock().unwrap();
let _guard = FactorSettingsGuard::new();
GLOBAL_SETTINGS
.use_univariate_factorization
.store(true, Ordering::Relaxed);
GLOBAL_SETTINGS
.use_bivariate_factorization
.store(true, Ordering::Relaxed);
let polynomial = parse!("x^2+y^2+z^2+1").to_polynomial::<_, u8>(&Z, None);
UNIVARIATE_IRREDUCIBILITY_SCOUTS.with(|count| count.set(0));
UNIVARIATE_IRREDUCIBILITY_CERTIFICATES.with(|count| count.set(0));
assert_eq!(polynomial.factor(), vec![(polynomial.clone(), 1)]);
UNIVARIATE_IRREDUCIBILITY_SCOUTS.with(|count| assert_eq!(count.get(), 1));
UNIVARIATE_IRREDUCIBILITY_CERTIFICATES.with(|count| assert_eq!(count.get(), 1));
}
#[test]
fn reducible_univariate_image_prefers_univariate_factorization() {
let _lock = GLOBAL_FACTOR_SETTINGS_LOCK.lock().unwrap();
let _guard = FactorSettingsGuard::new();
GLOBAL_SETTINGS
.use_univariate_factorization
.store(true, Ordering::Relaxed);
GLOBAL_SETTINGS
.use_bivariate_factorization
.store(true, Ordering::Relaxed);
let reducible = parse!("(x^2+y^2+z^2+1)*(x+y+2*z+1)")
.expand()
.to_polynomial::<_, u8>(&Z, None);
assert!(reducible.integer_factor_start_auto_decision(&[0, 1, 2]).0);
UNIVARIATE_IRREDUCIBILITY_SCOUTS.with(|count| count.set(0));
UNIVARIATE_IRREDUCIBILITY_CERTIFICATES.with(|count| count.set(0));
BIVARIATE_SAMPLE_FACTORIZATIONS.with(|count| count.set(0));
let factors = reducible.factor();
assert_eq!(factors.len(), 2);
assert_eq!(
factors
.iter()
.fold(reducible.one(), |product, (factor, power)| {
&product * &factor.pow(*power)
}),
reducible
);
UNIVARIATE_IRREDUCIBILITY_SCOUTS.with(|count| assert_eq!(count.get(), 1));
UNIVARIATE_IRREDUCIBILITY_CERTIFICATES.with(|count| assert_eq!(count.get(), 0));
BIVARIATE_SAMPLE_FACTORIZATIONS.with(|count| assert_eq!(count.get(), 0));
}
#[test]
fn degree_dropping_univariate_image_is_inconclusive() {
let degree_drop = parse!("x^2*y^3-x^2+x+y+z^4")
.expand()
.to_polynomial::<_, u8>(&Z, None);
let degrees = (0..degree_drop.nvars())
.map(|variable| degree_drop.degree(variable).to_u32() as usize)
.collect::<Vec<_>>();
assert_eq!(
degree_drop.univariate_specialization_factorization(°rees),
UnivariateSpecializationFactorization::Inconclusive
);
}
#[test]
fn repeated_univariate_specialization_is_classified_as_reducible() {
let polynomial = parse!("x^2+2*x+y^2+z^2-1").to_polynomial::<_, u8>(&Z, None);
assert_eq!(
polynomial.univariate_specialization_factorization(&[2, 2, 2]),
UnivariateSpecializationFactorization::ReducibleImage
);
assert_eq!(polynomial.factor(), vec![(polynomial.clone(), 1)]);
}
#[test]
fn integer_factorization_respects_global_start_settings() {
let _lock = GLOBAL_FACTOR_SETTINGS_LOCK.lock().unwrap();
let _guard = FactorSettingsGuard::new();
let input = "(1+v1+v2+v3)*(2+3*v1+5*v2+7*v3)";
let poly = parse!(input).to_polynomial::<_, u8>(&Z, None);
GLOBAL_SETTINGS
.use_univariate_factorization
.store(true, Ordering::Relaxed);
GLOBAL_SETTINGS
.use_bivariate_factorization
.store(false, Ordering::Relaxed);
UNIVARIATE_IRREDUCIBILITY_SCOUTS.with(|count| count.set(0));
assert_eq!(poly.factor().len(), 2);
UNIVARIATE_IRREDUCIBILITY_SCOUTS.with(|count| assert_eq!(count.get(), 0));
GLOBAL_SETTINGS
.use_univariate_factorization
.store(false, Ordering::Relaxed);
GLOBAL_SETTINGS
.use_bivariate_factorization
.store(true, Ordering::Relaxed);
UNIVARIATE_IRREDUCIBILITY_SCOUTS.with(|count| count.set(0));
assert_eq!(poly.factor().len(), 2);
UNIVARIATE_IRREDUCIBILITY_SCOUTS.with(|count| assert_eq!(count.get(), 0));
GLOBAL_SETTINGS
.use_univariate_factorization
.store(false, Ordering::Relaxed);
GLOBAL_SETTINGS
.use_bivariate_factorization
.store(false, Ordering::Relaxed);
UNIVARIATE_IRREDUCIBILITY_SCOUTS.with(|count| count.set(0));
assert_eq!(poly.factor(), vec![(poly, 1)]);
UNIVARIATE_IRREDUCIBILITY_SCOUTS.with(|count| assert_eq!(count.get(), 0));
}
#[test]
fn bivariate_sampling_uses_one_admissible_image_on_initial_primitive_attempt() {
let variables = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("y").into(),
symbol!("z").into(),
]));
let polynomial = parse!("(x+y+z+1)*(2*x+3*y+5*z+1)")
.expand()
.to_polynomial::<_, u8>(&Z, variables);
BIVARIATE_SAMPLE_FACTORIZATIONS.with(|count| count.set(0));
BIVARIATE_FIRST_SAMPLE_ACCEPTANCES.with(|count| count.set(0));
let mut order = vec![0, 1, 2];
let (factors, _, _, _, _) = polynomial.find_sample(&mut order, 10, None);
assert_eq!(factors.len(), 2);
BIVARIATE_SAMPLE_FACTORIZATIONS.with(|count| assert_eq!(count.get(), 1));
BIVARIATE_FIRST_SAMPLE_ACCEPTANCES.with(|count| assert_eq!(count.get(), 1));
BIVARIATE_SAMPLE_FACTORIZATIONS.with(|count| count.set(0));
BIVARIATE_FIRST_SAMPLE_ACCEPTANCES.with(|count| count.set(0));
let mut order = vec![0, 1, 2];
let (factors, _, _, _, _) = polynomial.find_sample(&mut order, 10, Some(2));
assert_eq!(factors.len(), 2);
BIVARIATE_SAMPLE_FACTORIZATIONS.with(|count| assert_eq!(count.get(), 3));
BIVARIATE_FIRST_SAMPLE_ACCEPTANCES.with(|count| assert_eq!(count.get(), 0));
}
fn dense_bivariate_wang_test_polynomial() -> MultivariatePolynomial<IntegerRing, u8> {
let variables = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("y").into(),
symbol!("z").into(),
]));
let left = parse!("2*x*y*z^6+(1+2*y)+(3+5*y)*z+(7+11*y)*z^2+(13+17*y)*z^3+(19+23*y)*z^4")
.expand()
.to_polynomial::<_, u8>(&Z, variables.clone());
let right = parse!(
"3*x*y*z^7+(30+31*y)+(37+41*y)*z+(43+47*y)*z^2+(53+59*y)*z^3+(61+67*y)*z^4+(71+73*y)*z^5"
)
.expand()
.to_polynomial::<_, u8>(&Z, variables);
&left * &right
}
#[test]
fn bivariate_sampling_reconstructs_a_monomial_lcoeff_from_one_prime_image() {
let _lock = GLOBAL_FACTOR_SETTINGS_LOCK.lock().unwrap();
let _guard = FactorSettingsGuard::new();
GLOBAL_SETTINGS
.use_univariate_factorization
.store(true, Ordering::Relaxed);
GLOBAL_SETTINGS
.use_bivariate_factorization
.store(true, Ordering::Relaxed);
let polynomial = dense_bivariate_wang_test_polynomial();
let (_, density) = polynomial.integer_factor_start_auto_decision(&[0, 1, 2]);
assert!(density > INTEGER_FACTOR_BIVARIATE_WANG_MIN_BOX_DENSITY);
BIVARIATE_SAMPLE_FACTORIZATIONS.with(|count| count.set(0));
BIVARIATE_FIRST_SAMPLE_ACCEPTANCES.with(|count| count.set(0));
BIVARIATE_WANG_SAMPLE_TRIES.with(|count| count.set(0));
let mut order = vec![0, 1, 2];
let (factors, sample_points, _, _, wang_lcoeffs) =
polynomial.find_sample(&mut order, 10, None);
assert_eq!(factors.len(), 2);
assert!(wang_lcoeffs.is_some());
assert_eq!(
sample_points,
[(1, Integer::from(5)), (2, Integer::from(7))]
);
BIVARIATE_SAMPLE_FACTORIZATIONS.with(|count| assert_eq!(count.get(), 1));
BIVARIATE_FIRST_SAMPLE_ACCEPTANCES.with(|count| assert_eq!(count.get(), 1));
BIVARIATE_WANG_SAMPLE_TRIES.with(|count| assert_eq!(count.get(), 1));
let mut factor_order = vec![0, 1, 2];
let factors =
polynomial.multivariate_factorization(&mut factor_order, 10, None, None, false);
assert_eq!(factors.len(), 2);
let reconstructed = factors
.into_iter()
.fold(polynomial.one(), |product, factor| &product * &factor);
assert_eq!(reconstructed, polynomial);
let negative = -polynomial.clone();
let mut order = vec![0, 1, 2];
let (_, _, _, _, negative_wang_lcoeffs) = negative.find_sample(&mut order, 10, None);
assert_eq!(negative_wang_lcoeffs.unwrap().0, Integer::from(-1));
let mut factor_order = vec![0, 1, 2];
let factors = negative.multivariate_factorization(&mut factor_order, 10, None, None, false);
assert_eq!(factors.len(), 2);
let reconstructed = factors
.into_iter()
.fold(negative.one(), |product, factor| &product * &factor);
assert_eq!(reconstructed, negative);
BIVARIATE_SAMPLE_FACTORIZATIONS.with(|count| count.set(0));
BIVARIATE_FIRST_SAMPLE_ACCEPTANCES.with(|count| count.set(0));
BIVARIATE_WANG_SAMPLE_TRIES.with(|count| count.set(0));
let mut order = vec![0, 1, 2];
let (factors, _, _, _, wang_lcoeffs) = polynomial.find_sample(&mut order, 10, Some(2));
assert_eq!(factors.len(), 2);
assert!(wang_lcoeffs.is_none());
BIVARIATE_SAMPLE_FACTORIZATIONS.with(|count| assert_eq!(count.get(), 3));
BIVARIATE_FIRST_SAMPLE_ACCEPTANCES.with(|count| assert_eq!(count.get(), 0));
BIVARIATE_WANG_SAMPLE_TRIES.with(|count| assert_eq!(count.get(), 0));
GLOBAL_SETTINGS
.use_univariate_factorization
.store(false, Ordering::Relaxed);
BIVARIATE_WANG_SAMPLE_TRIES.with(|count| count.set(0));
let mut order = vec![0, 1, 2];
let (_, _, _, _, wang_lcoeffs) = polynomial.find_sample(&mut order, 10, None);
assert!(wang_lcoeffs.is_none());
BIVARIATE_WANG_SAMPLE_TRIES.with(|count| assert_eq!(count.get(), 0));
}
#[test]
fn bivariate_wang_sampling_keeps_the_bounded_path_for_nonunit_content() {
let _lock = GLOBAL_FACTOR_SETTINGS_LOCK.lock().unwrap();
let _guard = FactorSettingsGuard::new();
GLOBAL_SETTINGS
.use_univariate_factorization
.store(true, Ordering::Relaxed);
GLOBAL_SETTINGS
.use_bivariate_factorization
.store(true, Ordering::Relaxed);
let polynomial = dense_bivariate_wang_test_polynomial().mul_coeff(Integer::from(2));
let (_, density) = polynomial.integer_factor_start_auto_decision(&[0, 1, 2]);
assert!(density > INTEGER_FACTOR_BIVARIATE_WANG_MIN_BOX_DENSITY);
BIVARIATE_SAMPLE_FACTORIZATIONS.with(|count| count.set(0));
BIVARIATE_FIRST_SAMPLE_ACCEPTANCES.with(|count| count.set(0));
BIVARIATE_WANG_SAMPLE_TRIES.with(|count| count.set(0));
let mut order = vec![0, 1, 2];
let (factors, _, _, _, wang_lcoeffs) = polynomial.find_sample(&mut order, 10, None);
assert_eq!(factors.len(), 2);
assert!(wang_lcoeffs.is_none());
BIVARIATE_SAMPLE_FACTORIZATIONS.with(|count| assert_eq!(count.get(), 3));
BIVARIATE_FIRST_SAMPLE_ACCEPTANCES.with(|count| assert_eq!(count.get(), 0));
BIVARIATE_WANG_SAMPLE_TRIES.with(|count| assert_eq!(count.get(), 2));
}
#[test]
fn bivariate_wang_sampling_rotates_past_nonprimitive_prime_images() {
let _lock = GLOBAL_FACTOR_SETTINGS_LOCK.lock().unwrap();
let _guard = FactorSettingsGuard::new();
GLOBAL_SETTINGS
.use_univariate_factorization
.store(true, Ordering::Relaxed);
GLOBAL_SETTINGS
.use_bivariate_factorization
.store(true, Ordering::Relaxed);
let variables = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("y").into(),
symbol!("z").into(),
]));
let polynomial = parse!(
"(x^2*y*z+z+3+(z-3)*(2+2*y+5*z+7*y*z+11*z^2+13*y*z^2+17*z^3+19*y*z^3))*(x*y*z+23+29*y+31*z+37*y*z+41*z^2+43*y*z^2+47*z^3+53*y*z^3+59*z^4+61*y*z^4+67*z^5+71*y*z^5)"
)
.expand()
.to_polynomial::<_, u8>(&Z, variables);
let (_, density) = polynomial.integer_factor_start_auto_decision(&[0, 1, 2]);
assert!(density > INTEGER_FACTOR_BIVARIATE_WANG_MIN_BOX_DENSITY);
BIVARIATE_SAMPLE_FACTORIZATIONS.with(|count| count.set(0));
BIVARIATE_FIRST_SAMPLE_ACCEPTANCES.with(|count| count.set(0));
BIVARIATE_WANG_SAMPLE_TRIES.with(|count| count.set(0));
let mut order = vec![0, 1, 2];
let (factors, sample_points, _, _, wang_lcoeffs) =
polynomial.find_sample(&mut order, 10, None);
assert_eq!(factors.len(), 2);
assert!(wang_lcoeffs.is_some());
assert_eq!(
sample_points,
[(1, Integer::from(3)), (2, Integer::from(2))]
);
BIVARIATE_SAMPLE_FACTORIZATIONS.with(|count| assert_eq!(count.get(), 1));
BIVARIATE_FIRST_SAMPLE_ACCEPTANCES.with(|count| assert_eq!(count.get(), 1));
BIVARIATE_WANG_SAMPLE_TRIES.with(|count| assert_eq!(count.get(), 2));
}
#[test]
fn bivariate_sampling_accepts_irreducible_image_with_a_bounded_retry() {
let variables = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("y").into(),
symbol!("z").into(),
]));
let polynomial = parse!("x*y+z+1").to_polynomial::<_, u8>(&Z, variables);
BIVARIATE_SAMPLE_FACTORIZATIONS.with(|count| count.set(0));
BIVARIATE_FIRST_SAMPLE_ACCEPTANCES.with(|count| count.set(0));
let mut order = vec![0, 1, 2];
let (factors, _, _, _, _) = polynomial.find_sample(&mut order, 10, Some(2));
assert_eq!(factors.len(), 1);
BIVARIATE_SAMPLE_FACTORIZATIONS.with(|count| assert_eq!(count.get(), 1));
BIVARIATE_FIRST_SAMPLE_ACCEPTANCES.with(|count| assert_eq!(count.get(), 0));
}
#[test]
fn factor_ff_square_free() {
let field = Zp::new(3);
let poly = parse!("(1+v1)*(1+v1^2)^2*(v1^4+1)^3").to_polynomial::<_, u8>(&field, None);
let res = [("1+v1^4", 3), ("1+v1^2", 2), ("1+v1", 1)];
let mut res = res
.iter()
.map(|(f, p)| {
(
parse!(f)
.expand()
.to_polynomial(&field, poly.variables().clone()),
*p,
)
})
.collect::<Vec<_>>();
res.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
let mut r = poly.square_free_factorization();
r.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
assert_eq!(r, res);
}
#[test]
fn factor_ff_bivariate() {
let field = Zp::new(997);
let poly = parse!("((v2+1)*v1^2+v1*v2+1)*((v2^2+2)*v1^2+v2+1)")
.to_polynomial::<_, u8>(&field, None);
let res = [("1+2*v1^2+v2+v2^2*v1^2", 1), ("1+v1^2+v2*v1+v2*v1^2", 1)];
let mut res = res
.iter()
.map(|(f, p)| {
(
parse!(f)
.expand()
.to_polynomial(&field, poly.variables().clone()),
*p,
)
})
.collect::<Vec<_>>();
res.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
let mut r = poly.factor();
r.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
assert_eq!(r, res);
}
#[test]
fn factor_square_free() {
let poly =
parse!("3*(2*v1^2+v2)(v1^3+v2)^2(1+4*v2)^2(1+v1)").to_polynomial::<_, u8>(&Z, None);
let res = [
("3", 1),
("1+4*v2", 2),
("1+v1", 1),
("v2+2*v1^2", 1),
("v2+v1^3", 2),
];
let mut res = res
.iter()
.map(|(f, p)| {
(
parse!(f)
.expand()
.to_polynomial(&Z, poly.variables().clone()),
*p,
)
})
.collect::<Vec<_>>();
res.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
let mut r = poly.square_free_factorization();
r.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
assert_eq!(r, res);
}
#[test]
fn factor_square_free_extracts_common_monomial_powers() {
let poly = parse!("-6*x^3*y^2*(x+y+1)^2")
.expand()
.to_polynomial::<_, u8>(&Z, None);
let expected = [("-6", 1), ("x", 3), ("y", 2), ("x+y+1", 2)];
let mut expected = expected
.into_iter()
.map(|(factor, multiplicity)| {
(
parse!(factor).to_polynomial(&Z, poly.variables().clone()),
multiplicity,
)
})
.collect::<Vec<_>>();
expected.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
let mut actual = poly.square_free_factorization();
actual.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
assert_eq!(actual, expected);
}
#[test]
fn factor_univariate_1() {
let poly = parse!("2*(4 + 3*v1)*(3 + 2*v1 + 3*v1^2)*(3 + 8*v1^2)*(4 + v1 + v1^16)")
.to_polynomial::<_, u8>(&Z, None);
let res = [
("2", 1),
("4+3*v1", 1),
("3+2*v1+3*v1^2", 1),
("3+8*v1^2", 1),
("4+v1+v1^16", 1),
];
let mut res = res
.iter()
.map(|(f, p)| {
(
parse!(f)
.expand()
.to_polynomial(&Z, poly.variables().clone()),
*p,
)
})
.collect::<Vec<_>>();
res.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
let mut r = poly.factor();
r.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
assert_eq!(r, res);
}
#[test]
fn factor_univariate_2() {
let poly =
parse!("(v1+1)(v1+2)(v1+3)^3(v1+4)(v1+5)(v1^2+6)(v1^3+7)(v1+8)^2(v1^4+9)(v1^5+v1+10)")
.to_polynomial::<_, u8>(&Z, None);
let res = [
("5+v1", 1),
("1+v1", 1),
("4+v1", 1),
("2+v1", 1),
("7+v1^3", 1),
("10+v1+v1^5", 1),
("6+v1^2", 1),
("9+v1^4", 1),
("8+v1", 2),
("3+v1", 3),
];
let mut res = res
.iter()
.map(|(f, p)| {
(
parse!(f)
.expand()
.to_polynomial(&Z, poly.variables().clone()),
*p,
)
})
.collect::<Vec<_>>();
res.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
let mut r = poly.factor();
r.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
assert_eq!(r, res);
}
fn dense_zp_test_polynomial(
prototype: &MultivariatePolynomial<Zp, u8>,
coefficients: &[FiniteFieldElement<u32>],
) -> MultivariatePolynomial<Zp, u8> {
let mut polynomial = prototype.zero_with_capacity(coefficients.len());
let mut exponents = vec![0u8; prototype.nvars()];
for (degree, coefficient) in coefficients.iter().enumerate() {
if prototype.ring().is_zero(coefficient) {
continue;
}
exponents[0] = degree as u8;
polynomial.append_monomial_back(*coefficient, &exponents);
}
polynomial
}
fn generated_dense_zp_coefficients(
field: &Zp,
coefficient_count: usize,
seed: u64,
) -> Vec<FiniteFieldElement<u32>> {
let nonzero_residue_count = u64::from(field.get_prime() - 1);
(0..coefficient_count)
.map(|index| {
let value = (17 * index as u64 + 29 * seed) % nonzero_residue_count + 1;
field.to_element(value as u32)
})
.collect()
}
fn generated_dense_zp_modulus(
prototype: &MultivariatePolynomial<Zp, u8>,
degree: usize,
seed: u64,
) -> MultivariatePolynomial<Zp, u8> {
let mut coefficients = generated_dense_zp_coefficients(prototype.ring(), degree, seed);
coefficients.push(prototype.ring().one());
dense_zp_test_polynomial(prototype, &coefficients)
}
fn dense_zp_test_coefficients(
polynomial: &MultivariatePolynomial<Zp, u8>,
) -> Vec<FiniteFieldElement<u32>> {
if polynomial.is_zero() {
Vec::new()
} else {
DenseZpDistinctDegreeContext::<u8>::dense_coefficients(polynomial, 0, polynomial.ring())
}
}
fn generic_dense_zp_product(
prototype: &MultivariatePolynomial<Zp, u8>,
left: &[FiniteFieldElement<u32>],
right: &[FiniteFieldElement<u32>],
) -> Vec<FiniteFieldElement<u32>> {
let left = dense_zp_test_polynomial(prototype, left);
let right = dense_zp_test_polynomial(prototype, right);
dense_zp_test_coefficients(&(&left * &right))
}
fn generic_dense_zp_product_remainder(
prototype: &MultivariatePolynomial<Zp, u8>,
modulus: &MultivariatePolynomial<Zp, u8>,
left: &[FiniteFieldElement<u32>],
right: &[FiniteFieldElement<u32>],
) -> Vec<FiniteFieldElement<u32>> {
let left = dense_zp_test_polynomial(prototype, left);
let right = dense_zp_test_polynomial(prototype, right);
let product = &left * &right;
dense_zp_test_coefficients(&product.quot_rem_univariate_monic(modulus).1)
}
#[test]
fn dense_zp_low_product_and_square_match_generic_multiplication() {
use DenseZpAccumulationMode::{DirectMontgomeryReduction, NativeRemainder, WideRemainder};
let cases = [
(3, 9, DirectMontgomeryReduction),
(17, 9, DirectMontgomeryReduction),
(65_000_011, 66, DirectMontgomeryReduction),
(65_000_011, 67, NativeRemainder),
(500_000_003, 74, WideRemainder),
(4_294_967_291, 1, DirectMontgomeryReduction),
(4_294_967_291, 2, WideRemainder),
];
let mut workspace = DenseZpMulModWorkspace::new(255);
let mut actual = Vec::new();
for (prime, coefficient_count, expected_mode) in cases {
let field = Zp::new(prime);
let prototype = parse!("x").to_polynomial::<_, u8>(&field, None);
let maximum_raw = FiniteFieldElement::from_inner(prime - 1);
let left = vec![maximum_raw; coefficient_count];
let right = vec![maximum_raw; coefficient_count];
let product_len = 2 * coefficient_count - 1;
assert_eq!(
DenseZpDistinctDegreeContext::<u8>::accumulation_mode(
&field,
left.len(),
right.len(),
product_len,
),
expected_mode
);
let generic_product = generic_dense_zp_product(&prototype, &left, &right);
for output_len in [
product_len,
1,
0,
product_len - 1,
coefficient_count,
product_len,
] {
DenseZpDistinctDegreeContext::<u8>::multiply_low_into(
&field,
&left,
&right,
output_len,
&mut actual,
&mut workspace.u64_accumulators,
&mut workspace.u128_accumulators,
);
let mut expected = generic_product.clone();
expected.resize(output_len, field.zero());
assert_eq!(
actual, expected,
"prime {prime}, product prefix {output_len}"
);
}
for output_len in [
product_len,
1,
0,
product_len - 1,
coefficient_count,
product_len,
] {
DenseZpDistinctDegreeContext::<u8>::square_low_into(
&field,
&left,
output_len,
&mut actual,
&mut workspace.u64_accumulators,
&mut workspace.u128_accumulators,
);
let mut expected = generic_product.clone();
expected.resize(output_len, field.zero());
assert_eq!(
actual, expected,
"prime {prime}, square prefix {output_len}"
);
}
}
}
#[test]
fn dense_zp_reciprocal_reduction_matches_generic_remainder() {
for (prime, modulus_degree) in [
(3, 1),
(17, 9),
(65_000_011, 66),
(65_000_011, 67),
(4_294_967_291, 4),
] {
let field = Zp::new(prime);
let prototype = parse!("x").to_polynomial::<_, u8>(&field, None);
let modulus = generated_dense_zp_modulus(&prototype, modulus_degree, 1);
let mut context = DenseZpDistinctDegreeContext::new(&modulus, 0).unwrap();
let left = generated_dense_zp_coefficients(&field, modulus_degree, 2);
let right = generated_dense_zp_coefficients(&field, modulus_degree, 3);
let expected = generic_dense_zp_product_remainder(&prototype, &modulus, &left, &right);
let mut actual = Vec::new();
DenseZpDistinctDegreeContext::<u8>::multiply_mod_into(
&context.field,
&context.modulus,
&context.reverse_modulus_inverse,
&left,
&right,
&mut actual,
&mut context.multiplication_workspace,
);
assert_eq!(actual, expected, "prime {prime}, dense product remainder");
let expected_square =
generic_dense_zp_product_remainder(&prototype, &modulus, &left, &left);
DenseZpDistinctDegreeContext::<u8>::square_mod_into(
&context.field,
&context.modulus,
&context.reverse_modulus_inverse,
&left,
&mut actual,
&mut context.multiplication_workspace,
);
assert_eq!(
actual, expected_square,
"prime {prime}, dense square remainder"
);
}
let field = Zp::new(5);
let prototype = parse!("x").to_polynomial::<_, u8>(&field, None);
let modulus =
dense_zp_test_polynomial(&prototype, &[field.one(), field.zero(), field.one()]);
let mut context = DenseZpDistinctDegreeContext::new(&modulus, 0).unwrap();
let left = [field.to_element(3), field.one()];
let right = [field.to_element(2), field.one()];
let mut actual = Vec::new();
DenseZpDistinctDegreeContext::<u8>::multiply_mod_into(
&context.field,
&context.modulus,
&context.reverse_modulus_inverse,
&left,
&right,
&mut actual,
&mut context.multiplication_workspace,
);
assert!(actual.is_empty(), "(x-2)(x+2) must vanish modulo x^2+1");
let modulus = dense_zp_test_polynomial(
&prototype,
&[field.one(), field.zero(), field.zero(), field.one()],
);
context.set_modulus(&modulus);
let x_squared = [field.zero(), field.zero(), field.one()];
DenseZpDistinctDegreeContext::<u8>::multiply_mod_into(
&context.field,
&context.modulus,
&context.reverse_modulus_inverse,
&x_squared,
&x_squared,
&mut actual,
&mut context.multiplication_workspace,
);
assert_eq!(
actual,
[field.zero(), field.to_element(4)],
"x^4 modulo x^3+1 must retain a zero-padded quotient"
);
}
#[test]
fn dense_zp_reciprocal_refreshes_after_modulus_shrink() {
let field = Zp::new(17);
let prototype = parse!("x").to_polynomial::<_, u8>(&field, None);
let initial_modulus = generated_dense_zp_modulus(&prototype, 8, 1);
let mut context = DenseZpDistinctDegreeContext::new(&initial_modulus, 0).unwrap();
let old_residue = generated_dense_zp_coefficients(&field, 8, 2);
let mut output = Vec::new();
DenseZpDistinctDegreeContext::<u8>::square_mod_into(
&context.field,
&context.modulus,
&context.reverse_modulus_inverse,
&old_residue,
&mut output,
&mut context.multiplication_workspace,
);
let smaller_modulus = generated_dense_zp_modulus(&prototype, 3, 3);
context.set_modulus(&smaller_modulus);
assert_eq!(context.reverse_modulus_inverse.len(), 3);
let left = generated_dense_zp_coefficients(&field, 3, 4);
let right = generated_dense_zp_coefficients(&field, 3, 5);
let expected =
generic_dense_zp_product_remainder(&prototype, &smaller_modulus, &left, &right);
DenseZpDistinctDegreeContext::<u8>::multiply_mod_into(
&context.field,
&context.modulus,
&context.reverse_modulus_inverse,
&left,
&right,
&mut output,
&mut context.multiplication_workspace,
);
assert_eq!(output, expected);
let mut expected_modulus = smaller_modulus.clone();
let expected = dense_zp_test_coefficients(
&dense_zp_test_polynomial(&prototype, &old_residue)
.exp_mod_univariate(Integer::from(field.get_prime()), &mut expected_modulus),
);
let mut actual = old_residue;
context.raise_to_characteristic(&mut actual);
assert_eq!(actual, expected);
}
#[test]
fn dense_zp_frobenius_map_survives_factor_modulus_shrink() {
let field = Zp::new(65_000_011);
let prototype = parse!("x").to_polynomial::<_, u8>(&field, None);
let left_factor = generated_dense_zp_modulus(&prototype, 9, 1);
let right_factor = generated_dense_zp_modulus(&prototype, 7, 2);
let modulus = &left_factor * &right_factor;
let mut context = DenseZpDistinctDegreeContext::new(&modulus, 0).unwrap();
let mut x_to_p = vec![field.zero(), field.one()];
context.raise_to_characteristic(&mut x_to_p);
let mut map = DenseZpFrobeniusContext::new::<u8>(
&context.field,
&context.modulus,
&context.reverse_modulus_inverse,
&x_to_p,
&mut context.multiplication_workspace,
);
let value = generated_dense_zp_coefficients(&field, 16, 3);
let mut expected = value.clone();
context.raise_to_characteristic(&mut expected);
let mut actual = Vec::new();
map.apply::<u8>(&field, &value, &mut actual);
assert_eq!(actual, expected);
context.set_modulus(&left_factor);
let mut reduced_value = value;
DenseZpDistinctDegreeContext::<u8>::remainder_monic(
&field,
&mut reduced_value,
&context.modulus,
);
expected.clone_from(&reduced_value);
context.raise_to_characteristic(&mut expected);
map.apply::<u8>(&field, &reduced_value, &mut actual);
DenseZpDistinctDegreeContext::<u8>::remainder_monic(&field, &mut actual, &context.modulus);
assert_eq!(actual, expected);
}
#[test]
fn dense_zp_frobenius_map_matches_all_accumulator_modes() {
use DenseZpAccumulationMode::{DirectMontgomeryReduction, NativeRemainder, WideRemainder};
for (prime, degree, expected_mode) in [
(65_000_011, 16, DirectMontgomeryReduction),
(65_000_011, 67, NativeRemainder),
(500_000_003, 74, WideRemainder),
] {
let field = Zp::new(prime);
assert_eq!(
DenseZpDistinctDegreeContext::<u8>::accumulation_mode_for_terms(&field, degree),
expected_mode
);
let prototype = parse!("x").to_polynomial::<_, u8>(&field, None);
let modulus = generated_dense_zp_modulus(&prototype, degree, 1);
let mut context = DenseZpDistinctDegreeContext::new(&modulus, 0).unwrap();
let mut x_to_p = vec![field.zero(), field.one()];
context.raise_to_characteristic(&mut x_to_p);
let mut map = DenseZpFrobeniusContext::new::<u8>(
&context.field,
&context.modulus,
&context.reverse_modulus_inverse,
&x_to_p,
&mut context.multiplication_workspace,
);
let value = generated_dense_zp_coefficients(&field, degree, 2);
let mut expected = value.clone();
context.raise_to_characteristic(&mut expected);
let mut actual = Vec::new();
map.apply::<u8>(&field, &value, &mut actual);
assert_eq!(actual, expected, "prime {prime}, degree {degree}");
}
}
#[test]
fn dense_zp_frobenius_cache_requires_next_step_break_even() {
let small_field = Zp::new(5);
let small_prototype = parse!("x").to_polynomial::<_, u8>(&small_field, None);
let small_modulus = generated_dense_zp_modulus(&small_prototype, 64, 1);
let small_context = DenseZpDistinctDegreeContext::new(&small_modulus, 0).unwrap();
assert_eq!(small_context.characteristic_power_product_count(), 3);
assert!(!small_context.should_cache_frobenius(25, 64));
let wide_field = Zp::new(65_000_011);
let wide_prototype = parse!("x").to_polynomial::<_, u8>(&wide_field, None);
let wide_modulus = generated_dense_zp_modulus(&wide_prototype, 46, 1);
let wide_context = DenseZpDistinctDegreeContext::new(&wide_modulus, 0).unwrap();
assert_eq!(wide_context.characteristic_power_product_count(), 41);
assert!(!wide_context.should_cache_frobenius(6, 46));
assert!(wide_context.should_cache_frobenius(8, 30));
}
#[test]
fn dense_zp_equal_degree_power_matches_generic_remainder() {
let field = Zp::new(17);
let prototype = parse!("x").to_polynomial::<_, u8>(&field, None);
let modulus = generated_dense_zp_modulus(&prototype, 30, 1);
let base = generated_dense_zp_coefficients(&field, 30, 2);
let mut context = DenseZpEqualDegreeContext::new(&modulus, 0, 10)
.expect("the degree-30 dense block must use the bounded workspace");
context.power_to_half_group_order(&base);
let characteristic = Integer::from(field.get_prime());
let exponent = (&characteristic.pow(10) - &Integer::one()) / &Integer::from(2);
let base_polynomial = dense_zp_test_polynomial(&prototype, &base);
let mut generic_modulus = modulus.clone();
let expected = dense_zp_test_coefficients(
&base_polynomial.exp_mod_univariate(exponent, &mut generic_modulus),
);
assert_eq!(context.result, expected);
}
#[test]
fn dense_zp_equal_degree_factorization_matches_generic_degree_64_block() {
let polynomial = parse!("((1+3*x)^33-1)*((1-5*x)^31+1)")
.expand()
.to_polynomial::<_, u8>(&Z, None);
let (_, cofactor) = polynomial
.remove_univariate_monomial_factor(0)
.expect("the degree-64 input has an exact factor x");
let Some(ModularPrimeScreen::Candidate(candidate)) =
cofactor.screen_univariate_mod_prime(0, 17, None)
else {
panic!("prime 17 must produce the retained modular image");
};
let (_, block) = candidate
.distinct_degree
.blocks
.into_iter()
.find(|(degree, _)| *degree == 10)
.expect("the retained image must have a degree-10 block");
assert_eq!(block.degree(0), 30);
let mut expected = block.equal_degree_factorization(10);
DENSE_ZP_EDF_BLOCKS.with(|blocks| blocks.set(0));
let mut actual = DenseZpEqualDegreeContext::factor(&block, 0, 10)
.expect("the retained block must use dense equal-degree factorization");
expected.sort_by(|left, right| left.internal_cmp(right));
actual.sort_by(|left, right| left.internal_cmp(right));
assert_eq!(actual, expected);
DENSE_ZP_EDF_BLOCKS.with(|blocks| assert_eq!(blocks.get(), 1));
let reconstructed = actual
.iter()
.fold(block.one(), |product, factor| &product * factor);
assert_eq!(reconstructed, block);
}
#[test]
fn bounded_distinct_degree_factorization_reports_exact_factor_counts() {
let field = Zp::new(11);
let one = parse!("1").to_polynomial::<_, u8>(&field, None);
let bounded_one = one.distinct_degree_factorization_bounded(Some(0)).unwrap();
assert_eq!(bounded_one.factor_count, 0);
assert!(bounded_one.blocks.is_empty());
assert_eq!(one.distinct_degree_factorization(), vec![(0, one.clone())]);
let polynomial = parse!("x*(x+1)*(x^2+1)")
.expand()
.to_polynomial::<_, u8>(&field, None);
let complete = polynomial
.distinct_degree_factorization_bounded(None)
.unwrap();
assert_eq!(complete.factor_count, 3);
assert_eq!(complete.blocks.len(), 2);
let reconstructed = complete
.blocks
.iter()
.fold(polynomial.one(), |product, (_, block)| &product * block);
assert_eq!(reconstructed, polynomial);
let limited = polynomial.distinct_degree_factorization_bounded(Some(2));
let Err(lower_bound) = limited else {
panic!("a two-factor limit must reject three irreducible factors");
};
assert_eq!(lower_bound, 3);
let admitted = polynomial
.distinct_degree_factorization_bounded(Some(3))
.unwrap();
assert_eq!(admitted.factor_count, complete.factor_count);
assert_eq!(admitted.blocks, complete.blocks);
}
#[test]
fn dense_zp_distinct_degree_factorization_matches_generic_path() {
let variables = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("y").into(),
symbol!("z").into(),
]));
let cases = [
(3, "(y+1)*(y^2+1)*(y^3+2*y+1)"),
(5, "(y+2)*(y^2+2)*(y^3+y+1)"),
(17, "(y+1)*(y+2)*(y+3)*(y^2+3)"),
(65_000_011, "(y+1)*(y+2)*(y+3)*(y^2+3)"),
(4_294_967_291, "(y+1)*(y^2+1)"),
];
for (prime, expression) in cases {
DENSE_ZP_DDF_MODULUS_UPDATES.with(|updates| updates.set(0));
let field = Zp::new(prime);
let polynomial = parse!(expression)
.expand()
.to_polynomial::<_, u8>(&field, variables.clone());
assert!(
polynomial
.univariate_gcd(&polynomial.derivative(1))
.is_one()
);
let generic = polynomial
.distinct_degree_factorization_bounded(None)
.unwrap();
let dense = DenseZpDistinctDegreeContext::new(&polynomial, 1)
.expect("the dense small-prime case must be selected")
.factor(None)
.unwrap();
assert_eq!(dense.factor_count, generic.factor_count);
assert_eq!(dense.blocks, generic.blocks);
if prime == 3 {
assert_eq!(
dense
.blocks
.iter()
.map(|(degree, _)| *degree)
.collect::<Vec<_>>(),
[1, 2, 3]
);
DENSE_ZP_DDF_MODULUS_UPDATES.with(|updates| assert!(updates.get() > 0));
}
let reconstructed = dense
.blocks
.iter()
.fold(polynomial.one(), |product, (_, block)| &product * block);
assert_eq!(reconstructed, polynomial);
for limit in 0..=generic.factor_count {
let generic = polynomial.distinct_degree_factorization_bounded(Some(limit));
let dense = DenseZpDistinctDegreeContext::new(&polynomial, 1)
.unwrap()
.factor(Some(limit));
match (generic, dense) {
(Ok(generic), Ok(dense)) => {
assert_eq!(dense.factor_count, generic.factor_count);
assert_eq!(dense.blocks, generic.blocks);
}
(Err(generic), Err(dense)) => assert_eq!(dense, generic),
_ => panic!("dense and generic bounded DDF outcomes differ"),
}
}
}
}
#[test]
fn sparse_modular_screen_keeps_generic_distinct_degree_path() {
DENSE_ZP_DDF_SCREENS.with(|screens| screens.set(0));
let polynomial = parse!("1+x+x^17").to_polynomial::<_, u8>(&Z, None);
let field = Zp::new(5);
let image = polynomial
.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
)
.make_monic();
assert!(DenseZpDistinctDegreeContext::new(&image, 0).is_none());
let screen = polynomial.screen_univariate_mod_prime(0, 5, None);
assert!(matches!(screen, Some(ModularPrimeScreen::Candidate(_))));
DENSE_ZP_DDF_SCREENS.with(|screens| assert_eq!(screens.get(), 0));
}
#[test]
fn degree_64_modular_screening_counts_and_rejects_early() {
DENSE_ZP_DDF_SCREENS.with(|screens| screens.set(0));
let polynomial = parse!("((1+3*x)^33-1)*((1-5*x)^31+1)")
.expand()
.to_polynomial::<_, u8>(&Z, None);
let (_, cofactor) = polynomial
.remove_univariate_monomial_factor(0)
.expect("the degree-64 input has an exact factor x");
assert!(cofactor.screen_univariate_mod_prime(0, 11, None).is_none());
for (prime, expected_count) in [(7, 8), (13, 7), (17, 6), (65_000_011, 19)] {
let Some(ModularPrimeScreen::Candidate(candidate)) =
cofactor.screen_univariate_mod_prime(0, prime, None)
else {
panic!("prime {prime} must produce a suitable modular image");
};
assert_eq!(candidate.distinct_degree.factor_count, expected_count);
}
LAST_BOUNDED_DDF_REJECTION_DEGREE.with(|degree| degree.set(0));
let Some(ModularPrimeScreen::FactorLimitExceeded { lower_bound }) =
cofactor.screen_univariate_mod_prime(0, 65_000_011, Some(8))
else {
panic!("the large-prime image must exceed the eight-factor limit");
};
assert_eq!(lower_bound, 19);
LAST_BOUNDED_DDF_REJECTION_DEGREE.with(|degree| assert_eq!(degree.get(), 2));
DENSE_ZP_DDF_SCREENS.with(|screens| assert_eq!(screens.get(), 5));
}
#[test]
fn factor_univariate_degree_63_bounds_third_small_image_before_selecting_wide_prime() {
PRODUCT_TREE_HENSEL_LIFT_CALLS.with(|calls| calls.set(0));
PRODUCT_TREE_EARLY_RECONSTRUCTION_ATTEMPTS.with(|attempts| attempts.set(0));
COMPETITIVE_SMALL_PRIME_DDF_REJECTIONS.with(|rejections| rejections.set(0));
let polynomial = parse!("((1+3*x)^32-1)*((1-5*x)^31+1)")
.expand()
.to_polynomial::<_, u8>(&Z, None);
let primitive = polynomial.clone().make_primitive();
let (_, factor_target) = primitive
.remove_univariate_monomial_factor(0)
.expect("the degree-63 input has an exact factor x");
assert_eq!(factor_target.degree(0), 62);
let bound = factor_target.coefficient_bound();
let Some(ModularPrimeScreen::Candidate(small_candidate)) =
factor_target.screen_univariate_mod_prime(0, 11, None)
else {
panic!("prime 11 must produce a suitable modular image");
};
let small_factor_count = small_candidate.distinct_degree.factor_count;
let small_digits =
MultivariatePolynomial::<IntegerRing, u8>::linear_hensel_modulus(&bound, 11).0;
assert_eq!(small_factor_count, 10);
assert!(
MultivariatePolynomial::<IntegerRing, u8>::has_high_linear_hensel_pressure(
62,
&bound,
small_factor_count,
small_digits
)
);
let Some(ModularPrimeScreen::Candidate(final_candidate)) =
factor_target.screen_univariate_mod_prime(0, 65_000_011, None)
else {
panic!("the dense-u64 prime must produce a suitable modular image");
};
let final_factor_count = final_candidate.distinct_degree.factor_count;
let final_digits =
MultivariatePolynomial::<IntegerRing, u8>::linear_hensel_modulus(&bound, 65_000_011).0;
assert_eq!(final_factor_count, 10);
assert_eq!(final_digits, 12);
assert!(
!MultivariatePolynomial::<IntegerRing, u8>::has_high_linear_hensel_pressure(
62,
&bound,
final_factor_count,
final_digits
)
);
let third_digits =
MultivariatePolynomial::<IntegerRing, u8>::linear_hensel_modulus(&bound, 29).0;
let ordinary_limit = (MultivariatePolynomial::<IntegerRing, u8>::linear_hensel_work(
small_factor_count,
small_digits,
) - 1)
/ third_digits
+ 1;
let competitive_limit =
MultivariatePolynomial::<IntegerRing, u8>::competitive_small_prime_factor_limit(
factor_target.degree(0) as usize,
small_factor_count,
small_digits,
final_factor_count,
final_digits,
third_digits,
);
assert!(competitive_limit < ordinary_limit);
let Some(ModularPrimeScreen::FactorLimitExceeded { lower_bound }) =
factor_target.screen_univariate_mod_prime(0, 29, Some(competitive_limit))
else {
panic!("prime 29 must exceed the competitive factor limit");
};
assert!(lower_bound > competitive_limit);
DENSE_ZP_DDF_SCREENS.with(|screens| screens.set(0));
DENSE_ZP_FROBENIUS_MAP_BUILDS.with(|builds| builds.set(0));
LAST_MODULAR_INTEGER_EDF_PRIME.with(|prime| prime.set(0));
let factors = polynomial.factor();
let reconstructed = factors
.iter()
.fold(polynomial.one(), |product, (factor, power)| {
&product * &factor.pow(*power)
});
assert_eq!(reconstructed, polynomial);
let mut degrees = factors
.iter()
.filter(|(factor, _)| !factor.is_constant())
.map(|(factor, power)| {
assert_eq!(*power, 1);
factor.degree(0)
})
.collect::<Vec<_>>();
degrees.sort_unstable();
assert_eq!(degrees, [1u8, 1, 1, 2, 4, 8, 16, 30]);
COMPETITIVE_SMALL_PRIME_DDF_REJECTIONS.with(|rejections| assert_eq!(rejections.get(), 1));
DENSE_ZP_DDF_SCREENS.with(|screens| assert_eq!(screens.get(), 4));
DENSE_ZP_FROBENIUS_MAP_BUILDS.with(|builds| assert_eq!(builds.get(), 1));
LAST_MODULAR_INTEGER_EDF_PRIME.with(|prime| assert_eq!(prime.get(), 65_000_011));
PRODUCT_TREE_HENSEL_LIFT_CALLS.with(|calls| assert_eq!(calls.get(), 0));
PRODUCT_TREE_EARLY_RECONSTRUCTION_ATTEMPTS.with(|attempts| assert_eq!(attempts.get(), 0));
}
#[test]
fn factor_univariate_degree_64_defers_discarded_equal_degree_factorization() {
MODULAR_INTEGER_EDF_CALLS.with(|calls| calls.set(0));
DENSE_ZP_EDF_BLOCKS.with(|blocks| blocks.set(0));
BOUNDED_DDF_REJECTIONS.with(|rejections| rejections.set(0));
LAST_MODULAR_INTEGER_EDF_PRIME.with(|prime| prime.set(0));
PRODUCT_TREE_HENSEL_LIFT_CALLS.with(|calls| calls.set(0));
PRODUCT_TREE_EARLY_RECONSTRUCTION_ATTEMPTS.with(|attempts| attempts.set(0));
PRODUCT_TREE_EARLY_RECONSTRUCTION_SUCCESSES.with(|successes| successes.set(0));
PRODUCT_TREE_EARLY_RECONSTRUCTION_EXPONENT.with(|exponent| exponent.set(0));
PRODUCT_TREE_LAST_BEZOUT_UPDATE_EXPONENT.with(|exponent| exponent.set(0));
PRODUCT_TREE_BALANCED_PAIR_ATTEMPTS.with(|attempts| attempts.set(0));
GEOMETRIC_SMALL_PRIME_BACKFILLS.with(|backfills| backfills.set(0));
COMPETITIVE_SMALL_PRIME_DDF_REJECTIONS.with(|rejections| rejections.set(0));
let polynomial = parse!("((1+3*x)^33-1)*((1-5*x)^31+1)")
.expand()
.to_polynomial::<_, u8>(&Z, None);
let (monomial_power, cofactor) = polynomial
.remove_univariate_monomial_factor(0)
.expect("the degree-64 input has an exact factor x");
assert_eq!(monomial_power, 1);
assert_eq!(cofactor.degree(0), 63);
assert_eq!(cofactor.clone().mul_exp(&[monomial_power]), polynomial);
assert!(cofactor.screen_univariate_mod_prime(0, 11, None).is_none());
let Some(ModularPrimeScreen::Candidate(prime_17_candidate)) =
cofactor.screen_univariate_mod_prime(0, 17, None)
else {
panic!("prime 17 must give the monomial-free cofactor a suitable image");
};
assert_eq!(prime_17_candidate.distinct_degree.factor_count, 6);
let factors = polynomial.factor();
let reconstructed = factors
.iter()
.fold(polynomial.one(), |product, (factor, power)| {
&product * &factor.pow(*power)
});
assert_eq!(reconstructed, polynomial);
let mut degrees = factors
.iter()
.filter(|(factor, _)| !factor.is_constant())
.map(|(factor, power)| {
assert_eq!(*power, 1);
factor.degree(0)
})
.collect::<Vec<_>>();
degrees.sort_unstable();
assert_eq!(degrees, [1u8, 1, 2, 10, 20, 30]);
MODULAR_INTEGER_EDF_CALLS.with(|calls| assert_eq!(calls.get(), 1));
DENSE_ZP_EDF_BLOCKS.with(|blocks| assert_eq!(blocks.get(), 1));
BOUNDED_DDF_REJECTIONS.with(|rejections| assert_eq!(rejections.get(), 0));
LAST_MODULAR_INTEGER_EDF_PRIME.with(|prime| assert_eq!(prime.get(), 17));
PRODUCT_TREE_HENSEL_LIFT_CALLS.with(|calls| assert_eq!(calls.get(), 1));
PRODUCT_TREE_EARLY_RECONSTRUCTION_ATTEMPTS.with(|attempts| assert_eq!(attempts.get(), 1));
PRODUCT_TREE_EARLY_RECONSTRUCTION_SUCCESSES
.with(|successes| assert_eq!(successes.get(), 1));
PRODUCT_TREE_EARLY_RECONSTRUCTION_EXPONENT.with(|exponent| assert_eq!(exponent.get(), 39));
PRODUCT_TREE_LAST_BEZOUT_UPDATE_EXPONENT.with(|exponent| assert_eq!(exponent.get(), 20));
PRODUCT_TREE_BALANCED_PAIR_ATTEMPTS.with(|attempts| assert_eq!(attempts.get(), 0));
GEOMETRIC_SMALL_PRIME_BACKFILLS.with(|backfills| assert_eq!(backfills.get(), 0));
COMPETITIVE_SMALL_PRIME_DDF_REJECTIONS.with(|rejections| assert_eq!(rejections.get(), 0));
}
#[test]
fn factor_univariate_degree_65_backfills_geometric_prime_gap() {
DENSE_INTEGER_I128_MULTIPLY_REMAINDERS.with(|operations| operations.set(0));
DENSE_ZP_DDF_SCREENS.with(|screens| screens.set(0));
BOUNDED_DDF_REJECTIONS.with(|rejections| rejections.set(0));
LAST_BOUNDED_DDF_REJECTION_DEGREE.with(|degree| degree.set(0));
GEOMETRIC_SMALL_PRIME_BACKFILLS.with(|backfills| backfills.set(0));
COMPETITIVE_SMALL_PRIME_DDF_REJECTIONS.with(|rejections| rejections.set(0));
LAST_MODULAR_INTEGER_EDF_PRIME.with(|prime| prime.set(0));
PRODUCT_TREE_BALANCED_PAIR_ATTEMPTS.with(|attempts| attempts.set(0));
PRODUCT_TREE_BALANCED_PAIR_CERTIFICATES.with(|certificates| certificates.set(0));
PRODUCT_TREE_BALANCED_PAIR_TARGET_EXPONENT.with(|exponent| exponent.set(0));
let polynomial = parse!("((1+3*x)^33-1)*((1-5*x)^32+1)")
.expand()
.to_polynomial::<_, u8>(&Z, None);
let factors = polynomial.factor();
let reconstructed = factors
.iter()
.fold(polynomial.one(), |product, (factor, power)| {
&product * &factor.pow(*power)
});
assert_eq!(reconstructed, polynomial);
let mut degrees = factors
.iter()
.filter(|(factor, _)| !factor.is_constant())
.map(|(factor, power)| {
assert_eq!(*power, 1);
factor.degree(0)
})
.collect::<Vec<_>>();
degrees.sort_unstable();
assert_eq!(degrees, [1u8, 2, 10, 20, 32]);
DENSE_ZP_DDF_SCREENS.with(|screens| assert_eq!(screens.get(), 3));
BOUNDED_DDF_REJECTIONS.with(|rejections| assert_eq!(rejections.get(), 1));
LAST_BOUNDED_DDF_REJECTION_DEGREE.with(|degree| assert_eq!(degree.get(), 4));
GEOMETRIC_SMALL_PRIME_BACKFILLS.with(|backfills| assert!(backfills.get() > 0));
COMPETITIVE_SMALL_PRIME_DDF_REJECTIONS.with(|rejections| assert_eq!(rejections.get(), 0));
LAST_MODULAR_INTEGER_EDF_PRIME.with(|prime| assert_eq!(prime.get(), 13));
PRODUCT_TREE_BALANCED_PAIR_ATTEMPTS.with(|attempts| assert_eq!(attempts.get(), 1));
PRODUCT_TREE_BALANCED_PAIR_CERTIFICATES
.with(|certificates| assert_eq!(certificates.get(), 1));
PRODUCT_TREE_BALANCED_PAIR_TARGET_EXPONENT.with(|exponent| assert_eq!(exponent.get(), 51));
DENSE_INTEGER_I128_MULTIPLY_REMAINDERS
.with(|operations| assert!((128..=130).contains(&operations.get())));
}
#[test]
fn univariate_monomial_extraction_uses_the_full_power_and_active_variable() {
let variables = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("y").into(),
symbol!("z").into(),
]));
let polynomial = parse!("-3*y^4").to_polynomial::<_, u16>(&Z, variables.clone());
let (power, cofactor) = polynomial
.remove_univariate_monomial_factor(1)
.expect("every term contains y^4");
assert_eq!(power, 4);
assert_eq!(cofactor, polynomial.constant(Integer::from(-3)));
assert!(cofactor.remove_univariate_monomial_factor(1).is_none());
let mut shift = vec![0u16; polynomial.nvars()];
shift[1] = power;
assert_eq!(cofactor.clone().mul_exp(&shift), polynomial);
for expression in ["7", "-7"] {
let constant = parse!(expression).to_polynomial::<_, u16>(&Z, variables.clone());
assert!(constant.remove_univariate_monomial_factor(1).is_none());
assert_eq!(constant.factor_reconstruct(), vec![constant.clone()]);
}
let variable = parse!("y").to_polynomial::<_, u16>(&Z, variables);
let factors = polynomial.factor_reconstruct();
assert_eq!(
factors.iter().filter(|factor| **factor == variable).count(),
4
);
assert_eq!(
factors.iter().filter(|factor| factor.is_constant()).count(),
1
);
assert_eq!(
factors
.iter()
.fold(polynomial.one(), |product, factor| &product * factor),
polynomial
);
}
#[test]
fn extracted_variable_factor_inherits_square_free_multiplicity_and_sign() {
let variables = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("y").into(),
symbol!("z").into(),
]));
let polynomial = parse!("-6*(y*(y^2+1))^3")
.expand()
.to_polynomial::<_, u16>(&Z, variables.clone());
let mut expected = [("-6", 1), ("y", 3), ("y^2+1", 3)]
.into_iter()
.map(|(factor, multiplicity)| {
(
parse!(factor).to_polynomial::<_, u16>(&Z, variables.clone()),
multiplicity,
)
})
.collect::<Vec<_>>();
expected.sort_by(|left, right| left.0.internal_cmp(&right.0).then(left.1.cmp(&right.1)));
let mut factors = polynomial.factor();
factors.sort_by(|left, right| left.0.internal_cmp(&right.0).then(left.1.cmp(&right.1)));
assert_eq!(factors, expected);
assert_eq!(
factors
.iter()
.fold(polynomial.one(), |product, (factor, multiplicity)| {
&product * &factor.pow(*multiplicity)
}),
polynomial
);
}
#[test]
fn quadratic_hensel_lift_handles_partial_final_precision() {
QUADRATIC_HENSEL_NONUNIT_RETRIES.with(|retries| retries.set(0));
let variables = Some(Arc::new(vec![symbol!("x").into()]));
let left = parse!("37+19*x+6*x^2").to_polynomial::<_, u8>(&Z, variables.clone());
let right = parse!("29-13*x+5*x^2+3*x^3").to_polynomial::<_, u8>(&Z, variables.clone());
let product = &left * &right;
let field = Zp::new(11);
let left_mod = left.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
);
let right_mod = right.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
);
let max_p = Integer::from(11).pow(65);
let quadratic = product
.hensel_lift_with_strategy(left_mod.clone(), right_mod.clone(), None, &max_p, true)
.unwrap();
let linear = product
.hensel_lift_with_strategy(left_mod, right_mod, None, &max_p, false)
.unwrap();
assert_eq!(&quadratic.0 * &quadratic.1, product);
assert_eq!(quadratic, linear);
QUADRATIC_HENSEL_NONUNIT_RETRIES.with(|retries| assert_eq!(retries.get(), 0));
}
#[test]
fn quadratic_hensel_lift_preserves_unsuccessful_congruence() {
QUADRATIC_HENSEL_NONUNIT_RETRIES.with(|retries| retries.set(0));
let variables = Some(Arc::new(vec![symbol!("x").into()]));
let polynomial = parse!("1+x^2").to_polynomial::<_, u8>(&Z, variables.clone());
let field = Zp::new(5);
let left = parse!("x-2").to_polynomial::<_, u8>(&field, variables.clone());
let right = parse!("x+2").to_polynomial::<_, u8>(&field, variables);
let max_p = Integer::from(5).pow(65);
let quadratic = polynomial
.hensel_lift_with_strategy(left.clone(), right.clone(), None, &max_p, true)
.unwrap_err();
let linear = polynomial
.hensel_lift_with_strategy(left, right, None, &max_p, false)
.unwrap_err();
let normalize = |factor: MultivariatePolynomial<IntegerRing, u8>| {
factor.map_coeff(|coefficient| coefficient.clone().symmetric_mod(&max_p), Z)
};
let quadratic = (normalize(quadratic.0), normalize(quadratic.1));
let linear = (normalize(linear.0), normalize(linear.1));
let error = &polynomial - &(&quadratic.0 * &quadratic.1);
assert_eq!(quadratic, linear);
assert!(
error
.coefficients
.iter()
.all(|coefficient| (coefficient % &max_p).is_zero())
);
QUADRATIC_HENSEL_NONUNIT_RETRIES.with(|retries| assert_eq!(retries.get(), 0));
}
#[test]
fn linear_hensel_lift_handles_base_prime_precision() {
let variables = Some(Arc::new(vec![symbol!("x").into()]));
let field = Zp::new(5);
let left = parse!("x-2").to_polynomial::<_, u8>(&field, variables.clone());
let right = parse!("x+2").to_polynomial::<_, u8>(&field, variables.clone());
let max_p = Integer::from(5);
let exact = parse!("x^2-4").to_polynomial::<_, u8>(&Z, variables.clone());
let exact_lift = exact
.hensel_lift_with_strategy(left.clone(), right.clone(), None, &max_p, false)
.unwrap();
assert_eq!(&exact_lift.0 * &exact_lift.1, exact);
let inexact = parse!("1+x^2").to_polynomial::<_, u8>(&Z, variables);
let inexact_lift = inexact
.hensel_lift_with_strategy(left, right, None, &max_p, false)
.unwrap_err();
let error = &inexact - &(&inexact_lift.0 * &inexact_lift.1);
assert!(
error
.coefficients
.iter()
.all(|coefficient| (coefficient % &max_p).is_zero())
);
}
#[test]
fn hensel_lift_handles_binary_prime_and_nontrivial_gamma() {
let variables = Some(Arc::new(vec![symbol!("x").into()]));
let binary_left = parse!("x+5").to_polynomial::<_, u8>(&Z, variables.clone());
let binary_right = parse!("x^2+3*x+3").to_polynomial::<_, u8>(&Z, variables.clone());
let binary_product = &binary_left * &binary_right;
let binary_field = Z2;
let binary_left_mod = binary_left.map_coeff(
|coefficient| coefficient.to_finite_field(&binary_field),
binary_field.clone(),
);
let binary_right_mod = binary_right.map_coeff(
|coefficient| coefficient.to_finite_field(&binary_field),
binary_field.clone(),
);
let binary_lift = binary_product
.hensel_lift_with_strategy(
binary_left_mod,
binary_right_mod,
None,
&Integer::from(2).pow(20),
true,
)
.unwrap();
assert_eq!(&binary_lift.0 * &binary_lift.1, binary_product);
QUADRATIC_HENSEL_LIFT_CALLS.with(|calls| calls.set(0));
QUADRATIC_HENSEL_NONUNIT_RETRIES.with(|retries| retries.set(0));
let nonmonic_left = parse!("2*x+131").to_polynomial::<_, u8>(&Z, variables.clone());
let nonmonic_right = parse!("3*x+127").to_polynomial::<_, u8>(&Z, variables);
let nonmonic_product = &nonmonic_left * &nonmonic_right;
let nonmonic_field = Zp::new(5);
let nonmonic_left_mod = nonmonic_left.map_coeff(
|coefficient| coefficient.to_finite_field(&nonmonic_field),
nonmonic_field.clone(),
);
let nonmonic_right_mod = nonmonic_right.map_coeff(
|coefficient| coefficient.to_finite_field(&nonmonic_field),
nonmonic_field.clone(),
);
let quadratic_nonmonic_lift = nonmonic_product
.hensel_lift_with_strategy(
nonmonic_left_mod.clone(),
nonmonic_right_mod.clone(),
Some(Integer::from(2)),
&Integer::from(5).pow(65),
true,
)
.unwrap();
let linear_nonmonic_lift = nonmonic_product
.hensel_lift_with_strategy(
nonmonic_left_mod,
nonmonic_right_mod,
Some(Integer::from(2)),
&Integer::from(5).pow(65),
false,
)
.unwrap();
assert_eq!(quadratic_nonmonic_lift, linear_nonmonic_lift);
assert_eq!(
&quadratic_nonmonic_lift.0 * &quadratic_nonmonic_lift.1,
nonmonic_product
);
QUADRATIC_HENSEL_LIFT_CALLS.with(|calls| assert!(calls.get() > 0));
QUADRATIC_HENSEL_NONUNIT_RETRIES.with(|retries| assert_eq!(retries.get(), 0));
}
#[test]
fn exact_hensel_subtrees_use_local_moduli_and_recombine_inexact_children() {
EXACT_HENSEL_SUBTREE_SPLITS.with(|splits| splits.set(0));
LOCAL_HENSEL_RECOMBINATION_NODES.with(|nodes| nodes.set(0));
EXACT_HENSEL_SUBTREE_MODULUS_BITS.with(|bits| bits.borrow_mut().clear());
let variables = Some(Arc::new(vec![symbol!("x").into()]));
let integer_factors = [
parse!("x^2+100").to_polynomial::<_, u8>(&Z, variables.clone()),
parse!("x^2+97").to_polynomial::<_, u8>(&Z, variables.clone()),
parse!("x^2+92").to_polynomial::<_, u8>(&Z, variables.clone()),
parse!("x^2+85").to_polynomial::<_, u8>(&Z, variables.clone()),
];
let polynomial = integer_factors
.iter()
.fold(integer_factors[0].one(), |product, factor| {
&product * factor
});
// The first four modular factors multiply to the first two exact quadratics and the
// remaining four multiply to the other two. Within each four-factor child, the first
// split crosses the two irreducible quadratics and is therefore only a congruence.
let field = Zp::new(101);
let modular_factors = vec![
parse!("x-1").to_polynomial::<_, u8>(&field, variables.clone()),
parse!("x-2").to_polynomial::<_, u8>(&field, variables.clone()),
parse!("x+1").to_polynomial::<_, u8>(&field, variables.clone()),
parse!("x+2").to_polynomial::<_, u8>(&field, variables.clone()),
parse!("x-3").to_polynomial::<_, u8>(&field, variables.clone()),
parse!("x-4").to_polynomial::<_, u8>(&field, variables.clone()),
parse!("x+3").to_polynomial::<_, u8>(&field, variables.clone()),
parse!("x+4").to_polynomial::<_, u8>(&field, variables),
];
let bound = polynomial.coefficient_bound();
let (_, max_p) =
MultivariatePolynomial::<IntegerRing, u8>::linear_hensel_modulus(&bound, 101);
let mut factors =
polynomial.factor_hensel_subtree(&modular_factors, &max_p, &bound, 0, false);
let mut expected = integer_factors.to_vec();
factors.sort_by(|left, right| left.internal_cmp(right));
expected.sort_by(|left, right| left.internal_cmp(right));
assert_eq!(factors, expected);
EXACT_HENSEL_SUBTREE_SPLITS.with(|splits| assert_eq!(splits.get(), 1));
LOCAL_HENSEL_RECOMBINATION_NODES.with(|nodes| assert_eq!(nodes.get(), 2));
EXACT_HENSEL_SUBTREE_MODULUS_BITS.with(|bits| {
let bits = bits.borrow();
assert_eq!(bits.len(), 3);
assert!(bits[1] < bits[0], "left child did not lower its modulus");
assert!(bits[2] < bits[0], "right child did not lower its modulus");
});
}
#[test]
fn exact_hensel_subtrees_handle_nonmonic_children_at_base_prime_precision() {
EXACT_HENSEL_SUBTREE_SPLITS.with(|splits| splits.set(0));
LOCAL_HENSEL_RECOMBINATION_NODES.with(|nodes| nodes.set(0));
EXACT_HENSEL_SUBTREE_MODULUS_BITS.with(|bits| bits.borrow_mut().clear());
let variables = Some(Arc::new(vec![symbol!("x").into()]));
let integer_factors = [
parse!("2*x+1").to_polynomial::<_, u8>(&Z, variables.clone()),
parse!("x+1").to_polynomial::<_, u8>(&Z, variables.clone()),
parse!("x+2").to_polynomial::<_, u8>(&Z, variables.clone()),
parse!("x+3").to_polynomial::<_, u8>(&Z, variables.clone()),
];
let polynomial = integer_factors
.iter()
.fold(integer_factors[0].one(), |product, factor| {
&product * factor
});
let field = Zp::new(101);
let modular_factors = integer_factors
.iter()
.map(|factor| {
factor
.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
)
.make_monic()
})
.collect::<Vec<_>>();
let bound = polynomial.coefficient_bound();
let (_, max_p) =
MultivariatePolynomial::<IntegerRing, u8>::linear_hensel_modulus(&bound, 101);
let mut factors =
polynomial.factor_hensel_subtree(&modular_factors, &max_p, &bound, 0, true);
let mut expected = integer_factors.to_vec();
factors.sort_by(|left, right| left.internal_cmp(right));
expected.sort_by(|left, right| left.internal_cmp(right));
assert_eq!(factors, expected);
EXACT_HENSEL_SUBTREE_SPLITS.with(|splits| assert_eq!(splits.get(), 3));
LOCAL_HENSEL_RECOMBINATION_NODES.with(|nodes| assert_eq!(nodes.get(), 0));
EXACT_HENSEL_SUBTREE_MODULUS_BITS.with(|bits| {
let bits = bits.borrow();
assert_eq!(bits.len(), 3);
assert!(bits[0] > bits[1]);
assert_eq!(bits[1], Integer::from(101).significant_bits());
assert_eq!(bits[2], Integer::from(101).significant_bits());
});
}
#[test]
fn local_hensel_recombination_supports_more_than_ten_modular_leaves() {
LLL_RECOMBINATION_SUCCESSES.with(|successes| successes.set(0));
LOCAL_HENSEL_RECOMBINATION_NODES.with(|nodes| nodes.set(0));
let variables = Some(Arc::new(vec![symbol!("x").into()]));
let integer_factors = [100, 97, 92, 85, 76, 65].map(|constant| {
parse!(format!("x^2+{constant}").as_str()).to_polynomial::<_, u8>(&Z, variables.clone())
});
let polynomial = integer_factors
.iter()
.fold(integer_factors[0].one(), |product, factor| {
&product * factor
});
let field = Zp::new(101);
let mut modular_factors = (1..=6)
.map(|root| {
parse!(format!("x-{root}").as_str())
.to_polynomial::<_, u8>(&field, variables.clone())
})
.collect::<Vec<_>>();
modular_factors.extend((1..=6).map(|root| {
parse!(format!("x+{root}").as_str()).to_polynomial::<_, u8>(&field, variables.clone())
}));
let bound = polynomial.coefficient_bound();
let (_, max_p) =
MultivariatePolynomial::<IntegerRing, u8>::linear_hensel_modulus(&bound, 101);
let mut factors =
polynomial.factor_hensel_subtree(&modular_factors, &max_p, &bound, 0, false);
let mut expected = integer_factors.to_vec();
factors.sort_by(|left, right| left.internal_cmp(right));
expected.sort_by(|left, right| left.internal_cmp(right));
assert_eq!(factors, expected);
LOCAL_HENSEL_RECOMBINATION_NODES.with(|nodes| assert!(nodes.get() > 0));
LLL_RECOMBINATION_SUCCESSES.with(|successes| assert!(successes.get() > 0));
}
#[test]
fn sparse_modular_divisor_accepts_negative_symmetric_leading_coefficient() {
let variables = Some(Arc::new(vec![symbol!("x").into()]));
let divisor = parse!("1-x").to_polynomial::<_, u8>(&Z, variables.clone());
let value = parse!("1+2*x+x^3").to_polynomial::<_, u8>(&Z, variables.clone());
let multiplier = parse!("2-x+x^2").to_polynomial::<_, u8>(&Z, variables.clone());
let expected = parse!("-1").to_polynomial::<_, u8>(&Z, variables);
let modulus = Integer::from(3);
let context = IntegerModularUnivariateContext::new(&modulus, &divisor);
let prepared = context.prepare_divisor(&divisor);
assert_eq!(
prepared.coefficients,
vec![Integer::from(-1), Integer::one()]
);
let actual = context.multiply_remainder(&value, &multiplier, &prepared);
let field = Zp::new(3);
let value_mod = value.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
);
let multiplier_mod = multiplier.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
);
let mut divisor_mod = divisor.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
);
let reference = (&value_mod * &multiplier_mod)
.quot_rem_univariate(&mut divisor_mod)
.1
.map_coeff(|coefficient| field.to_symmetric_integer(coefficient), Z);
assert_eq!(actual, reference);
assert_eq!(actual, expected);
}
#[test]
fn bounded_i128_multiply_remainder_checks_width_and_canonical_inputs() {
let template = parse!("1+x^3").to_polynomial::<_, u8>(&Z, None);
let dense_indices = (0..16).collect::<Vec<u32>>();
let reference = |context: &DenseIntegerModularUnivariateContext<u8>,
value: &[Integer],
multiplier: &[Integer],
divisor: &[Integer]| {
let reduced = context.remainder_monic(value.to_vec(), divisor);
let product = context.multiply_raw(&reduced, multiplier);
context.remainder_monic(product, divisor)
};
let accepted_modulus_value = 1i128 << 63;
let accepted_modulus = Integer::from(accepted_modulus_value);
let accepted_context = DenseIntegerModularUnivariateContext::new(
&accepted_modulus,
0,
&template,
&dense_indices,
);
let accepted_maximum = Integer::from(accepted_modulus_value - 1);
let accepted_value = vec![accepted_maximum.clone(), accepted_maximum.clone()];
let accepted_multiplier = vec![accepted_maximum.clone()];
let accepted_divisor = vec![accepted_maximum.clone(), Integer::one()];
let expected = reference(
&accepted_context,
&accepted_value,
&accepted_multiplier,
&accepted_divisor,
);
DENSE_INTEGER_I128_MULTIPLY_REMAINDERS.with(|operations| operations.set(0));
let actual = accepted_context
.try_i128_multiply_remainder_monic(
&accepted_value,
&accepted_multiplier,
&accepted_divisor,
)
.expect("the checked bound immediately below 2^127 must fit");
assert_eq!(actual, expected);
DENSE_INTEGER_I128_MULTIPLY_REMAINDERS.with(|operations| assert_eq!(operations.get(), 1));
let rejected_modulus_value = accepted_modulus_value + 1;
let rejected_modulus = Integer::from(rejected_modulus_value);
let rejected_context = DenseIntegerModularUnivariateContext::new(
&rejected_modulus,
0,
&template,
&dense_indices,
);
let rejected_maximum = Integer::from(rejected_modulus_value - 1);
let rejected_value = vec![rejected_maximum.clone(), rejected_maximum.clone()];
let rejected_multiplier = vec![rejected_maximum.clone()];
let rejected_divisor = vec![rejected_maximum, Integer::one()];
assert!(
rejected_context
.try_i128_multiply_remainder_monic(
&rejected_value,
&rejected_multiplier,
&rejected_divisor,
)
.is_none()
);
let expected = reference(
&rejected_context,
&rejected_value,
&rejected_multiplier,
&rejected_divisor,
);
let actual = rejected_context.multiply_remainder_monic(
rejected_value,
&rejected_multiplier,
&rejected_divisor,
);
assert_eq!(actual, expected);
DENSE_INTEGER_I128_MULTIPLY_REMAINDERS.with(|operations| assert_eq!(operations.get(), 1));
assert!(
accepted_context
.try_i128_multiply_remainder_monic(
&[Integer::from(-1)],
&accepted_multiplier,
&accepted_divisor,
)
.is_none()
);
assert!(
accepted_context
.try_i128_multiply_remainder_monic(
std::slice::from_ref(&accepted_modulus),
&accepted_multiplier,
&accepted_divisor,
)
.is_none()
);
assert!(
accepted_context
.try_i128_multiply_remainder_monic(
&accepted_value,
&accepted_multiplier,
&[Integer::from(2), Integer::from(2)],
)
.is_none()
);
assert_eq!(
accepted_context.try_i128_multiply_remainder_monic(
&[],
&accepted_multiplier,
&accepted_divisor,
),
Some(Vec::new())
);
assert_eq!(
accepted_context.try_i128_multiply_remainder_monic(
&accepted_value,
&[],
&accepted_divisor,
),
Some(Vec::new())
);
assert_eq!(
accepted_context.try_i128_multiply_remainder_monic(
&accepted_value,
&accepted_multiplier,
&[Integer::one()],
),
Some(Vec::new())
);
assert_eq!(
accepted_context.try_i128_multiply_remainder_monic(
&[Integer::zero(), Integer::zero()],
&accepted_multiplier,
&accepted_divisor,
),
Some(Vec::new())
);
}
#[test]
fn dense_integer_modular_univariate_arithmetic_matches_reference() {
let variables = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("y").into(),
symbol!("z").into(),
]));
let left = parse!("340282366920938463463374607431768211507-37*y^2+109*y^7")
.to_polynomial::<_, u8>(&Z, variables.clone());
let right = parse!("-170141183460469231731687303715884105741+43*y^3-71*y^6")
.to_polynomial::<_, u8>(&Z, variables.clone());
let dividend = parse!(
"19-31*y+47*y^2+53*y^3-71*y^5+89*y^8+340282366920938463463374607431768211507*y^11"
)
.to_polynomial::<_, u8>(&Z, variables.clone());
let unit_leading_divisor =
parse!("17-29*y^2+23*y^4+2*y^6").to_polynomial::<_, u8>(&Z, variables.clone());
let monic_divisor = parse!("17-29*y^2+23*y^4+y^6").to_polynomial::<_, u8>(&Z, variables);
let dense_indices = (0..32).collect::<Vec<u32>>();
for modulus in [Integer::from(5).pow(8), Integer::from(5).pow(65)] {
let context =
DenseIntegerModularUnivariateContext::new(&modulus, 1, ÷nd, &dense_indices);
let left_dense = context.dense_coefficients(&left);
let right_dense = context.dense_coefficients(&right);
assert!(left_dense[1].is_zero());
assert!(right_dense[1].is_zero());
let raw_product = context.multiply_raw(&left_dense, &right_dense);
assert_eq!(context.from_dense_coefficients(raw_product), &left * &right);
let field = FiniteField::<Integer>::new_non_prime(modulus.clone());
let left_mod = left.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
);
let right_mod = right.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
);
let reference_product =
(&left_mod * &right_mod).map_coeff(|coefficient| field.to_integer(coefficient), Z);
let modular_product = context.multiply_mod(&left_dense, &right_dense);
assert_eq!(
context.from_dense_coefficients(modular_product),
reference_product
);
let dividend_dense = context.dense_coefficients(÷nd);
let divisor_dense = context.dense_coefficients(&monic_divisor);
let remainder = context.remainder_monic(dividend_dense.clone(), &divisor_dense);
let dividend_mod = dividend.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
);
let mut divisor_mod = monic_divisor.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
);
let reference_remainder = dividend_mod
.quot_rem_univariate(&mut divisor_mod)
.1
.map_coeff(|coefficient| field.to_integer(coefficient), Z);
assert_eq!(
context.from_dense_coefficients(remainder),
reference_remainder
);
let mut unit_leading_divisor_dense = context.dense_coefficients(&unit_leading_divisor);
context.reduce_in_place(&mut unit_leading_divisor_dense);
let prepared_divisor = context.prepare_unit_leading_divisor(unit_leading_divisor_dense);
let product_remainder =
context.multiply_remainder(dividend_dense.clone(), &right_dense, &prepared_divisor);
let mut unit_leading_divisor_mod = unit_leading_divisor.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
);
let reference_product_remainder = (÷nd_mod * &right_mod)
.quot_rem_univariate(&mut unit_leading_divisor_mod)
.1
.map_coeff(|coefficient| field.to_integer(coefficient), Z);
assert_eq!(
context.from_dense_coefficients(product_remainder),
reference_product_remainder
);
let zero = Vec::<Integer>::new();
assert!(context.multiply_raw(&zero, &left_dense).is_empty());
assert!(context.multiply_mod(&left_dense, &zero).is_empty());
assert!(
context
.remainder_monic(zero.clone(), &divisor_dense)
.is_empty()
);
assert!(
context
.exact_bezout_residual_quotient_mod(
&zero,
&left_dense,
&[Integer::one()],
&[Integer::one()],
&Integer::from(5),
)
.is_empty()
);
}
}
#[test]
fn dense_hensel_residuals_and_corrections_match_exact_reference() {
let variables = Some(Arc::new(vec![symbol!("x").into(), symbol!("y").into()]));
let template = parse!("1+y^8").to_polynomial::<_, u8>(&Z, variables);
let dense_indices = (0..32).collect::<Vec<u32>>();
for (scale, correction_modulus) in [
(Integer::from(5).pow(3), Integer::from(5).pow(2)),
(Integer::from(5).pow(65), Integer::from(5).pow(33)),
] {
assert!(correction_modulus <= scale);
let next_modulus = &scale * &correction_modulus;
let correction_context = DenseIntegerModularUnivariateContext::new(
&correction_modulus,
1,
&template,
&dense_indices,
);
let next_context = DenseIntegerModularUnivariateContext::new(
&next_modulus,
1,
&template,
&dense_indices,
);
let left = vec![&scale - 3, Integer::from(2), Integer::one()];
let right = vec![Integer::from(7), Integer::from(3), Integer::one()];
let error = vec![
&correction_modulus - 1,
Integer::from(11),
&correction_modulus - 2,
Integer::from(3),
];
let mut target = correction_context.multiply_raw(&left, &right);
target.resize(target.len().max(error.len()), Integer::zero());
for (target_coefficient, error_coefficient) in target.iter_mut().zip(&error) {
*target_coefficient += error_coefficient * &scale;
}
let exact_product_residual =
correction_context.exact_product_residual(&target, &left, &right);
assert_eq!(
exact_product_residual,
error
.iter()
.map(|coefficient| coefficient * &scale)
.collect::<Vec<_>>()
);
let product_residual = correction_context
.exact_product_residual_quotient_mod(&target, &left, &right, &scale);
assert_eq!(product_residual, correction_context.reduce(&error));
let scaled_error = error
.iter()
.map(|coefficient| coefficient * &scale)
.collect::<Vec<_>>();
assert_eq!(
correction_context.exact_scalar_quotient_mod(scaled_error, &scale),
correction_context.reduce(&error)
);
// (scale-1)*(1+y) + (2+y) = 1 + scale*(1+y), so the
// Bezout residual quotient is exactly -(1+y).
let bezout_residual = correction_context.exact_bezout_residual_quotient_mod(
&[&scale - 1],
&[Integer::one(), Integer::one()],
&[Integer::one()],
&[Integer::from(2), Integer::one()],
&scale,
);
assert_eq!(
bezout_residual,
vec![&correction_modulus - 1, &correction_modulus - 1]
);
let old = vec![&scale - 1, &scale - 2, Integer::one()];
let delta = vec![&correction_modulus - 1, &correction_modulus - 2];
let old_mod_correction = correction_context.reduce(&old);
let mut expected = old.clone();
for (coefficient, correction) in expected.iter_mut().zip(&delta) {
*coefficient += correction * &scale;
}
let lifted = next_context.lift_correction(old, &delta, &scale);
assert_eq!(lifted, expected);
assert_eq!(correction_context.reduce(&lifted), old_mod_correction);
assert!(
lifted.iter().all(|coefficient| {
!coefficient.is_negative() && coefficient < &next_modulus
})
);
assert_eq!(lifted[0], &next_modulus - 1);
let signed_old = vec![Integer::from(-7), Integer::from(3), Integer::one()];
let signed_delta = vec![Integer::from(5), Integer::from(-2)];
let mut expected_signed = signed_old.clone();
for (coefficient, correction) in expected_signed.iter_mut().zip(&signed_delta) {
*coefficient += correction * &scale;
}
assert_eq!(
correction_context.add_scaled_exact(signed_old, &signed_delta, &scale),
expected_signed
);
let mut symmetric = lifted.clone();
next_context.symmetrize_in_place(&mut symmetric);
let expected_symmetric = lifted
.into_iter()
.map(|coefficient| coefficient.symmetric_mod(&next_modulus))
.collect::<Vec<_>>();
assert_eq!(symmetric, expected_symmetric);
}
}
#[test]
fn integer_modular_univariate_symmetric_reduction_matches_integer() {
let template = parse!("1+x").to_polynomial::<_, u8>(&Z, None);
for modulus in [Integer::from(4), Integer::from(5).pow(65)] {
let context = IntegerModularUnivariateContext::new(&modulus, &template);
let half_modulus: Integer = &modulus / 2;
let minimum_symmetric: Integer = &half_modulus + Integer::one() - &modulus;
for multiplier in -2..=2 {
for offset in -2..=2 {
let value: Integer = &modulus * multiplier + offset;
assert_eq!(
context.symmetric_reduce(value.clone()),
value.symmetric_mod(&modulus)
);
}
}
for value in [
&minimum_symmetric - 1,
minimum_symmetric.clone(),
&minimum_symmetric + 1,
&half_modulus - 1,
half_modulus.clone(),
&half_modulus + 1,
] {
assert_eq!(
context.symmetric_reduce(value.clone()),
value.symmetric_mod(&modulus)
);
}
}
}
#[test]
fn integer_modular_univariate_arithmetic_matches_finite_field_reference() {
let variables = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("y").into(),
symbol!("z").into(),
]));
let dividend = parse!("19-31*y+47*y^2+53*y^3-71*y^5+89*y^8")
.to_polynomial::<_, u8>(&Z, variables.clone());
let divisor = parse!("17+23*y-29*y^2-3*y^3").to_polynomial::<_, u8>(&Z, variables.clone());
let monic_divisor =
parse!("17+23*y-29*y^2+y^3").to_polynomial::<_, u8>(&Z, variables.clone());
let short_sparse_dividend =
parse!("-13+7*y^2").to_polynomial::<_, u8>(&Z, variables.clone());
let higher_degree_monic_divisor =
parse!("-3+11*y-19*y^2+23*y^4+y^6").to_polynomial::<_, u8>(&Z, variables);
let zero_dividend = dividend.zero();
assert!(short_sparse_dividend.degree(1) < higher_degree_monic_divisor.degree(1));
for modulus in [Integer::from(5).pow(8), Integer::from(5).pow(65)] {
let context = IntegerModularUnivariateContext::new(&modulus, ÷nd);
assert_eq!(context.variable, 1);
let reduced_dividend = context.reduce(÷nd);
let reduced_divisor = context.reduce(&divisor);
let (quotient, remainder) = context.quot_rem(&reduced_dividend, &reduced_divisor);
let field = FiniteField::<Integer>::new_non_prime(modulus.clone());
let dividend_mod = dividend.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
);
let divisor_mod = divisor.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
);
let reference_add = (÷nd_mod + &divisor_mod)
.map_coeff(|coefficient| field.to_symmetric_integer(coefficient), Z);
let reference_product = (÷nd_mod * &divisor_mod)
.map_coeff(|coefficient| field.to_symmetric_integer(coefficient), Z);
let mut divisor_for_division = divisor_mod.clone();
let (reference_quotient, reference_remainder) =
dividend_mod.quot_rem_univariate(&mut divisor_for_division);
let reference_quotient = reference_quotient
.map_coeff(|coefficient| field.to_symmetric_integer(coefficient), Z);
let reference_remainder = reference_remainder
.map_coeff(|coefficient| field.to_symmetric_integer(coefficient), Z);
assert_eq!(
context.add(&reduced_dividend, &reduced_divisor),
reference_add
);
assert_eq!(
context.multiply(&reduced_dividend, &reduced_divisor),
reference_product
);
assert_eq!(quotient, reference_quotient);
assert_eq!(remainder, reference_remainder);
let product = "ient * &reduced_divisor;
let error = &(&reduced_dividend - &product) - &remainder;
assert!(remainder.degree(1) < reduced_divisor.degree(1));
assert!(
error
.coefficients
.iter()
.all(|coefficient| (coefficient % &modulus).is_zero())
);
let half_modulus = &modulus / 2;
let near_half: Integer = &half_modulus - 1;
let dense_divisor = context.from_dense_coefficients(
(0..=16)
.map(|index| {
if index == 16 {
Integer::from(2)
} else if index % 2 == 0 {
near_half.clone()
} else {
-&near_half
}
})
.collect(),
);
let dense_quotient = context.from_dense_coefficients(
(0..=31)
.map(|index| {
if index % 7 == 2 {
Integer::zero()
} else if index % 2 == 0 {
near_half.clone()
} else {
-&near_half
}
})
.collect(),
);
let dense_remainder = context.from_dense_coefficients(
(0..16)
.map(|index| {
if index % 2 == 0 {
near_half.clone()
} else {
-&near_half
}
})
.collect(),
);
let dense_dividend = context.add(
&context.multiply(&dense_quotient, &dense_divisor),
&dense_remainder,
);
let (actual_quotient, actual_remainder) =
context.quot_rem(&dense_dividend, &dense_divisor);
assert_eq!(actual_quotient, dense_quotient);
assert_eq!(actual_remainder, dense_remainder);
assert!(
actual_quotient
.coefficients
.iter()
.chain(&actual_remainder.coefficients)
.all(|coefficient| coefficient == &coefficient.clone().symmetric_mod(&modulus))
);
let prepared_divisor = context.prepare_divisor(&reduced_divisor);
let direct_product_remainder =
context.multiply_remainder(&reduced_dividend, &dense_quotient, &prepared_divisor);
let reference_product_remainder = context
.quot_rem(
&context.multiply(&reduced_dividend, &dense_quotient),
&reduced_divisor,
)
.1;
assert_eq!(direct_product_remainder, reference_product_remainder);
let constant_divisor = context.from_dense_coefficients(vec![Integer::from(2)]);
let (constant_quotient, constant_remainder) =
context.quot_rem(&dense_dividend, &constant_divisor);
assert!(constant_remainder.is_zero());
assert_eq!(
context.multiply(&constant_quotient, &constant_divisor),
dense_dividend
);
let reduced_short_dividend = context.reduce(&short_sparse_dividend);
let (short_quotient, short_remainder) =
context.quot_rem(&reduced_short_dividend, &reduced_divisor);
assert!(short_quotient.is_zero());
assert_eq!(short_remainder, reduced_short_dividend);
for (monic_dividend, monic_divisor) in [
(÷nd, &monic_divisor),
(&short_sparse_dividend, &higher_degree_monic_divisor),
(&zero_dividend, &higher_degree_monic_divisor),
] {
let remainder = context.remainder_monic(monic_dividend, monic_divisor);
let dividend_mod = monic_dividend.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
);
let mut divisor_mod = monic_divisor.map_coeff(
|coefficient| coefficient.to_finite_field(&field),
field.clone(),
);
let reference_remainder = dividend_mod
.quot_rem_univariate(&mut divisor_mod)
.1
.map_coeff(|coefficient| field.to_symmetric_integer(coefficient), Z);
assert_eq!(remainder, reference_remainder);
assert!(remainder.coefficients.iter().all(|coefficient| {
coefficient == &coefficient.clone().symmetric_mod(&modulus)
}));
}
}
}
#[test]
fn factor_univariate_high_height_uses_quadratic_hensel_lift() {
QUADRATIC_HENSEL_LIFT_CALLS.with(|calls| calls.set(0));
LAST_MODULAR_INTEGER_EDF_PRIME.with(|prime| prime.set(0));
EXACT_HENSEL_SUBTREE_SPLITS.with(|splits| splits.set(0));
LOCAL_HENSEL_RECOMBINATION_NODES.with(|nodes| nodes.set(0));
EXACT_HENSEL_SUBTREE_MODULUS_BITS.with(|bits| bits.borrow_mut().clear());
let polynomial = parse!("((1+65537*x)^17-1)*((1-65539*x)^16+1)")
.expand()
.to_polynomial::<_, u8>(&Z, None);
let factors = polynomial.factor();
let expanded = factors
.iter()
.fold(polynomial.one(), |product, (factor, power)| {
&product * &factor.pow(*power)
});
let mut factor_degrees = factors
.iter()
.map(|(factor, _)| factor.degree(0).to_u32())
.filter(|degree| *degree > 0)
.collect::<Vec<_>>();
factor_degrees.sort_unstable();
assert_eq!(factor_degrees, [1, 16, 16]);
assert_eq!(expanded, polynomial);
LAST_MODULAR_INTEGER_EDF_PRIME.with(|prime| assert_eq!(prime.get(), 5));
EXACT_HENSEL_SUBTREE_SPLITS.with(|splits| assert!(splits.get() > 0));
LOCAL_HENSEL_RECOMBINATION_NODES.with(|nodes| assert!(nodes.get() > 0));
EXACT_HENSEL_SUBTREE_MODULUS_BITS.with(|bits| {
let bits = bits.borrow();
assert!(bits.len() >= 2);
assert!(
bits[1..].iter().any(|child_bits| *child_bits < bits[0]),
"an exact child did not lower its Hensel modulus"
);
});
QUADRATIC_HENSEL_LIFT_CALLS.with(|calls| assert!(calls.get() > 0));
}
#[test]
fn factor_univariate_many_modular_factors_keeps_linear_hensel_lift() {
QUADRATIC_HENSEL_LIFT_CALLS.with(|calls| calls.set(0));
let polynomial = parse!("((1+3*x)^32-1)*((1-5*x)^31+1)")
.expand()
.to_polynomial::<_, u8>(&Z, None);
let factors = polynomial.factor();
let expanded = factors
.iter()
.fold(polynomial.one(), |product, (factor, power)| {
&product * &factor.pow(*power)
});
assert_eq!(expanded, polynomial);
QUADRATIC_HENSEL_LIFT_CALLS.with(|calls| assert_eq!(calls.get(), 0));
}
#[test]
fn factor_bivariate() {
let input = "(v1^2+v2+v1+1)(3*v1+v2^2+4)*(6*v1*(v2+1)+v2+5)*(7*v1*v2+4)";
let poly = parse!(input).to_polynomial::<_, u8>(&Z, None);
let res = [
("(1+v2+v1+v1^2)", 1),
("(5+v2+6*v1+6*v1*v2)", 1),
("(4+v2^2+3*v1)", 1),
("(4+7*v1*v2)", 1),
];
let mut res = res
.iter()
.map(|(f, p)| {
(
parse!(f)
.expand()
.to_polynomial(&Z, poly.variables().clone()),
*p,
)
})
.collect::<Vec<_>>();
res.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
let mut r = poly.factor();
r.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
assert_eq!(r, res);
}
#[test]
fn high_height_two_factor_bivariate_reconstruction_uses_a_wide_prime() {
let variables = Some(Arc::new(vec![symbol!("x").into(), symbol!("y").into()]));
let height = Integer::from(2).pow(130);
let left = parse!("x+y")
.to_polynomial::<_, u8>(&Z, variables.clone())
.add_constant(height.clone());
let right = parse!("x+2*y")
.to_polynomial::<_, u8>(&Z, variables)
.add_constant(height + Integer::from(1));
let polynomial = &left * &right;
assert!(!polynomial.bivariate_irreducibility_test());
assert!(
polynomial.coefficient_bound().significant_bits()
>= BIVARIATE_WIDE_PRIME_MIN_BOUND_BITS
);
LAST_BIVARIATE_RECONSTRUCTION_PRIME.with(|prime| prime.set(0));
let factors = polynomial.bivariate_factor_reconstruct(0, 1);
LAST_BIVARIATE_RECONSTRUCTION_PRIME.with(|prime| {
assert!(prime.get() > BIVARIATE_WIDE_PRIME_START as u32);
});
assert_eq!(factors.len(), 2);
assert!(
factors
.iter()
.all(|factor| factor.degree(0) == 1 && factor.degree(1) == 1)
);
let reconstructed = factors
.iter()
.fold(polynomial.one(), |product, factor| &product * factor);
assert_eq!(reconstructed, polynomial);
}
#[test]
fn high_height_bivariate_reconstruction_falls_back_after_rejected_wide_primes() {
let variables = Some(Arc::new(vec![symbol!("x").into(), symbol!("y").into()]));
let rejected_prime_product = PrimeIteratorU64::new(BIVARIATE_WIDE_PRIME_START)
.take(BIVARIATE_WIDE_PRIME_ATTEMPTS)
.fold(Integer::one(), |product, prime| {
product * Integer::from(prime)
});
let height = Integer::from(2).pow(130);
let left = parse!(format!("{rejected_prime_product}*x+y+{height}").as_str())
.to_polynomial::<_, u8>(&Z, variables.clone());
let right = parse!(format!("x+2*y+{}", &height + Integer::from(1)).as_str())
.to_polynomial::<_, u8>(&Z, variables);
let polynomial = &left * &right;
assert!(
polynomial.coefficient_bound().significant_bits()
>= BIVARIATE_WIDE_PRIME_MIN_BOUND_BITS
);
LAST_BIVARIATE_RECONSTRUCTION_PRIME.with(|prime| prime.set(0));
let factors = polynomial.bivariate_factor_reconstruct(0, 1);
LAST_BIVARIATE_RECONSTRUCTION_PRIME.with(|prime| {
assert!(prime.get() < BIVARIATE_WIDE_PRIME_START as u32);
});
assert_eq!(factors.len(), 2);
let reconstructed = factors
.iter()
.fold(polynomial.one(), |product, factor| &product * factor);
assert_eq!(reconstructed, polynomial);
}
#[test]
fn factor_multivariate() {
let input = "(v1*(2+2*v2+2*v3)+1)*(v1*(4+v3^2)+v2+3)*(v1*(v4+v4^2+4+v2)+v4+5)";
let poly = parse!(input).to_polynomial::<_, u8>(&Z, None);
let res = [
("5+v4+4*v1+v1*v4+v1*v4^2+v1*v2", 1),
("1+2*v1+2*v1*v3+2*v1*v2 ", 1),
("3+v2+4*v1+v1*v3^2", 1),
];
let mut res = res
.iter()
.map(|(f, p)| {
(
parse!(f)
.expand()
.to_polynomial(&Z, poly.variables().clone()),
*p,
)
})
.collect::<Vec<_>>();
res.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
let mut r = poly.factor();
r.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
assert_eq!(r, res);
}
#[test]
fn quadratic_discriminant_square_root_uses_even_multiplicities() {
let discriminant = parse!("36*(x+1)^4*(y+2)^6")
.expand()
.to_polynomial::<_, u8>(&Z, None);
let ExactPolynomialSquareRoot::Root(root) =
discriminant.square_root_from_square_free_decomposition()
else {
panic!("the discriminant is an exact square");
};
assert_eq!(&root * &root, discriminant);
let not_a_square = parse!("36*(x+1)^3*(y+2)^6")
.expand()
.to_polynomial::<_, u8>(&Z, None);
assert!(matches!(
not_a_square.square_root_from_square_free_decomposition(),
ExactPolynomialSquareRoot::NotSquare
));
}
#[test]
fn packed_sparse_square_root_reconstructs_exact_large_coefficients() {
let mut expected = parse!(
"1393796574908163946345982392040522594123777*x^9*y^2+17*x^7*z^5-23*x^3*y^4*z+31*y*z^8-41"
)
.to_polynomial::<_, u16>(&Z, None);
if expected.lcoeff().is_negative() {
expected = -expected;
}
let square = &expected * &expected;
let context = SparsePolynomialSquareRootContext::new(square.nvars(), square.nterms());
let Some(ExactPolynomialSquareRoot::Root(root)) = context.reconstruct(&square) else {
panic!("the packed sparse square-root path must recover the exact root");
};
assert_eq!(root, expected);
let not_a_square = square + expected.one();
let context =
SparsePolynomialSquareRootContext::new(not_a_square.nvars(), not_a_square.nterms());
assert!(matches!(
context.reconstruct(¬_a_square),
Some(ExactPolynomialSquareRoot::NotSquare)
));
for mut expected in [
parse!("x^2+2*x*y-2*y^2").to_polynomial::<_, u16>(&Z, None),
parse!("x^6+2*x^4*y^2+3*x^2*y^4+5*y^6").to_polynomial::<_, u16>(&Z, None),
] {
if expected.lcoeff().is_negative() {
expected = -expected;
}
let square = &expected * &expected;
let context = SparsePolynomialSquareRootContext::new(square.nvars(), square.nterms());
let Some(ExactPolynomialSquareRoot::Root(root)) = context.reconstruct(&square) else {
panic!("the packed path must handle cancellations and product collisions");
};
assert_eq!(root, expected);
}
let odd_degree = parse!("x^3+x+1").to_polynomial::<_, u16>(&Z, None);
let context =
SparsePolynomialSquareRootContext::new(odd_degree.nvars(), odd_degree.nterms());
assert!(matches!(
context.reconstruct(&odd_degree),
Some(ExactPolynomialSquareRoot::NotSquare)
));
}
#[test]
fn packed_sparse_square_matches_generic_integer_multiplication() {
let template = parse!("x+y+z+u+v").to_polynomial::<_, u16>(&Z, None);
let mut sparse = template.zero();
for index in 0..64usize {
let coefficient = if index == 0 {
Integer::from(2).pow(160) + Integer::from(1)
} else if index % 2 == 0 {
Integer::from(index as i64 + 1)
} else {
Integer::from(-(index as i64) - 1)
};
let exponents = [
(2 * index) as u16,
(index * index % 128) as u16,
(3 * index % 128) as u16,
(5 * index % 128) as u16,
(7 * index % 128) as u16,
];
sparse.append_monomial(coefficient, &exponents);
}
assert_eq!(sparse.nterms(), 64);
let expected = &sparse * &sparse;
let context = PackedSparsePolynomialSquareContext::new(sparse.nvars(), sparse.nterms());
assert_eq!(context.try_square(&sparse), Some(expected));
let dense = parse!("(1+x+y+z+u)^4")
.expand()
.to_polynomial::<_, u16>(&Z, None);
assert!(dense.nterms() >= 64);
let context = PackedSparsePolynomialSquareContext::new(dense.nvars(), dense.nterms());
assert!(context.try_square(&dense).is_none());
}
#[test]
fn early_quadratic_split_is_certified_and_preserves_content() {
let polynomial = parse!("-6*w^3*(x+(1+a+b+c+d)^4)*(x+(1+2*a+3*b+5*c+7*d)^4)")
.expand()
.to_polynomial::<_, u16>(&Z, None);
assert!(polynomial.nterms() >= MIN_EARLY_QUADRATIC_FACTOR_TERMS);
EARLY_SEPARABLE_PAIR_PROBE_CALLS.with(|count| count.set(0));
EARLY_SEPARABLE_PAIR_SPLITS.with(|count| count.set(0));
let factors = polynomial
.factor_quadratic_before_square_free()
.expect("the exact quadratic split must bypass the general square-free pass");
let mut product = polynomial.one();
for (factor, multiplicity) in &factors {
product = &product * &factor.pow(*multiplicity);
}
assert_eq!(product, polynomial);
assert_eq!(factors.len(), 4);
let w = parse!("w").to_polynomial(&Z, polynomial.variables().clone());
assert!(factors.iter().any(|(factor, multiplicity)| {
factor.is_constant() && factor.get_constant() == Integer::from(-6) && *multiplicity == 1
}));
assert!(
factors
.iter()
.any(|(factor, multiplicity)| factor == &w && *multiplicity == 3)
);
EARLY_SEPARABLE_PAIR_PROBE_CALLS.with(|count| assert_eq!(count.get(), 0));
EARLY_SEPARABLE_PAIR_SPLITS.with(|count| assert_eq!(count.get(), 0));
}
#[test]
fn early_separable_pair_scout_recovers_an_omitted_variable_factor() {
let variables = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("y").into(),
symbol!("a").into(),
symbol!("b").into(),
symbol!("c").into(),
symbol!("d").into(),
]));
let polynomial = parse!("((1+a+b+c+d)^4+a*b*c*d)*(x+y+1)*(x+2*y+3)")
.expand()
.to_polynomial::<_, u16>(&Z, variables);
assert!(polynomial.nterms() >= MIN_EARLY_QUADRATIC_FACTOR_TERMS);
EARLY_SEPARABLE_PAIR_PROBE_CALLS.with(|count| count.set(0));
EARLY_SEPARABLE_PAIR_SPLITS.with(|count| count.set(0));
let factors = polynomial
.factor_quadratic_before_square_free()
.expect("the well-supported y layers share the factor that omits y");
let reconstructed = factors
.iter()
.fold(polynomial.one(), |product, (factor, power)| {
&product * &factor.pow(*power)
});
assert_eq!(reconstructed, polynomial);
assert!(factors.iter().any(|(factor, _)| factor.degree(1) == 0));
EARLY_SEPARABLE_PAIR_PROBE_CALLS.with(|count| assert!(count.get() <= 2));
EARLY_SEPARABLE_PAIR_SPLITS.with(|count| assert_eq!(count.get(), 1));
}
#[test]
fn early_separable_pair_scout_intersects_every_coefficient_layer() {
let polynomial = parse!("(x+1)*(1+y)+(x^2+x+1)*y^2")
.expand()
.to_polynomial::<_, u8>(&Z, None);
let context = SeparableCoefficientContentContext::new(&polynomial);
let degrees = (0..polynomial.nvars())
.map(|variable| polynomial.degree(variable).to_u32() as usize)
.collect::<Vec<_>>();
let modular_probe_context = EarlyFactorModularProbeContext::new(°rees);
let modular_image =
match modular_probe_context.probe_quadratic_discriminant(&polynomial, 0, true) {
EarlyQuadraticDiscriminantProbe::Nonsquare(image)
| EarlyQuadraticDiscriminantProbe::Square(image) => image,
};
assert!(
context
.split_from_well_supported_layer_pairs(
&polynomial,
0,
&modular_probe_context,
&modular_image,
)
.is_none()
);
}
#[test]
fn quadratic_discriminant_modular_probes_only_reject_nonsquares() {
let variables = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("y").into(),
symbol!("z").into(),
]));
let exact_split = parse!("(x+(1+y+z)^3)*(x+(2+3*y-z)^2)")
.expand()
.to_polynomial::<_, u8>(&Z, variables.clone());
let degrees = (0..exact_split.nvars())
.map(|variable| exact_split.degree(variable).to_u32() as usize)
.collect::<Vec<_>>();
let context = EarlyFactorModularProbeContext::new(°rees);
assert!(matches!(
context.probe_quadratic_discriminant(&exact_split, 0, false),
EarlyQuadraticDiscriminantProbe::Square(_)
));
let zero_discriminant = parse!("(x+y+1)^2")
.expand()
.to_polynomial::<_, u8>(&Z, variables);
let degrees = (0..zero_discriminant.nvars())
.map(|variable| zero_discriminant.degree(variable).to_u32() as usize)
.collect::<Vec<_>>();
let context = EarlyFactorModularProbeContext::new(°rees);
assert!(matches!(
context.probe_quadratic_discriminant(&zero_discriminant, 0, false),
EarlyQuadraticDiscriminantProbe::Square(_)
));
let nonsquare = parse!("x^2+y").to_polynomial::<_, u8>(&Z, None);
let degrees = (0..nonsquare.nvars())
.map(|variable| nonsquare.degree(variable).to_u32() as usize)
.collect::<Vec<_>>();
let context = EarlyFactorModularProbeContext::new(°rees);
let quadratic_variable = degrees.iter().position(|degree| *degree == 2).unwrap();
assert!(matches!(
context.probe_quadratic_discriminant(&nonsquare, quadratic_variable, false),
EarlyQuadraticDiscriminantProbe::Nonsquare(_)
));
}
#[test]
fn cached_coefficient_pair_images_match_direct_specialization() {
let variables = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("y").into(),
symbol!("z").into(),
symbol!("w").into(),
]));
let polynomial = parse!("(1+x+x^2)*(1+y+y^5+z^3+y^2*z^2+w^4+2*z*w^2+65519*z^2*w)")
.expand()
.to_polynomial::<_, u8>(&Z, variables);
let degrees = (0..polynomial.nvars())
.map(|variable| polynomial.degree(variable).to_u32() as usize)
.collect::<Vec<_>>();
let context = EarlyFactorModularProbeContext::new(°rees);
let modular_image = match context.probe_quadratic_discriminant(&polynomial, 0, true) {
EarlyQuadraticDiscriminantProbe::Nonsquare(image)
| EarlyQuadraticDiscriminantProbe::Square(image) => image,
};
assert_eq!(modular_image.quadratic_variable, 0);
assert_eq!(modular_image.base_retained_variable, Some(1));
let coefficient_variable = 1;
let retained_variable = 3;
let coefficient_degrees = [0, 1];
let actual = context
.coefficient_pair_images(
&polynomial,
coefficient_variable,
coefficient_degrees,
&modular_image,
)
.unwrap();
let mut expected: [Vec<FiniteFieldElement<u32>>; 2] =
std::array::from_fn(|_| vec![context.field.zero(); degrees[retained_variable] + 1]);
for term in &polynomial {
let coefficient_degree = term.exponents[coefficient_variable].to_u32();
let Some(image_index) = coefficient_degrees
.iter()
.position(|degree| *degree == coefficient_degree)
else {
continue;
};
let mut value = term.coefficient.to_finite_field(&context.field);
for (variable, exponent) in term.exponents.iter().enumerate() {
if variable == coefficient_variable
|| variable == retained_variable
|| variable == modular_image.quadratic_variable
|| Some(variable) == modular_image.base_retained_variable
{
continue;
}
context.field.mul_assign(
&mut value,
&context.powers[variable][exponent.to_u32() as usize],
);
}
context.field.add_assign(
&mut expected[image_index][term.exponents[retained_variable].to_u32() as usize],
&value,
);
}
for image in &mut expected {
context.trim(image);
}
assert_eq!(actual, expected);
}
#[test]
fn early_quadratic_split_falls_back_on_inconclusive_discriminants() {
for polynomial in [
parse!("x^2+y+z").to_polynomial::<_, u8>(&Z, None),
parse!("x^2+y*x+z").to_polynomial::<_, u8>(&Z, None),
parse!("x^2+y^200*x+1").to_polynomial::<_, u8>(&Z, None),
] {
let degrees = (0..polynomial.nvars())
.map(|var| polynomial.degree(var).to_u32() as usize)
.collect::<Vec<_>>();
assert!(polynomial.try_early_factor_split(°rees).is_none());
}
}
#[test]
fn quadratic_factorization_handles_constant_discriminant() {
let vars = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("y").into(),
symbol!("z").into(),
]));
let poly = parse!("(x+y+z)*(x+y+z+1)")
.expand()
.to_polynomial::<_, u8>(&Z, vars);
let Some(QuadraticFactorization::Split(factors)) = poly.factor_quadratic_variable(0) else {
panic!("the quadratic must split");
};
assert_eq!(&factors[0] * &factors[1], poly);
}
#[test]
fn quadratic_factorization_handles_repeated_discriminant_root_factor() {
let vars = Some(Arc::new(vec![symbol!("x").into(), symbol!("y").into()]));
let poly = parse!("(y+x^2)*(y-x^2)")
.expand()
.to_polynomial::<_, u8>(&Z, vars);
let Some(QuadraticFactorization::Split(factors)) = poly.factor_quadratic_variable(1) else {
panic!("the quadratic must split");
};
assert_eq!(&factors[0] * &factors[1], poly);
}
#[test]
fn quadratic_factorization_reconstructs_nonunit_leading_coefficients() {
let vars = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("u").into(),
symbol!("v").into(),
symbol!("w").into(),
]));
let poly = parse!("((u+1)*x+v+w+1)*((v+2)*x+u+w+3)")
.expand()
.to_polynomial::<_, u8>(&Z, vars);
let Some(QuadraticFactorization::Split(factors)) = poly.factor_quadratic_variable(0) else {
panic!("the quadratic must split");
};
assert_eq!(&factors[0] * &factors[1], poly);
assert_eq!(poly.factor().len(), 2);
}
#[test]
fn quadratic_factorization_rejects_nonsquare_discriminant() {
let vars = Some(Arc::new(vec![
symbol!("x").into(),
symbol!("y").into(),
symbol!("z").into(),
]));
let poly = parse!("x^2+y^2+z^2+1").to_polynomial::<_, u8>(&Z, vars);
assert!(matches!(
poly.factor_quadratic_variable(0),
Some(QuadraticFactorization::Irreducible)
));
assert_eq!(poly.factor(), vec![(poly.clone(), 1)]);
assert!(poly.is_irreducible());
}
#[test]
fn quadratic_factorization_widens_discriminant_exponents() {
let polynomial = parse!("x^2+y^200*x+z+1").to_polynomial::<_, u8>(&Z, None);
let degrees = (0..polynomial.nvars())
.map(|variable| polynomial.degree(variable).to_u32() as usize)
.collect::<Vec<_>>();
assert!(matches!(
polynomial.factor_quadratic(°rees),
Some(QuadraticFactorization::Irreducible)
));
let factors = polynomial.factor();
assert_eq!(factors, vec![(polynomial, 1)]);
let split_polynomial = parse!("(x+y^200+z)*(x+y^20+1)")
.expand()
.to_polynomial::<_, u8>(&Z, None);
let degrees = (0..split_polynomial.nvars())
.map(|variable| split_polynomial.degree(variable).to_u32() as usize)
.collect::<Vec<_>>();
let Some(QuadraticFactorization::Split(split)) =
split_polynomial.factor_quadratic(°rees)
else {
panic!("the widened discriminant must recover both linear factors");
};
assert_eq!(&split[0] * &split[1], split_polynomial);
let factors = split_polynomial.factor();
assert_eq!(factors.len(), 2);
let reconstructed = factors
.into_iter()
.fold(split_polynomial.one(), |product, (factor, multiplicity)| {
&product * &factor.pow(multiplicity)
});
assert_eq!(reconstructed, split_polynomial);
}
#[test]
fn wang_lcoeff_reconstruction_accepts_unlucky_content_sample() {
let vars = Some(Arc::new(vec![
symbol!("m").into(),
symbol!("u").into(),
symbol!("v").into(),
]));
let poly = parse!("(3*m-u)*(5*m-v)")
.expand()
.to_polynomial::<_, u8>(&Z, vars);
let sample_points = vec![(1, Integer::from(3)), (2, Integer::from(10))];
let univariate_factors = ["m-1", "m-2"]
.iter()
.map(|f| parse!(f).to_polynomial(&Z, poly.variables().clone()))
.collect::<Vec<_>>();
let lcoeff = poly.univariate_lcoeff(0);
let lcoeff_factorization = lcoeff.factor();
assert!(
poly.reconstruct_lcoeffs_from_univariate_sample(
&lcoeff,
&lcoeff_factorization,
&univariate_factors,
&sample_points,
&Integer::from(1),
)
.is_none()
);
let reconstructed = poly
.reconstruct_lcoeffs_from_univariate_sample(
&lcoeff,
&lcoeff_factorization,
&univariate_factors,
&sample_points,
&Integer::from(15),
)
.unwrap();
assert_eq!(reconstructed, vec![poly.one(), poly.one()]);
}
#[test]
fn wang_lcoeff_reconstruction_rejects_scalar_factor_collisions() {
let vars = Some(Arc::new(vec![symbol!("m").into(), symbol!("x").into()]));
let poly = parse!("(3*m*x+1)*(5*m*x+1)")
.expand()
.to_polynomial::<_, u8>(&Z, vars);
let lcoeff = poly.univariate_lcoeff(0);
let lcoeff_factorization = lcoeff.factor();
let lcoeff_content = lcoeff.content().abs();
let colliding_sample = vec![(1, Integer::from(3))];
let colliding_factors = ["9*m+1", "15*m+1"]
.iter()
.map(|f| parse!(f).to_polynomial(&Z, poly.variables().clone()))
.collect::<Vec<_>>();
assert!(
!MultivariatePolynomial::lcoeff_sample_supports_wang_reconstruction(
&lcoeff_factorization,
&lcoeff_content,
&colliding_sample,
)
);
assert!(
poly.reconstruct_lcoeffs_from_univariate_sample(
&lcoeff,
&lcoeff_factorization,
&colliding_factors,
&colliding_sample,
&Integer::one(),
)
.is_none()
);
let coprime_sample = vec![(1, Integer::from(2))];
let coprime_factors = ["6*m+1", "10*m+1"]
.iter()
.map(|f| parse!(f).to_polynomial(&Z, poly.variables().clone()))
.collect::<Vec<_>>();
assert!(
MultivariatePolynomial::lcoeff_sample_supports_wang_reconstruction(
&lcoeff_factorization,
&lcoeff_content,
&coprime_sample,
)
);
let reconstructed = poly
.reconstruct_lcoeffs_from_univariate_sample(
&lcoeff,
&lcoeff_factorization,
&coprime_factors,
&coprime_sample,
&Integer::one(),
)
.unwrap();
let expected = ["3*x", "5*x"]
.iter()
.map(|f| parse!(f).to_polynomial(&Z, poly.variables().clone()))
.collect::<Vec<_>>();
assert_eq!(reconstructed, expected);
}
#[test]
fn wang_lcoeff_sampling_allows_nonmonomial_fixed_divisors() {
let vars = Some(Arc::new(vec![symbol!("y").into()]));
let lcoeff = parse!("2*(y^2-y+2)")
.expand()
.to_polynomial::<_, u8>(&Z, vars);
let lcoeff_factorization = lcoeff.factor();
assert!(
MultivariatePolynomial::lcoeff_sample_supports_wang_reconstruction(
&lcoeff_factorization,
&lcoeff.content().abs(),
&[(0, Integer::from(3))],
)
);
}
#[test]
fn wang_univariate_sample_starts_with_coprime_prime_coordinates() {
let vars = Some(Arc::new(vec![
symbol!("m").into(),
symbol!("x1").into(),
symbol!("x2").into(),
symbol!("x3").into(),
symbol!("x4").into(),
symbol!("x5").into(),
symbol!("x6").into(),
symbol!("x7").into(),
]));
let poly = parse!("(6*m*x1*x2*x3*x4*x5*x6*x7+1)*(m+1)")
.expand()
.to_polynomial::<_, u8>(&Z, vars);
let mut order = (0..8).collect::<Vec<_>>();
// The leading-coefficient content is divisible by 2 and 3, so the
// deterministic coordinates start with the next distinct primes.
let (factors, true_lcoeffs, sample_points, _, image, content) = poly
.find_univariate_sample(&mut order, 10, Some(2))
.unwrap();
let expected_points = [5i64, 7, 11, 13, 17, 19, 23]
.into_iter()
.map(Integer::from)
.collect::<Vec<_>>();
assert_eq!(
sample_points
.iter()
.map(|(_, point)| point.clone())
.collect::<Vec<_>>(),
expected_points
);
assert_eq!(factors.len(), 2);
let mut repeated_image = poly.clone();
for (var, point) in sample_points.iter().rev() {
repeated_image = repeated_image.replace(*var, point);
}
assert_eq!(image, repeated_image);
let reconstructed_lcoeff = true_lcoeffs
.iter()
.fold(poly.one(), |product, lcoeff| &product * lcoeff)
.mul_coeff(content);
assert_eq!(reconstructed_lcoeff, poly.univariate_lcoeff(order[0]));
}
#[test]
fn wang_univariate_sampling_retries_prime_block_after_specialization_content() {
let vars = Some(Arc::new(vec![symbol!("m").into(), symbol!("x").into()]));
let poly = parse!("(x*m+x+2)*(m+1)")
.expand()
.to_polynomial::<_, u8>(&Z, vars);
let mut order = vec![0, 1];
let (factors, _, sample_points, _, _, content) = poly
.find_univariate_sample(&mut order, 10, Some(2))
.unwrap();
assert_eq!(sample_points, vec![(1, Integer::from(3))]);
assert!(content.is_one());
assert_eq!(factors.len(), 2);
}
#[test]
fn wang_univariate_retry_relaxes_stale_factor_bound() {
let vars = Some(Arc::new(vec![
symbol!("m").into(),
symbol!("x1").into(),
symbol!("x2").into(),
symbol!("x3").into(),
symbol!("x4").into(),
symbol!("x5").into(),
symbol!("x6").into(),
symbol!("x7").into(),
]));
let poly =
parse!("(m*x1*x2*x3*x4*x5*x6*x7+1+x1+x2+x3+x4+x5+x6+x7)*(m+1+x1+x2+x3+x4+x5+x6+x7)")
.expand()
.to_polynomial::<_, u8>(&Z, vars);
let mut order = (0..8).collect::<Vec<_>>();
assert!(!poly.integer_factor_start_auto_decision(&order).0);
let factors = poly.multivariate_factorization(&mut order, 10, Some(0), None, false);
let product = factors
.iter()
.fold(poly.one(), |product, factor| &product * factor);
assert_eq!(factors.len(), 2);
assert_eq!(product, poly);
}
#[test]
fn integer_factor_auto_selects_bivariate_start_below_density_threshold() {
let vars = Some(Arc::new(vec![
symbol!("m").into(),
symbol!("n").into(),
symbol!("z").into(),
]));
let poly = parse!("(1+m)*(1+n)*(1+z+z^2+z^3)")
.expand()
.to_polynomial::<_, u8>(&Z, vars);
let order = [0, 1, 2];
let (use_bivariate, density) = poly.integer_factor_start_auto_decision(&order);
assert_eq!(density, 4.0);
assert!(use_bivariate);
}
#[test]
fn bivariate_retry_context_advances_and_restores_univariate_order() {
let mut retry_context =
BivariateFactorRetryContext::new(Some(UnivariateFactorFallbackState {
order: vec![2usize, 0usize, 1usize],
coefficient_upper_bound: 10,
}));
let mut coefficient_upper_bound = 10i64;
assert!(
retry_context
.advance(&mut coefficient_upper_bound)
.is_none()
);
assert_eq!(coefficient_upper_bound, 20);
assert!(
retry_context
.advance(&mut coefficient_upper_bound)
.is_none()
);
assert_eq!(coefficient_upper_bound, 30);
let univariate_state = retry_context.advance(&mut coefficient_upper_bound).unwrap();
assert_eq!(coefficient_upper_bound, 40);
assert_eq!(univariate_state.order, [2, 0, 1]);
assert_eq!(univariate_state.coefficient_upper_bound, 10);
assert!(
retry_context
.advance(&mut coefficient_upper_bound)
.is_none()
);
let mut bivariate_only = BivariateFactorRetryContext::new(None);
for expected_bound in [60i64, 70, 80, 90] {
assert!(
bivariate_only
.advance(&mut coefficient_upper_bound)
.is_none()
);
assert_eq!(coefficient_upper_bound, expected_bound);
}
}
#[test]
fn bivariate_wang_density_keeps_dense_polybench_geometries() {
assert!(!integer_factor_bivariate_wang_density_supported(36, 2, 2));
assert!(integer_factor_bivariate_wang_density_supported(
2329, 21, 21
));
assert!(integer_factor_bivariate_wang_density_supported(
1665, 17, 19
));
assert!(!integer_factor_bivariate_wang_density_supported(
1803, 24, 20
));
}
#[test]
fn integer_factor_auto_reorders_polybench_8_sharp_84_geometry() {
let degrees = [8, 10, 8, 32, 24, 5, 3, 3];
let leading_layer_lengths = [3, 2, 2, 1, 1, 8, 2, 35];
let mut order = [3, 4, 1, 0, 2, 5, 6, 7];
assert!(reorder_integer_factor_variables_for_sparse_univariate(
true,
1878,
°rees,
&leading_layer_lengths,
&mut order,
));
assert_eq!(order, [6, 3, 4, 1, 0, 2, 5, 7]);
}
#[test]
fn sparse_univariate_bivariate_fallback_restores_original_order() {
let polynomial = parse!("1+x+y+z").to_polynomial::<_, u8>(&Z, None);
let original_order = [0, 1, 2];
let mut speculative_order = [2, 0, 1];
let factors = polynomial.multivariate_factorization_bivariate_fallback(
&mut speculative_order,
Some(&original_order),
10,
Some(1),
None,
);
assert_eq!(speculative_order, original_order);
assert_eq!(factors, vec![polynomial]);
}
#[test]
fn integer_factor_auto_keeps_polybench_8_uniform_159_order() {
let degrees = [12, 15, 21, 13, 15, 14, 16, 16];
let leading_layer_lengths = [18, 2, 1, 2, 3, 4, 2, 2];
let mut order = [2, 6, 7, 1, 4, 5, 3, 0];
let original_order = order;
assert!(!reorder_integer_factor_variables_for_sparse_univariate(
true,
1813,
°rees,
&leading_layer_lengths,
&mut order,
));
assert_eq!(order, original_order);
}
#[test]
fn sparse_univariate_order_reordering_obeys_route_guards() {
let degrees = [8, 10, 8, 32, 24, 5, 3, 3];
let leading_layer_lengths = [3, 2, 2, 1, 1, 8, 2, 35];
let original_order = [3, 4, 1, 0, 2, 5, 6, 7];
let mut order = original_order;
assert!(!reorder_integer_factor_variables_for_sparse_univariate(
false,
1878,
°rees,
&leading_layer_lengths,
&mut order,
));
assert_eq!(order, original_order);
let mut quadratic_degrees = degrees;
quadratic_degrees[5] = 2;
let mut order = original_order;
assert!(!reorder_integer_factor_variables_for_sparse_univariate(
true,
1878,
&quadratic_degrees,
&leading_layer_lengths,
&mut order,
));
assert_eq!(order, original_order);
let mut large_candidate_layer = leading_layer_lengths;
large_candidate_layer[6] = 3;
let mut order = original_order;
assert!(!reorder_integer_factor_variables_for_sparse_univariate(
true,
1878,
°rees,
&large_candidate_layer,
&mut order,
));
assert_eq!(order, original_order);
let small_degrees = [9, 24, 1, 5];
let small_layers = [1, 1, 2, 8];
let small_order = [0, 1, 3, 2];
let mut order = small_order;
assert!(!reorder_integer_factor_variables_for_sparse_univariate(
true,
255,
&small_degrees,
&small_layers,
&mut order,
));
assert_eq!(order, small_order);
let degree_ratio_guard = [11, 24, 3, 5];
let degree_ratio_layers = [1, 1, 2, 8];
let degree_ratio_order = [0, 1, 3, 2];
let mut order = degree_ratio_order;
assert!(!reorder_integer_factor_variables_for_sparse_univariate(
true,
520,
°ree_ratio_guard,
°ree_ratio_layers,
&mut order,
));
assert_eq!(order, degree_ratio_order);
let exact_degree_ratio = [12, 24, 3, 5];
let mut order = degree_ratio_order;
assert!(reorder_integer_factor_variables_for_sparse_univariate(
true,
520,
&exact_degree_ratio,
°ree_ratio_layers,
&mut order,
));
assert_eq!(order, [2, 0, 1, 3]);
let mut order = original_order;
assert!(!reorder_integer_factor_variables_for_sparse_univariate(
true,
1319,
°rees,
&leading_layer_lengths,
&mut order,
));
assert_eq!(order, original_order);
let mut order = original_order;
assert!(!reorder_integer_factor_variables_for_sparse_univariate(
true,
4126,
°rees,
&leading_layer_lengths,
&mut order,
));
assert_eq!(order, original_order);
}
const WANG_RESIDUAL_CONTENT_FACTORS: [&str; 4] = [
"3*m-u-w-v",
"128*m^6-128*m^5*u-352*m^5*w-112*m^5*v+40*m^4*u^2+232*m^4*u*w+100*m^4*u*v+272*m^4*w^2+432*m^4*w*v+32*m^4*v^2-4*m^3*u^3-30*m^3*u^2*w-22*m^3*u^2*v-134*m^3*u*w^2-276*m^3*u*w*v-25*m^3*u*v^2-80*m^3*w^3-266*m^3*w^2*v-210*m^3*w*v^2-3*m^3*v^3-2*m^2*u^3*w+m^2*u^3*v+12*m^2*u^2*w^2+33*m^2*u^2*w*v+3*m^2*u^2*v^2+22*m^2*u*w^3+126*m^2*u*w^2*v+123*m^2*u*w*v^2+2*m^2*u*v^3+8*m^2*w^4+50*m^2*w^3*v+89*m^2*w^2*v^2+47*m^2*w*v^3+m*u^3*w*v-8*m*u^2*w^2*v-11*m*u^2*w*v^2-13*m*u*w^3*v-38*m*u*w^2*v^2-25*m*u*w*v^3-2*m*w^4*v-8*m*w^3*v^2-10*m*w^2*v^3-4*m*w*v^4+u^2*w^2*v^2+u^2*w*v^3+2*u*w^3*v^2+4*u*w^2*v^3+2*u*w*v^4",
"128*m^4-80*m^3*u-80*m^3*w-96*m^3*v+12*m^2*u^2+52*m^2*u*v+12*m^2*w^2+52*m^2*w*v+24*m^2*v^2+9*m*u^2*w-10*m*u^2*v+9*m*u*w^2+4*m*u*w*v-12*m*u*v^2-10*m*w^2*v-12*m*w*v^2-2*m*v^3+u^3*v-3*u^2*w*v+2*u^2*v^2-3*u*w^2*v-2*u*w*v^2+u*v^3+w^3*v+2*w^2*v^2+w*v^3",
"16*m^3-8*m^2*u-8*m^2*w-20*m^2*v+m*u^2+2*m*u*w+6*m*u*v+m*w^2+6*m*w*v+8*m*v^2-u*w*v-u*v^2-w*v^2-v^3",
];
fn wang_residual_content_input() -> String {
WANG_RESIDUAL_CONTENT_FACTORS
.iter()
.map(|f| format!("({f})"))
.collect::<Vec<_>>()
.join("*")
}
#[test]
fn wang_univariate_lcoeff_keeps_residual_content_separate() {
let vars = Some(Arc::new(vec![
symbol!("m").into(),
symbol!("u").into(),
symbol!("w").into(),
symbol!("v").into(),
]));
let poly = parse!(wang_residual_content_input())
.expand()
.to_polynomial::<_, u16>(&Z, vars);
let mut order = vec![0, 3, 2, 1];
let (_, lcoeff_divs, _, _, _, univariate_content) =
poly.find_univariate_sample(&mut order, 10, None).unwrap();
let lcoeff_div_product = lcoeff_divs
.iter()
.fold(poly.one(), |product, lcoeff| &product * lcoeff);
let actual_lcoeff = poly.univariate_lcoeff(order[0]);
assert!(!univariate_content.is_one());
assert_ne!(lcoeff_div_product, actual_lcoeff);
assert_eq!(
lcoeff_div_product.mul_coeff(univariate_content),
actual_lcoeff
);
}
#[test]
fn factor_multivariate_wang_residual_content() {
let vars = Some(Arc::new(vec![
symbol!("m").into(),
symbol!("u").into(),
symbol!("w").into(),
symbol!("v").into(),
]));
let poly = parse!(wang_residual_content_input())
.expand()
.to_polynomial::<_, u16>(&Z, vars);
let mut res = WANG_RESIDUAL_CONTENT_FACTORS
.iter()
.map(|f| {
(
parse!(f)
.expand()
.to_polynomial(&Z, poly.variables().clone()),
1,
)
})
.collect::<Vec<_>>();
res.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
let mut r = poly.factor();
r.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
assert_eq!(r, res);
}
#[test]
fn factor_overall_minus() {
let poly = parse!("-v1*v3^2-v1*v2*v3^2").to_polynomial::<_, u8>(
&Z,
Some(Arc::new(vec![
symbol!("v1").into(),
symbol!("v2").into(),
symbol!("v3").into(),
])),
);
let res = [("-1", 1), ("v3", 2), ("1+v2", 1), ("v1", 1)];
let mut res = res
.iter()
.map(|(f, p)| {
(
parse!(f)
.expand()
.to_polynomial(&Z, poly.variables().clone()),
*p,
)
})
.collect::<Vec<_>>();
res.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
let mut r = poly.factor();
r.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
assert_eq!(r, res);
}
#[test]
fn factor_multivariate_2() {
let poly = parse!("v2^2*v3-v1*v2*v3+v1*v2*v3^2+v1*v2^2-v1^2*v3^2+v1^2*v2*v3")
.to_polynomial::<_, u8>(
&Z,
Some(Arc::new(vec![
symbol!("v1").into(),
symbol!("v2").into(),
symbol!("v3").into(),
])),
);
let res = [("v2+v1*v3", 1), ("v2*v3-v1*v3+v1*v2", 1)];
let mut res = res
.iter()
.map(|(f, p)| {
(
parse!(f)
.expand()
.to_polynomial(&Z, poly.variables().clone()),
*p,
)
})
.collect::<Vec<_>>();
res.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
let mut r = poly.factor();
r.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
assert_eq!(r, res);
}
#[test]
fn galois_upgrade() {
let a =
parse!("x^7(y^5+y^4+y^3+y^2)+x^5(y^3+y)+x^4(y^4+y)+x^3(y^2+y)+x^2y+x*y^2+x*y+x+y+1")
.to_polynomial::<_, u8>(&Z2, None);
assert_eq!(a.factor().len(), 2)
}
#[test]
fn algebraic_extension() {
let a = parse!("z^4+z^3+(2+a-a^2)z^2+(1+a^2-2a^3)z-2").to_polynomial::<_, u8>(&Q, None);
let f = parse!("a^4-3").to_polynomial::<_, u16>(&Q, None);
let f = AlgebraicExtension::new(f);
let mut factors = a.to_number_field(&f).factor();
let f1 = parse!("(1-a^2)+(1-a)*z+z^2")
.to_polynomial::<_, u8>(&Q, a.get_vars().clone())
.to_number_field(&f);
let f2 = parse!("(1+a^2)+(a)*z+z^2")
.to_polynomial::<_, u8>(&Q, a.get_vars().clone())
.to_number_field(&f);
factors.sort_by(|a, b| a.0.internal_cmp(&b.0).then(a.1.cmp(&b.1)));
assert_eq!(factors, vec![(f1, 1), (f2, 1)])
}
}