#![allow(clippy::needless_range_loop)]
use super::cert::SosPoly;
use super::gram::monomial_basis;
use super::linalg::{psd_decompose, solve_affine};
use super::lp::{Lp, LpStatus, Rel};
use super::ratpoly::{Exponents, RatPoly};
use super::sdp::{min_eigenvalue, smallest_magnitude_eigenvectors, Family};
use rug::Rational;
use std::collections::{BTreeMap, BTreeSet};
struct SplitMix64(u64);
impl SplitMix64 {
fn new(seed: u64) -> Self {
SplitMix64(seed)
}
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn next_signed(&mut self, scale: f64) -> f64 {
let bits = self.next_u64() >> 11; let u = (bits as f64) * (1.0 / (1u64 << 53) as f64); (2.0 * u - 1.0) * scale
}
}
fn add_exp(a: &[u32], b: &[u32]) -> Exponents {
a.iter().zip(b).map(|(x, y)| x + y).collect()
}
fn pack_len(n: usize) -> usize {
n * (n + 1) / 2
}
fn unpack(n: usize, v: &[Rational]) -> Vec<Vec<Rational>> {
let mut q = vec![vec![Rational::from(0); n]; n];
let mut idx = 0;
for i in 0..n {
for j in i..n {
q[i][j] = v[idx].clone();
q[j][i] = v[idx].clone();
idx += 1;
}
}
q
}
fn gram_system(target: &RatPoly, basis: &[Exponents]) -> (Vec<Vec<Rational>>, Vec<Rational>) {
let n = basis.len();
let ncols = pack_len(n);
let mut col_of: BTreeMap<(usize, usize), usize> = BTreeMap::new();
let mut idx = 0;
for i in 0..n {
for j in i..n {
col_of.insert((i, j), idx);
idx += 1;
}
}
let mut rows: BTreeMap<Exponents, Vec<Rational>> = BTreeMap::new();
for i in 0..n {
for j in i..n {
let e = add_exp(&basis[i], &basis[j]);
let coeff = if i == j {
Rational::from(1)
} else {
Rational::from(2)
};
let row = rows
.entry(e)
.or_insert_with(|| vec![Rational::from(0); ncols]);
row[col_of[&(i, j)]] += coeff;
}
}
let mut all_exps: BTreeSet<Exponents> = rows.keys().cloned().collect();
all_exps.extend(target.terms().keys().cloned());
let mut out_rows = Vec::with_capacity(all_exps.len());
let mut out_rhs = Vec::with_capacity(all_exps.len());
for e in &all_exps {
let row = rows
.get(e)
.cloned()
.unwrap_or_else(|| vec![Rational::from(0); ncols]);
out_rows.push(row);
out_rhs.push(target.coeff(e));
}
(out_rows, out_rhs)
}
const MAX_NEWTON_REDUCTION_WORK: usize = 400_000;
fn in_convex_hull(support: &[Exponents], point: &[u32]) -> bool {
let mut lp = Lp::new(support.len());
for k in 0..point.len() {
let row: Vec<Rational> = support.iter().map(|s| Rational::from(s[k])).collect();
lp.constrain(row, Rel::Eq, Rational::from(point[k]));
}
lp.constrain(
vec![Rational::from(1); support.len()],
Rel::Eq,
Rational::from(1),
);
!matches!(lp.solve(), LpStatus::Infeasible)
}
fn half_newton_reduce(target: &RatPoly, basis: Vec<Exponents>) -> Vec<Exponents> {
let support: Vec<Exponents> = target.terms().keys().cloned().collect();
if support.is_empty() || basis.is_empty() {
return basis;
}
if basis.len().saturating_mul(support.len()) > MAX_NEWTON_REDUCTION_WORK {
return basis;
}
let nvars = support[0].len();
let mut hi = vec![0u32; nvars];
for s in &support {
for k in 0..nvars {
hi[k] = hi[k].max(s[k]);
}
}
basis
.into_iter()
.filter(|e| {
let doubled: Vec<u32> = e.iter().map(|c| 2 * c).collect();
if (0..nvars).any(|k| doubled[k] > hi[k]) {
return false;
}
in_convex_hull(&support, &doubled)
})
.collect()
}
fn rat_to_f64(r: &Rational) -> f64 {
r.to_f64()
}
fn frob_dot(a: &[Vec<f64>], b: &[Vec<f64>]) -> f64 {
a.iter()
.zip(b)
.map(|(ra, rb)| ra.iter().zip(rb).map(|(x, y)| x * y).sum::<f64>())
.sum()
}
fn orthonormalize(dirs: &[Vec<Vec<f64>>]) -> (Vec<Vec<Vec<f64>>>, Vec<Vec<f64>>) {
let m = dirs.len();
let mut e: Vec<Vec<Vec<f64>>> = dirs.to_vec();
let mut r = vec![vec![0.0f64; m]; m];
for k in 0..m {
for j in 0..k {
let proj = frob_dot(&e[j], &e[k]);
r[j][k] = proj;
let (ej, ek) = {
let (left, right) = e.split_at_mut(k);
(&left[j], &mut right[0])
};
for (row_k, row_j) in ek.iter_mut().zip(ej.iter()) {
for (v_k, v_j) in row_k.iter_mut().zip(row_j.iter()) {
*v_k -= proj * v_j;
}
}
}
let norm = frob_dot(&e[k], &e[k]).sqrt();
r[k][k] = norm;
if norm > 1e-12 {
for row in e[k].iter_mut() {
for v in row.iter_mut() {
*v /= norm;
}
}
}
}
(e, r)
}
fn back_substitute_upper(r: &[Vec<f64>], s: &[f64]) -> Option<Vec<f64>> {
let m = s.len();
let mut t = vec![0.0f64; m];
for k in (0..m).rev() {
let mut acc = s[k];
for (j, tj) in t.iter().enumerate().skip(k + 1) {
acc -= r[k][j] * tj;
}
if r[k][k].abs() < 1e-12 {
return None;
}
t[k] = acc / r[k][k];
}
Some(t)
}
fn round_to_rational(x: f64, max_den: i64) -> Option<Rational> {
if !x.is_finite() {
return None;
}
if x == 0.0 {
return Some(Rational::from(0));
}
let neg = x < 0.0;
let mut val = x.abs();
let (mut h_prev, mut h_cur): (i64, i64) = (0, 1);
let (mut k_prev, mut k_cur): (i64, i64) = (1, 0);
for _ in 0..40 {
if !val.is_finite() {
break;
}
let a_f = val.floor();
if a_f.abs() > 1e15 {
break;
}
let a = a_f as i64;
let h_next = a.checked_mul(h_cur).and_then(|v| v.checked_add(h_prev));
let k_next = a.checked_mul(k_cur).and_then(|v| v.checked_add(k_prev));
let (Some(h_next), Some(k_next)) = (h_next, k_next) else {
break;
};
if k_next > max_den || k_next <= 0 {
break;
}
h_prev = h_cur;
h_cur = h_next;
k_prev = k_cur;
k_cur = k_next;
let frac = val - a_f;
if frac < 1e-13 {
break;
}
val = 1.0 / frac;
}
if k_cur == 0 {
None
} else {
let mag = Rational::from((h_cur, k_cur));
Some(if neg { -mag } else { mag })
}
}
const DENOM_CAPS: &[i64] = &[1, 4, 16, 64, 256, 1024, 4096, 16384, 65536];
const FLOOR_SCHEDULE: &[f64] = &[
1.0, 0.3, 0.1, 0.03, 0.01, 0.003, 0.001, 0.0003, 0.0001, 0.00003, 0.00001, 0.0,
];
const RANDOM_RESTARTS: usize = 4;
const RESTART_SCALES: &[f64] = &[1.0, 4.0];
const ROUNDING_CANDIDATES: usize = 6;
const MAX_FREE_PARAMETERS: usize = 200;
fn ceiling_note(basis_len: usize, free_params: usize, at_least: bool) -> String {
let qualifier = if at_least { "at least " } else { "" };
format!(
"PSD Gram search: NOT SEARCHED — the affine Gram family over this {basis_len}-monomial \
basis has {qualifier}{free_params} free parameters, above the numeric-search ceiling of \
{MAX_FREE_PARAMETERS}; no search was attempted at this basis, so this is a budget that \
fired, not a search that came up empty"
)
}
fn row_count(target: &RatPoly, basis: &[Exponents]) -> usize {
let mut exps: BTreeSet<Exponents> = target.terms().keys().cloned().collect();
for i in 0..basis.len() {
for j in i..basis.len() {
exps.insert(add_exp(&basis[i], &basis[j]));
}
}
exps.len()
}
const DR_LAMBDAS: &[f64] = &[1.0];
const DR_ITERS: usize = 15_000;
const DR_POLISH_CANDIDATES: usize = 2;
fn anneal_from(family: &Family, start: Vec<f64>) -> Vec<f64> {
let mut t = start;
for &floor in FLOOR_SCHEDULE {
if !super::budget_ok() {
return t;
}
if let Some(next) = family.search_from(t.clone(), floor, 150) {
t = next;
}
}
t
}
fn multistart_anneal(family: &Family, dim: usize) -> Vec<Vec<f64>> {
let mut starts: Vec<Vec<f64>> = vec![vec![0.0; dim]];
let mut rng = SplitMix64::new(0xC0FFEE_D15EA5E5);
for &scale in RESTART_SCALES {
for _ in 0..RANDOM_RESTARTS {
starts.push((0..dim).map(|_| rng.next_signed(scale)).collect());
}
}
let mut results: Vec<(f64, Vec<f64>)> = starts
.into_iter()
.map(|start| {
let t = anneal_from(family, start);
let eig = min_eigenvalue(&family.at(&t));
(eig, t)
})
.collect();
results.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
for (eig, t) in results.iter_mut().take(DR_POLISH_CANDIDATES) {
if !super::budget_ok() {
break;
}
for &lambda in DR_LAMBDAS {
if let Some(cand) = family.douglas_rachford_from(t.clone(), 0.0, lambda, DR_ITERS) {
let cand_eig = min_eigenvalue(&family.at(&cand));
if cand_eig > *eig {
*eig = cand_eig;
*t = cand;
}
}
}
}
results.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
results.into_iter().map(|(_, t)| t).collect()
}
fn rat_family_at(
base: &[Vec<Rational>],
dirs: &[Vec<Vec<Rational>>],
t: &[Rational],
) -> Vec<Vec<Rational>> {
let n = base.len();
let mut q = base.to_vec();
for (tk, dir) in t.iter().zip(dirs.iter()) {
if *tk == 0 {
continue;
}
for i in 0..n {
for j in 0..n {
if dir[i][j] != 0 {
q[i][j] += Rational::from(tk * &dir[i][j]);
}
}
}
}
q
}
fn search_rational_family(
nvars: usize,
basis: &[Exponents],
target: &RatPoly,
base_rat: &[Vec<Rational>],
dirs_rat: &[Vec<Vec<Rational>>],
log: &mut Vec<String>,
) -> (Option<SosPoly>, Vec<Vec<Vec<f64>>>) {
let try_point = |t: &[Rational]| -> Option<SosPoly> {
let q = rat_family_at(base_rat, dirs_rat, t);
let decomp = psd_decompose(&q)?;
let mut sos = SosPoly::default();
for (d, v) in decomp {
if d <= 0 {
continue;
}
let mut square = RatPoly::zero(nvars);
for (u, c) in v.iter().enumerate() {
if *c != 0 {
square = square.add(&RatPoly::monomial(nvars, basis[u].clone(), c.clone()));
}
}
sos.push(d, square);
}
if sos.to_poly(nvars) == *target {
Some(sos)
} else {
None
}
};
if dirs_rat.is_empty() {
return (try_point(&[]), Vec::new());
}
if dirs_rat.len() > MAX_FREE_PARAMETERS {
log.push(ceiling_note(basis.len(), dirs_rat.len(), false));
return (None, Vec::new());
}
log.push(format!(
"PSD Gram search: searching a {}-parameter affine family over a {}-monomial basis",
dirs_rat.len(),
basis.len()
));
let base: Vec<Vec<f64>> = base_rat
.iter()
.map(|row| row.iter().map(rat_to_f64).collect())
.collect();
let dirs: Vec<Vec<Vec<f64>>> = dirs_rat
.iter()
.map(|dir| {
dir.iter()
.map(|row| row.iter().map(rat_to_f64).collect())
.collect()
})
.collect();
let (ortho_dirs, r) = orthonormalize(&dirs);
let family = Family::new(base, ortho_dirs);
let dim = dirs.len();
let candidates = multistart_anneal(&family, dim);
let candidate_matrices: Vec<Vec<Vec<f64>>> = candidates.iter().map(|s| family.at(s)).collect();
for s in candidates.iter().take(ROUNDING_CANDIDATES) {
if !super::budget_ok() {
return (None, candidate_matrices);
}
let Some(t) = back_substitute_upper(&r, s) else {
continue;
};
for &max_den in DENOM_CAPS {
let t_rat: Option<Vec<Rational>> =
t.iter().map(|x| round_to_rational(*x, max_den)).collect();
let Some(t_rat) = t_rat else { continue };
if let Some(sos) = try_point(&t_rat) {
return (Some(sos), candidate_matrices);
}
}
}
(None, candidate_matrices)
}
const FACIAL_CORANK_GUESSES: &[usize] = &[1, 2, 3];
const FACIAL_DENOM_CAPS: &[i64] = &[64, 4096];
const FACIAL_CANDIDATES: usize = 2;
const FACIAL_SEARCH_BUDGET: usize = 3;
#[allow(clippy::type_complexity)]
fn restrict_by_near_null_vectors(
base_rat: &[Vec<Rational>],
dirs_rat: &[Vec<Vec<Rational>>],
vs: &[Vec<Rational>],
) -> Option<(Vec<Vec<Rational>>, Vec<Vec<Vec<Rational>>>)> {
let n = base_rat.len();
let dim = dirs_rat.len();
let mut rows = Vec::with_capacity(vs.len() * n);
let mut rhs = Vec::with_capacity(vs.len() * n);
for v in vs {
for i in 0..n {
let mut row = vec![Rational::from(0); dim];
for (k, dir) in dirs_rat.iter().enumerate() {
let mut acc = Rational::from(0);
for j in 0..n {
if dir[i][j] != 0 && v[j] != 0 {
acc += Rational::from(&dir[i][j] * &v[j]);
}
}
row[k] = acc;
}
let mut b = Rational::from(0);
for j in 0..n {
if base_rat[i][j] != 0 && v[j] != 0 {
b += Rational::from(&base_rat[i][j] * &v[j]);
}
}
rows.push(row);
rhs.push(-b);
}
}
let sol = solve_affine(&rows, &rhs)?;
let mut new_base = base_rat.to_vec();
for (k, tk) in sol.particular.iter().enumerate() {
if *tk == 0 {
continue;
}
for i in 0..n {
for j in 0..n {
if dirs_rat[k][i][j] != 0 {
new_base[i][j] += Rational::from(tk * &dirs_rat[k][i][j]);
}
}
}
}
let mut new_dirs = Vec::with_capacity(sol.nullspace.len());
for coeffs in &sol.nullspace {
let mut nd = vec![vec![Rational::from(0); n]; n];
for (k, ck) in coeffs.iter().enumerate() {
if *ck == 0 {
continue;
}
for i in 0..n {
for j in 0..n {
if dirs_rat[k][i][j] != 0 {
nd[i][j] += Rational::from(ck * &dirs_rat[k][i][j]);
}
}
}
}
new_dirs.push(nd);
}
Some((new_base, new_dirs))
}
#[allow(clippy::too_many_arguments)]
fn facial_reduction_search(
nvars: usize,
basis: &[Exponents],
target: &RatPoly,
base_rat: &[Vec<Rational>],
dirs_rat: &[Vec<Vec<Rational>>],
candidates: &[Vec<Vec<f64>>],
log: &mut Vec<String>,
) -> Option<SosPoly> {
let max_corank = *FACIAL_CORANK_GUESSES.iter().max().unwrap_or(&0);
let mut budget = FACIAL_SEARCH_BUDGET;
for q_best in candidates.iter().take(FACIAL_CANDIDATES) {
let near_null = smallest_magnitude_eigenvectors(q_best, max_corank);
for &corank in FACIAL_CORANK_GUESSES {
if budget == 0 {
return None;
}
if corank > near_null.len() {
continue;
}
for &max_den in FACIAL_DENOM_CAPS {
let vs: Option<Vec<Vec<Rational>>> = near_null[..corank]
.iter()
.map(|v| {
v.iter()
.map(|&x| round_to_rational(x, max_den))
.collect::<Option<Vec<_>>>()
})
.collect();
let Some(vs) = vs else { continue };
let Some((new_base, new_dirs)) =
restrict_by_near_null_vectors(base_rat, dirs_rat, &vs)
else {
continue;
};
if new_dirs.len() >= dirs_rat.len() {
continue;
}
if budget == 0 {
return None;
}
budget -= 1;
let (found, _deeper) =
search_rational_family(nvars, basis, target, &new_base, &new_dirs, log);
if found.is_some() {
return found;
}
}
}
}
None
}
fn pack(n: usize, m: &[Vec<Rational>]) -> Vec<Rational> {
let mut v = Vec::with_capacity(pack_len(n));
for i in 0..n {
for j in i..n {
v.push(m[i][j].clone());
}
}
v
}
const MAX_SYMMETRY_NVARS: usize = 6;
fn permutations(nvars: usize) -> Vec<Vec<usize>> {
fn helper(items: &mut Vec<usize>, k: usize, out: &mut Vec<Vec<usize>>) {
if k == items.len() {
out.push(items.clone());
return;
}
for i in k..items.len() {
items.swap(k, i);
helper(items, k + 1, out);
items.swap(k, i);
}
}
let mut items: Vec<usize> = (0..nvars).collect();
let mut out = Vec::new();
helper(&mut items, 0, &mut out);
out
}
fn transform_exponents(e: &Exponents, perm: &[usize], signs: &[i8]) -> (Exponents, i8) {
let mut e2 = vec![0u32; e.len()];
let mut sign = 1i8;
for i in 0..e.len() {
e2[perm[i]] = e[i];
if signs[i] < 0 && e[i] % 2 == 1 {
sign = -sign;
}
}
(e2, sign)
}
fn symmetry_fixes_target(target: &RatPoly, perm: &[usize], signs: &[i8]) -> bool {
for (e, c) in target.terms() {
let (e2, sign) = transform_exponents(e, perm, signs);
let expected = if sign < 0 { -c.clone() } else { c.clone() };
if target.coeff(&e2) != expected {
return false;
}
}
true
}
fn detect_polynomial_symmetry_group(target: &RatPoly, nvars: usize) -> Vec<(Vec<usize>, Vec<i8>)> {
if nvars == 0 || nvars > MAX_SYMMETRY_NVARS {
return vec![((0..nvars).collect(), vec![1i8; nvars])];
}
let mut group = Vec::new();
for perm in permutations(nvars) {
for mask in 0..(1u32 << nvars) {
let signs: Vec<i8> = (0..nvars)
.map(|i| if (mask >> i) & 1 == 1 { -1 } else { 1 })
.collect();
if symmetry_fixes_target(target, &perm, &signs) {
group.push((perm.clone(), signs));
}
}
}
group
}
fn signed_permutation_action(
basis: &[Exponents],
perm: &[usize],
signs: &[i8],
) -> Vec<(usize, i8)> {
basis
.iter()
.map(|e| {
let (e2, sign) = transform_exponents(e, perm, signs);
let j = basis
.iter()
.position(|b| *b == e2)
.expect("basis is closed under variable permutation/sign flip");
(j, sign)
})
.collect()
}
fn conjugate_by_action(m: &[Vec<Rational>], action: &[(usize, i8)]) -> Vec<Vec<Rational>> {
let n = m.len();
let mut out = vec![vec![Rational::from(0); n]; n];
for (i1, &(j1, s1)) in action.iter().enumerate() {
for (i2, &(j2, s2)) in action.iter().enumerate() {
if m[i1][i2] == 0 {
continue;
}
out[j1][j2] = if s1 * s2 < 0 {
-m[i1][i2].clone()
} else {
m[i1][i2].clone()
};
}
}
out
}
fn symmetrize_matrix(m: &[Vec<Rational>], actions: &[Vec<(usize, i8)>]) -> Vec<Vec<Rational>> {
let n = m.len();
let mut acc = vec![vec![Rational::from(0); n]; n];
for action in actions {
let c = conjugate_by_action(m, action);
for i in 0..n {
for j in 0..n {
if c[i][j] != 0 {
acc[i][j] += &c[i][j];
}
}
}
}
let g = Rational::from(actions.len() as i64);
for row in acc.iter_mut() {
for v in row.iter_mut() {
if *v != 0 {
*v /= &g;
}
}
}
acc
}
fn row_reduce_basis(mut rows: Vec<Vec<Rational>>) -> Vec<Vec<Rational>> {
let Some(ncols) = rows.first().map(|r| r.len()) else {
return Vec::new();
};
let m = rows.len();
let mut r = 0usize;
for c in 0..ncols {
let Some(p) = (r..m).find(|&i| rows[i][c] != 0) else {
continue;
};
rows.swap(r, p);
let inv = rows[r][c].clone();
for v in rows[r].iter_mut() {
*v /= &inv;
}
let prow = rows[r].clone();
for (i, row) in rows.iter_mut().enumerate() {
if i == r || row[c] == 0 {
continue;
}
let f = row[c].clone();
for (t, pv) in row.iter_mut().zip(prow.iter()) {
*t -= Rational::from(&f * pv);
}
}
r += 1;
if r == m {
break;
}
}
rows.truncate(r);
rows
}
const MIN_PARAMS_FOR_SYMMETRY_SEARCH: usize = 100;
fn symmetry_reduced_search(
nvars: usize,
basis: &[Exponents],
target: &RatPoly,
base_rat: &[Vec<Rational>],
dirs_rat: &[Vec<Vec<Rational>>],
log: &mut Vec<String>,
) -> Option<SosPoly> {
if nvars == 0 || nvars > MAX_SYMMETRY_NVARS || dirs_rat.len() < MIN_PARAMS_FOR_SYMMETRY_SEARCH {
return None;
}
let group = detect_polynomial_symmetry_group(target, nvars);
if group.len() <= 1 {
return None;
}
let actions: Vec<Vec<(usize, i8)>> = group
.iter()
.map(|(perm, signs)| signed_permutation_action(basis, perm, signs))
.collect();
let n = basis.len();
let sym_base = symmetrize_matrix(base_rat, &actions);
let sym_dirs_packed: Vec<Vec<Rational>> = dirs_rat
.iter()
.map(|d| pack(n, &symmetrize_matrix(d, &actions)))
.collect();
let reduced_packed = row_reduce_basis(sym_dirs_packed);
if reduced_packed.len() >= dirs_rat.len() {
return None;
}
let sym_dirs: Vec<Vec<Vec<Rational>>> = reduced_packed.iter().map(|v| unpack(n, v)).collect();
let (found, sym_candidates) =
search_rational_family(nvars, basis, target, &sym_base, &sym_dirs, log);
if found.is_some() {
return found;
}
if sym_candidates.is_empty() {
return None;
}
facial_reduction_search(
nvars,
basis,
target,
&sym_base,
&sym_dirs,
&sym_candidates,
log,
)
}
pub fn psd_search_logged(
target: &RatPoly,
basis_deg: u32,
log: &mut Vec<String>,
) -> Option<SosPoly> {
let nvars = target.nvars();
let (basis, homogeneous_len) = restricted_basis(target, basis_deg);
if basis.len() < homogeneous_len {
log.push(format!(
"half-Newton-polytope reduction: {homogeneous_len} → {} monomials in the Gram basis",
basis.len()
));
}
let n = basis.len();
if n == 0 {
return if target.is_zero() {
Some(SosPoly::default())
} else {
None
};
}
let min_free = pack_len(n).saturating_sub(row_count(target, &basis));
if min_free > MAX_FREE_PARAMETERS {
log.push(ceiling_note(n, min_free, true));
return None;
}
let (rows, rhs) = gram_system(target, &basis);
let sol = solve_affine(&rows, &rhs)?;
let base_rat = unpack(n, &sol.particular);
let dirs_rat: Vec<Vec<Vec<Rational>>> = sol.nullspace.iter().map(|d| unpack(n, d)).collect();
let (found, candidates) =
search_rational_family(nvars, &basis, target, &base_rat, &dirs_rat, log);
if found.is_some() {
return found;
}
if candidates.is_empty() {
return None;
}
if let Some(found) = facial_reduction_search(
nvars,
&basis,
target,
&base_rat,
&dirs_rat,
&candidates,
log,
) {
return Some(found);
}
symmetry_reduced_search(nvars, &basis, target, &base_rat, &dirs_rat, log)
}
pub fn psd_search(target: &RatPoly, basis_deg: u32) -> Option<SosPoly> {
let mut log = Vec::new();
psd_search_logged(target, basis_deg, &mut log)
}
fn restricted_basis(target: &RatPoly, basis_deg: u32) -> (Vec<Exponents>, usize) {
let nvars = target.nvars();
let basis: Vec<Exponents> = match target.is_homogeneous() {
Some(d) if d == 2 * basis_deg => monomial_basis(nvars, basis_deg)
.into_iter()
.filter(|e| e.iter().sum::<u32>() == basis_deg)
.collect(),
_ => monomial_basis(nvars, basis_deg),
};
let homogeneous_len = basis.len();
(half_newton_reduce(target, basis), homogeneous_len)
}
pub fn searched_basis_len(target: &RatPoly, basis_deg: u32) -> usize {
restricted_basis(target, basis_deg).0.len()
}
#[cfg(test)]
mod tests {
use super::*;
fn r(n: i64, d: i64) -> Rational {
Rational::from((n, d))
}
#[test]
fn psd_search_finds_the_dsos_reachable_case() {
let mut p = RatPoly::monomial(2, vec![2, 0], Rational::from(1));
p = p.add(&RatPoly::monomial(2, vec![1, 1], Rational::from(-2)));
p = p.add(&RatPoly::monomial(2, vec![0, 2], Rational::from(2)));
let sos = psd_search(&p, 1).expect("PSD search should find a certificate");
assert_eq!(sos.to_poly(2), p);
}
#[test]
fn psd_search_finds_a_unique_rational_gram_matrix() {
let mut p = RatPoly::monomial(2, vec![2, 0], r(1, 4));
p = p.add(&RatPoly::monomial(2, vec![1, 1], r(1, 3)));
p = p.add(&RatPoly::monomial(2, vec![0, 2], r(1, 9)));
let sos = psd_search(&p, 1).expect("PSD search should find a certificate");
assert_eq!(sos.to_poly(2), p);
}
#[test]
fn psd_search_refuses_unreachable_degree() {
let p = RatPoly::monomial(1, vec![4], Rational::from(1));
assert!(psd_search(&p, 1).is_none());
}
#[test]
fn psd_search_refuses_a_negative_definite_form() {
let p = RatPoly::monomial(2, vec![2, 0], Rational::from(-1)).add(&RatPoly::monomial(
2,
vec![0, 2],
Rational::from(-1),
));
assert!(psd_search(&p, 1).is_none());
}
#[test]
fn psd_search_finds_a_certificate_with_genuine_off_diagonal_freedom() {
let mut p = RatPoly::monomial(2, vec![4, 0], Rational::from(1));
p = p.add(&RatPoly::monomial(2, vec![0, 4], Rational::from(1)));
p = p.add(&RatPoly::monomial(2, vec![2, 2], Rational::from(2)));
let sos = psd_search(&p, 2).expect("PSD search should find a certificate");
assert_eq!(sos.to_poly(2), p);
}
#[test]
fn psd_search_certifies_homogeneous_motzkin_times_sum_of_squares_at_n1() {
let mut m = RatPoly::monomial(3, vec![4, 2, 0], Rational::from(1));
m = m.add(&RatPoly::monomial(3, vec![2, 4, 0], Rational::from(1)));
m = m.add(&RatPoly::monomial(3, vec![2, 2, 2], Rational::from(-3)));
m = m.add(&RatPoly::monomial(3, vec![0, 0, 6], Rational::from(1)));
assert_eq!(m.is_homogeneous(), Some(6));
let sigma = RatPoly::sum_of_squares(3);
let q = m.mul(&sigma);
assert_eq!(q.is_homogeneous(), Some(8));
let (reduced, homogeneous_len) = restricted_basis(&q, 4);
assert_eq!(homogeneous_len, 15);
assert_eq!(reduced.len(), 9);
let sos = psd_search(&q, 4).expect(
"(x^2+y^2+z^2)·Motzkin_hom is a sum of squares at N=1 — the classical fact that \
makes Motzkin the standard PSD-not-SOS example — so the search must find a \
certificate here",
);
assert_eq!(sos.to_poly(3), q);
}
#[test]
fn psd_search_certifies_choi_lam_times_sum_of_squares_at_n1() {
let mono = |ex: Vec<u32>, c: i64| RatPoly::monomial(4, ex, Rational::from(c));
let mut cl = mono(vec![2, 2, 0, 0], 1);
cl = cl.add(&mono(vec![0, 2, 2, 0], 1));
cl = cl.add(&mono(vec![2, 0, 2, 0], 1));
cl = cl.add(&mono(vec![0, 0, 0, 4], 1));
cl = cl.add(&mono(vec![1, 1, 1, 1], -4));
assert_eq!(cl.is_homogeneous(), Some(4));
let q = cl.mul(&RatPoly::sum_of_squares(4));
assert_eq!(q.is_homogeneous(), Some(6));
let sos = psd_search(&q, 3)
.expect("(Σxᵢ²)·Choi–Lam is SOS at N=1; the search should find a certificate");
assert_eq!(sos.to_poly(4), q);
}
#[test]
fn half_newton_reduction_is_a_no_op_when_the_polytope_is_already_full() {
let mono = |ex: Vec<u32>, c: i64| RatPoly::monomial(3, ex, Rational::from(c));
let mut r = mono(vec![6, 0, 0], 1);
r = r.add(&mono(vec![0, 6, 0], 1));
r = r.add(&mono(vec![0, 0, 6], 1));
r = r.add(&mono(vec![4, 2, 0], -1));
r = r.add(&mono(vec![2, 4, 0], -1));
r = r.add(&mono(vec![0, 4, 2], -1));
r = r.add(&mono(vec![0, 2, 4], -1));
r = r.add(&mono(vec![4, 0, 2], -1));
r = r.add(&mono(vec![2, 0, 4], -1));
r = r.add(&mono(vec![2, 2, 2], 3));
let q = r.mul(&RatPoly::sum_of_squares(3));
let (reduced, homogeneous_len) = restricted_basis(&q, 4);
assert_eq!(homogeneous_len, 15);
assert_eq!(reduced.len(), 15, "Robinson admits no Newton reduction");
let (reduced_direct, direct_len) = restricted_basis(&r, 3);
assert_eq!((direct_len, reduced_direct.len()), (10, 10));
let mut c = mono(vec![4, 2, 0], 1);
c = c.add(&mono(vec![0, 4, 2], 1));
c = c.add(&mono(vec![2, 0, 4], 1));
c = c.add(&mono(vec![2, 2, 2], -3));
let (reduced_cyclic, cyclic_len) = restricted_basis(&c.mul(&RatPoly::sum_of_squares(3)), 4);
assert_eq!((cyclic_len, reduced_cyclic.len()), (15, 9));
}
#[test]
fn the_free_parameter_ceiling_is_reported_not_silently_dropped() {
let mono = |ex: Vec<u32>, c: i64| RatPoly::monomial(3, ex, Rational::from(c));
let mut p = mono(vec![8, 0, 0], 1);
p = p.add(&mono(vec![0, 8, 0], 1));
p = p.add(&mono(vec![0, 0, 8], 1));
p = p.add(&mono(vec![0, 0, 0], 1));
let mut log = Vec::new();
assert!(psd_search_logged(&p, 4, &mut log).is_none());
let ceiling = log
.iter()
.find(|l| l.contains("NOT SEARCHED"))
.unwrap_or_else(|| panic!("the free-parameter ceiling must be logged; got {log:?}"));
assert!(
ceiling.contains("465") && ceiling.contains(&MAX_FREE_PARAMETERS.to_string()),
"the ceiling report must name both the family size and the ceiling: {ceiling}"
);
assert!(
!log.iter().any(|l| l.contains("searching a")),
"nothing was searched, so nothing may claim to have searched: {log:?}"
);
}
#[test]
fn a_search_that_really_runs_says_so() {
let mut p = RatPoly::monomial(2, vec![4, 0], Rational::from(1));
p = p.add(&RatPoly::monomial(2, vec![0, 4], Rational::from(1)));
p = p.add(&RatPoly::monomial(2, vec![2, 2], Rational::from(2)));
let mut log = Vec::new();
assert!(psd_search_logged(&p, 2, &mut log).is_some());
assert!(
log.iter().any(|l| l.contains("searching a")),
"a search that runs must record that it ran: {log:?}"
);
assert!(!log.iter().any(|l| l.contains("NOT SEARCHED")));
}
#[test]
fn symmetry_group_and_zero_vector_shrink_n2_family() {
let mut m = RatPoly::monomial(3, vec![4, 2, 0], Rational::from(1));
m = m.add(&RatPoly::monomial(3, vec![2, 4, 0], Rational::from(1)));
m = m.add(&RatPoly::monomial(3, vec![2, 2, 2], Rational::from(-3)));
m = m.add(&RatPoly::monomial(3, vec![0, 0, 6], Rational::from(1)));
let sigma2 = RatPoly::sum_of_squares(3).pow(2);
let q = m.mul(&sigma2);
assert_eq!(q.is_homogeneous(), Some(10));
assert_eq!(m.eval(&[r(1, 1), r(1, 1), r(1, 1)]), Rational::from(0));
assert_eq!(q.eval(&[r(1, 1), r(1, 1), r(1, 1)]), Rational::from(0));
let basis: Vec<Exponents> = monomial_basis(3, 5)
.into_iter()
.filter(|e| e.iter().sum::<u32>() == 5)
.collect();
let n = basis.len();
let (rows, rhs) = gram_system(&q, &basis);
let sol = solve_affine(&rows, &rhs).expect("consistent");
assert_eq!(sol.dimension(), 165);
let group = detect_polynomial_symmetry_group(&q, 3);
assert_eq!(
group.len(),
16,
"expected the order-16 group: swap x,y composed with independent sign flips \
of x, y, z (every exponent in q is even)"
);
let base_rat = unpack(n, &sol.particular);
let dirs_rat: Vec<Vec<Vec<Rational>>> =
sol.nullspace.iter().map(|d| unpack(n, d)).collect();
let actions: Vec<Vec<(usize, i8)>> = group
.iter()
.map(|(perm, signs)| signed_permutation_action(&basis, perm, signs))
.collect();
let sym_base = symmetrize_matrix(&base_rat, &actions);
let sym_dirs_packed: Vec<Vec<Rational>> = dirs_rat
.iter()
.map(|d| pack(n, &symmetrize_matrix(d, &actions)))
.collect();
let sym_dirs: Vec<Vec<Vec<Rational>>> = row_reduce_basis(sym_dirs_packed)
.iter()
.map(|v| unpack(n, v))
.collect();
assert_eq!(sym_dirs.len(), 26);
let mut quad = RatPoly::zero(3);
for i in 0..n {
for j in 0..n {
let e = add_exp(&basis[i], &basis[j]);
quad = quad.add(&RatPoly::monomial(3, e, sym_base[i][j].clone()));
}
}
assert_eq!(quad, q);
let v_ones: Vec<Rational> = vec![Rational::from(1); n];
let (restricted_base, restricted_dirs) =
restrict_by_near_null_vectors(&sym_base, &sym_dirs, &[v_ones])
.expect("z(1,1,1)=0 must be consistent with the symmetric family");
assert_eq!(restricted_dirs.len(), 16);
let mut quad2 = RatPoly::zero(3);
for i in 0..n {
for j in 0..n {
let e = add_exp(&basis[i], &basis[j]);
quad2 = quad2.add(&RatPoly::monomial(3, e, restricted_base[i][j].clone()));
}
}
assert_eq!(quad2, q);
let v_grad_x: Vec<Rational> = basis.iter().map(|e| Rational::from(e[0])).collect();
assert!(
restrict_by_near_null_vectors(
&sym_base,
&sym_dirs,
&[vec![Rational::from(1); n], v_grad_x]
)
.is_none(),
"grad_x should NOT be a valid null direction of the true certificate"
);
}
#[test]
fn psd_search_certifies_robinsons_form_with_a_reznick_multiplier() {
let mono = |ex: Vec<u32>, c: i64| RatPoly::monomial(3, ex, Rational::from(c));
let mut r = mono(vec![6, 0, 0], 1);
r = r.add(&mono(vec![0, 6, 0], 1));
r = r.add(&mono(vec![0, 0, 6], 1));
r = r.add(&mono(vec![4, 2, 0], -1));
r = r.add(&mono(vec![2, 4, 0], -1));
r = r.add(&mono(vec![0, 4, 2], -1));
r = r.add(&mono(vec![0, 2, 4], -1));
r = r.add(&mono(vec![4, 0, 2], -1));
r = r.add(&mono(vec![2, 0, 4], -1));
r = r.add(&mono(vec![2, 2, 2], 3));
assert_eq!(r.is_homogeneous(), Some(6));
assert!(
psd_search(&r, 3).is_none(),
"Robinson's form is not itself SOS, so a direct (unmultiplied) search must refuse"
);
let sigma = RatPoly::sum_of_squares(3);
let q = r.mul(&sigma);
assert_eq!(q.is_homogeneous(), Some(8));
let sos = psd_search(&q, 4)
.expect("(x^2+y^2+z^2)*Robinson is a classical SOS example; the search should find it");
assert_eq!(sos.to_poly(3), q);
}
}
#[cfg(test)]
mod diag {
use super::*;
fn homogeneous_motzkin_times_sos() -> (RatPoly, Vec<Exponents>) {
let mut m = RatPoly::monomial(3, vec![4, 2, 0], Rational::from(1));
m = m.add(&RatPoly::monomial(3, vec![2, 4, 0], Rational::from(1)));
m = m.add(&RatPoly::monomial(3, vec![2, 2, 2], Rational::from(-3)));
m = m.add(&RatPoly::monomial(3, vec![0, 0, 6], Rational::from(1)));
let sigma = RatPoly::sum_of_squares(3);
let q = m.mul(&sigma);
let basis: Vec<Exponents> = monomial_basis(3, 4)
.into_iter()
.filter(|e| e.iter().sum::<u32>() == 4)
.collect();
(q, basis)
}
#[test]
fn diag_step1_step2_trajectory_and_family_sanity() {
let (q, basis) = homogeneous_motzkin_times_sos();
let n = basis.len();
let (rows, rhs) = gram_system(&q, &basis);
eprintln!(
"DIAG n={n} rows={} cols={}",
rows.len(),
rows.first().map(|r| r.len()).unwrap_or(0)
);
let sol = solve_affine(&rows, &rhs).expect("consistent");
eprintln!("DIAG nullspace_dim={}", sol.dimension());
let dim = sol.dimension();
let mut t_check = vec![Rational::from(0); dim];
for (i, slot) in t_check.iter_mut().enumerate() {
*slot = Rational::from((3 * (i as i64 + 1) - 7, 2 * (i as i64 + 1) + 3));
}
let packed = sol.at(&t_check);
let q_check = unpack(n, &packed);
let mut quad = RatPoly::zero(3);
for i in 0..n {
for j in 0..n {
let e = add_exp(&basis[i], &basis[j]);
quad = quad.add(&RatPoly::monomial(3, e, q_check[i][j].clone()));
}
}
assert_eq!(
quad, q,
"DIAG family sanity check FAILED: z^T Q(t) z != q at an arbitrary rational t"
);
eprintln!("DIAG family sanity check: OK (z^T Q(t) z == q at a random rational t)");
let base: Vec<Vec<f64>> = unpack(n, &sol.particular)
.iter()
.map(|row| row.iter().map(rat_to_f64).collect())
.collect();
let dirs: Vec<Vec<Vec<f64>>> = sol
.nullspace
.iter()
.map(|dir| {
unpack(n, dir)
.iter()
.map(|row| row.iter().map(rat_to_f64).collect())
.collect()
})
.collect();
eprintln!(
"DIAG base min_eig (t=0, before any projection) = {}",
min_eigenvalue(
&unpack(n, &sol.particular)
.iter()
.map(|row| row.iter().map(rat_to_f64).collect())
.collect::<Vec<Vec<f64>>>()
)
);
let (ortho_dirs, _r) = orthonormalize(&dirs);
let family = Family::new(base, ortho_dirs);
let mut t = vec![0.0f64; dirs.len()];
for &floor in FLOOR_SCHEDULE {
if let Some(next) = family.search_from(t.clone(), floor, 150) {
t = next;
}
let eig = min_eigenvalue(&family.at(&t));
eprintln!("DIAG floor={floor:>10}: min_eig={eig}");
}
}
#[test]
fn diag_step3_planted_singular_example() {
let basis: Vec<Exponents> = monomial_basis(3, 4)
.into_iter()
.filter(|e| e.iter().sum::<u32>() == 4)
.collect();
let n = basis.len();
eprintln!("DIAG planted: n={n}");
let mut rng = SplitMix64::new(0xBEEF_CAFE);
let vecs: Vec<Vec<i64>> = (0..3)
.map(|_| (0..n).map(|_| (rng.next_u64() % 7) as i64 - 3).collect())
.collect();
let mut q0 = vec![vec![Rational::from(0); n]; n];
for v in &vecs {
for i in 0..n {
for j in 0..n {
q0[i][j] += Rational::from(v[i] * v[j]);
}
}
}
assert!(
psd_decompose(&q0).is_some(),
"planted Q0 should be PSD by construction"
);
let mut target = RatPoly::zero(3);
for i in 0..n {
for j in 0..n {
let e = add_exp(&basis[i], &basis[j]);
target = target.add(&RatPoly::monomial(3, e, q0[i][j].clone()));
}
}
assert_eq!(target.is_homogeneous(), Some(8));
let (rows, rhs) = gram_system(&target, &basis);
let sol = solve_affine(&rows, &rhs).expect("consistent");
eprintln!("DIAG planted nullspace_dim={}", sol.dimension());
let sos = psd_search(&target, 4);
eprintln!(
"DIAG planted psd_search found certificate: {}",
sos.is_some()
);
let sos = sos.expect(
"the search mechanism must find a planted singular-PSD example of the same \
nullspace dimension as Motzkin — if this starts failing, the regression is in \
the search/family machinery itself, not in any one hard instance",
);
assert_eq!(
sos.to_poly(3),
target,
"DIAG planted certificate failed exact re-expansion"
);
}
}
#[cfg(test)]
mod pack_len_tests {
use super::pack_len;
#[test]
fn pack_len_is_exact_up_to_2_32() {
let top = (1_usize << 32) - 1;
for n in (0..1 << 12).chain(top - (1 << 12)..=top) {
let l = pack_len(n);
assert_eq!(l as u128 * 2, n as u128 * (n as u128 + 1), "n = {n}");
if n >= 1 {
assert_eq!(l, pack_len(n - 1) + n, "n = {n}");
}
}
}
}