pub mod cert;
pub mod gram;
pub mod linalg;
pub mod lp;
pub mod psd;
pub mod ratpoly;
pub mod sdp;
pub use cert::{CertificateKind, Multiplier, PositivityCertificate, SosPoly, SosTerm};
pub use ratpoly::RatPoly;
use crate::kernel::{ExprId, ExprPool};
use lp::{Lp, LpStatus, Rel};
use ratpoly::Exponents;
use rug::Rational;
use std::fmt;
#[derive(Debug, Clone, Copy)]
pub struct SosOpts {
pub basis_degree: Option<u32>,
pub level: u32,
}
impl Default for SosOpts {
fn default() -> Self {
SosOpts {
basis_degree: None,
level: 2,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SosError {
NotPolynomial(String),
NoCertificate(String),
Negative(String),
InvalidInput(String),
VerificationFailed(String),
}
const SEARCH_TRACE_MARKER: &str = "what the search actually did:";
impl SosError {
pub fn search_trace(&self) -> Option<&str> {
match self {
SosError::NoCertificate(msg) => msg
.split_once(SEARCH_TRACE_MARKER)
.map(|(_, trace)| trace.trim_start_matches('\n')),
_ => None,
}
}
pub fn hit_a_search_ceiling(&self) -> bool {
self.search_trace()
.is_some_and(|trace| trace.contains("NOT SEARCHED"))
}
}
impl fmt::Display for SosError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SosError::NotPolynomial(s) => write!(f, "sos: not a polynomial: {s}"),
SosError::NoCertificate(s) => write!(f, "sos: no certificate found: {s}"),
SosError::Negative(s) => write!(f, "sos: the target is negative somewhere: {s}"),
SosError::InvalidInput(s) => write!(f, "sos: invalid input: {s}"),
SosError::VerificationFailed(s) => {
write!(f, "sos: certificate failed exact verification: {s}")
}
}
}
}
impl std::error::Error for SosError {}
impl crate::errors::AlkahestError for SosError {
fn code(&self) -> &'static str {
match self {
SosError::NotPolynomial(_) => "E-SOS-001",
SosError::NoCertificate(_) => "E-SOS-002",
SosError::Negative(_) => "E-SOS-003",
SosError::InvalidInput(_) => "E-SOS-004",
SosError::VerificationFailed(_) => "E-SOS-005",
}
}
fn remediation(&self) -> Option<&'static str> {
Some(match self {
SosError::NotPolynomial(_) => {
"positivity certificates are for polynomials in the listed variables; expand or \
clear denominators first, and pass every symbol that occurs as a variable"
}
SosError::NoCertificate(_) => {
"record this as unknown, not as a closed branch: read the 'what the search \
actually did' trace in the message first — a line marked NOT SEARCHED is a \
size ceiling that fired, not a search that came up empty — then raise \
basis_degree (unconstrained) or level (constrained); this is not a proof \
that no SOS decomposition exists, and still less that the inequality is \
false — alkahest.decide is the complete (and far more expensive) fallback"
}
SosError::Negative(_) => {
"the witness point in the message satisfies the constraints and makes the target \
negative; the claim is false as stated"
}
SosError::InvalidInput(_) => {
"pass at least one variable, and keep basis_degree/level within the supported range"
}
SosError::VerificationFailed(_) => {
"internal: report the target and constraints as a minimal failing example"
}
})
}
}
fn negativity_witness(
target: &RatPoly,
constraints: &[RatPoly],
nvars: usize,
) -> Option<Vec<Rational>> {
const GRID: [(i32, i32); 9] = [
(0, 1),
(1, 1),
(-1, 1),
(1, 2),
(-1, 2),
(2, 1),
(-2, 1),
(3, 1),
(-3, 1),
];
let per_var = if nvars <= 2 {
GRID.len()
} else if nvars <= 4 {
5
} else {
3
};
let total = per_var.checked_pow(nvars as u32)?;
if total > 20_000 {
return None;
}
for idx in 0..total {
let mut rest = idx;
let mut point = Vec::with_capacity(nvars);
for _ in 0..nvars {
let (num, den) = GRID[rest % per_var];
rest /= per_var;
point.push(Rational::from((num, den)));
}
if constraints.iter().any(|g| g.eval(&point) < 0) {
continue;
}
if target.eval(&point) < 0 {
return Some(point);
}
}
None
}
fn format_point(names: &[String], point: &[Rational]) -> String {
names
.iter()
.zip(point.iter())
.map(|(n, v)| format!("{n} = {v}"))
.collect::<Vec<_>>()
.join(", ")
}
fn var_names(vars: &[ExprId], pool: &ExprPool) -> Vec<String> {
vars.iter().map(|&v| pool.display(v).to_string()).collect()
}
fn finish(cert: PositivityCertificate) -> Result<PositivityCertificate, SosError> {
match cert.verify() {
Ok(()) => Ok(cert),
Err(why) => Err(SosError::VerificationFailed(why)),
}
}
pub(crate) const MAX_MULTIPLIER_POWER: u32 = 4;
const MAX_MULTIPLIER_BASIS_LEN: usize = 90;
pub(crate) fn budget_ok() -> bool {
match crate::budget::check_all() {
Ok(()) => true,
Err(trip) => {
crate::budget::record_trip(trip);
false
}
}
}
fn multiplier_search(
target: &RatPoly,
nvars: usize,
max_power: u32,
max_basis_len: usize,
basis_degree: Option<u32>,
log: &mut Vec<String>,
) -> Option<(RatPoly, SosPoly, u32)> {
for n in 1..=max_power {
if !budget_ok() {
return None;
}
let sigma = RatPoly::sum_of_squares(nvars).pow(n);
let q = target.mul(&sigma);
let qdeg = q.total_degree();
if qdeg % 2 != 0 {
continue;
}
let min_basis_deg = qdeg.div_ceil(2);
let basis_deg = basis_degree.unwrap_or(min_basis_deg).max(min_basis_deg);
let raw_len = gram::monomial_basis(nvars, basis_deg).len();
let basis_len = psd::searched_basis_len(&q, basis_deg);
if basis_len > max_basis_len {
log.push(format!(
"multiplier search: N={n} NOT SEARCHED — σ·p would need a degree-{basis_deg} \
basis of {basis_len} monomials ({raw_len} before the homogeneity and \
half-Newton reductions), over the search budget of {max_basis_len}; stopping \
here, so no multiplier power from N={n} up was searched at all"
));
break;
}
log.push(format!(
"multiplier search: trying σ = (Σxᵢ²)^{n}, searching the full PSD Gram cone for \
σ·p over the degree-{basis_deg} monomial basis ({basis_len} monomials, {raw_len} \
before reduction)"
));
if let Some(sos) = psd::psd_search_logged(&q, basis_deg, log) {
log.push(format!("multiplier search succeeded at N={n}"));
return Some((sigma, sos, basis_deg));
}
log.push(format!(
"multiplier search: N={n} produced no certificate (see the lines above for what \
ran at this power and what was skipped)"
));
}
None
}
pub fn sos_decompose(
expr: ExprId,
vars: &[ExprId],
pool: &ExprPool,
opts: &SosOpts,
) -> Result<PositivityCertificate, SosError> {
if vars.is_empty() {
return Err(SosError::InvalidInput(
"at least one variable is required".into(),
));
}
crate::budget::clear_trip();
let names = var_names(vars, pool);
let target = RatPoly::from_expr(expr, vars, pool).map_err(SosError::NotPolynomial)?;
let nvars = vars.len();
let mut log = Vec::new();
if let Some(c) = target.as_constant() {
if c < 0 {
return Err(SosError::Negative(format!(
"the target is the negative constant {c}"
)));
}
let mut sos = SosPoly::default();
sos.push(c.clone(), RatPoly::one(nvars));
log.push(format!("target is the non-negative constant {c}"));
return finish(PositivityCertificate {
vars: vars.to_vec(),
var_names: names,
target,
constraints: Vec::new(),
kind: CertificateKind::Sos,
degree: 0,
terms: vec![Multiplier {
constraints: Vec::new(),
sos,
}],
log,
});
}
let deg = target.total_degree();
if let Some(point) = negativity_witness(&target, &[], nvars) {
return Err(SosError::Negative(format!(
"p({}) = {} < 0",
format_point(&names, &point),
target.eval(&point)
)));
}
if deg % 2 == 1 {
return Err(SosError::NoCertificate(format!(
"total degree {deg} is odd, so p cannot be a sum of squares (no witness point was \
found on the sampling grid, so this is a statement about the SOS question, not a \
claim that p is negative)"
)));
}
let basis_deg = opts.basis_degree.unwrap_or(deg.div_ceil(2));
if basis_deg > 12 {
return Err(SosError::InvalidInput(
"basis_degree above 12 is refused: the monomial basis (and the exact LP over it) \
grows too fast to be useful"
.into(),
));
}
log.push(format!(
"searching the diagonally dominant cone over the degree-{basis_deg} monomial basis"
));
let Some(sos) = gram::dsos_search(&target, basis_deg) else {
log.push(
"diagonally dominant search failed; trying the full PSD Gram cone directly".to_string(),
);
if let Some(sos) = psd::psd_search_logged(&target, basis_deg, &mut log) {
log.push(
"full PSD Gram search succeeded (p is SOS but its Gram matrix is not \
diagonally dominant)"
.to_string(),
);
return finish(PositivityCertificate {
vars: vars.to_vec(),
var_names: names,
target,
constraints: Vec::new(),
kind: CertificateKind::Sos,
degree: basis_deg,
terms: vec![Multiplier {
constraints: Vec::new(),
sos,
}],
log,
});
}
if let Some((_sigma, sos, mult_basis_deg)) = multiplier_search(
&target,
nvars,
MAX_MULTIPLIER_POWER,
MAX_MULTIPLIER_BASIS_LEN,
opts.basis_degree,
&mut log,
) {
return finish(PositivityCertificate {
vars: vars.to_vec(),
var_names: names,
target,
constraints: Vec::new(),
kind: CertificateKind::Sos,
degree: mult_basis_deg,
terms: vec![Multiplier {
constraints: Vec::new(),
sos,
}],
log,
});
}
let trace = log
.iter()
.map(|line| format!("\n - {line}"))
.collect::<String>();
return Err(SosError::NoCertificate(format!(
"undecided, not a refutation — no diagonally dominant or general PSD Gram matrix \
over the degree-{basis_deg} monomial basis reproduces p, and no Reznick multiplier \
(Σxᵢ²)^N up to N={MAX_MULTIPLIER_POWER} that was actually searched made σ·p SOS \
either. None of this is a proof that p is not SOS (with or without a multiplier), \
and still less that p is not non-negative — only that no certificate of these \
shapes was found at this size. Read the trace below before treating this as \
exhaustive: a line marked NOT SEARCHED is a budget that fired, not a search that \
came up empty. Raise basis_degree, or fall back to alkahest.decide.\
\n{SEARCH_TRACE_MARKER}{trace}"
)));
};
finish(PositivityCertificate {
vars: vars.to_vec(),
var_names: names,
target,
constraints: Vec::new(),
kind: CertificateKind::Sos,
degree: basis_deg,
terms: vec![Multiplier {
constraints: Vec::new(),
sos,
}],
log,
})
}
fn constraint_products(k: usize, level: u32) -> Vec<Vec<usize>> {
let mut out = vec![Vec::new()];
let mut frontier: Vec<Vec<usize>> = vec![Vec::new()];
for _ in 0..level {
let mut next = Vec::new();
for base in &frontier {
let start = base.last().copied().unwrap_or(0);
for i in start..k {
let mut v = base.clone();
v.push(i);
next.push(v);
}
}
out.extend(next.iter().cloned());
frontier = next;
}
out
}
pub fn prove_nonneg(
expr: ExprId,
constraints: &[ExprId],
vars: &[ExprId],
pool: &ExprPool,
opts: &SosOpts,
) -> Result<PositivityCertificate, SosError> {
if constraints.is_empty() {
return sos_decompose(expr, vars, pool, opts);
}
crate::budget::clear_trip();
if vars.is_empty() {
return Err(SosError::InvalidInput(
"at least one variable is required".into(),
));
}
if opts.level == 0 || opts.level > 8 {
return Err(SosError::InvalidInput(
"level must be between 1 and 8".into(),
));
}
let names = var_names(vars, pool);
let nvars = vars.len();
let target = RatPoly::from_expr(expr, vars, pool).map_err(SosError::NotPolynomial)?;
let gs: Vec<RatPoly> = constraints
.iter()
.map(|&g| RatPoly::from_expr(g, vars, pool).map_err(SosError::NotPolynomial))
.collect::<Result<_, _>>()?;
if let Some(point) = negativity_witness(&target, &gs, nvars) {
return Err(SosError::Negative(format!(
"p({}) = {} < 0 at a point satisfying every constraint",
format_point(&names, &point),
target.eval(&point)
)));
}
let products = constraint_products(gs.len(), opts.level);
let expanded: Vec<RatPoly> = products
.iter()
.map(|idxs| {
let mut acc = RatPoly::one(nvars);
for &i in idxs {
acc = acc.mul(&gs[i]);
}
acc
})
.collect();
let mut monomials: std::collections::BTreeSet<Exponents> = Default::default();
for e in &expanded {
monomials.extend(e.terms().keys().cloned());
}
monomials.extend(target.terms().keys().cloned());
let mut prog = Lp::new(expanded.len());
for m in &monomials {
let row: Vec<Rational> = expanded.iter().map(|e| e.coeff(m)).collect();
prog.constrain(row, Rel::Eq, target.coeff(m));
}
let weights = match prog.solve() {
LpStatus::Optimal(w) => w,
_ => {
return Err(SosError::NoCertificate(format!(
"undecided, not a refutation — no non-negative combination of constraint \
products up to level {} reproduces the target. Raise level, or the claim may \
need a Putinar-style certificate with SOS (not merely non-negative constant) \
multipliers. Record this as unknown, not as a closed branch",
opts.level
)));
}
};
let mut terms = Vec::new();
for (idxs, w) in products.iter().zip(weights.iter()) {
if *w == 0 {
continue;
}
let mut sos = SosPoly::default();
sos.push(w.clone(), RatPoly::one(nvars));
terms.push(Multiplier {
constraints: idxs.clone(),
sos,
});
}
finish(PositivityCertificate {
vars: vars.to_vec(),
var_names: names,
target,
constraints: gs,
kind: CertificateKind::Handelman,
degree: opts.level,
terms,
log: vec![format!(
"Handelman search over {} constraint products up to level {}",
products.len(),
opts.level
)],
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::errors::AlkahestError;
use crate::kernel::Domain;
fn setup() -> (ExprPool, ExprId, ExprId) {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let y = pool.symbol("y", Domain::Real);
(pool, x, y)
}
fn motzkin(pool: &ExprPool, x: ExprId, y: ExprId, z: ExprId) -> ExprId {
let sq = |e: ExprId| pool.mul(vec![e, e]);
pool.add(vec![
pool.mul(vec![sq(sq(x)), sq(y)]),
pool.mul(vec![sq(x), sq(sq(y))]),
pool.mul(vec![pool.integer(-3_i32), sq(x), sq(y), sq(z)]),
sq(sq(z)),
])
}
#[test]
fn sos_decompose_honours_a_wall_budget() {
let (pool, x, y) = setup();
let z = pool.symbol("z", Domain::Real);
let p = motzkin(&pool, x, y, z);
let _guard = crate::budget::enter(
crate::budget::Budget::new().with_wall(std::time::Duration::from_millis(200)),
);
let start = std::time::Instant::now();
let err = sos_decompose(p, &[x, y, z], &pool, &SosOpts::default())
.expect_err("no SOS certificate exists for the Motzkin form");
assert!(start.elapsed().as_secs() < 60, "budget was not consulted");
assert!(matches!(err, SosError::NoCertificate(_)), "{err:?}");
let trip = crate::budget::take_trip().expect("the budget trip must be recorded");
assert_eq!(trip.code(), "E-BUDGET-001");
}
#[test]
fn perfect_square_is_certified() {
let (pool, x, y) = setup();
let p = pool.add(vec![
pool.mul(vec![x, x]),
pool.mul(vec![pool.integer(-2_i32), x, y]),
pool.mul(vec![y, y]),
]);
let cert = sos_decompose(p, &[x, y], &pool, &SosOpts::default()).expect("certificate");
assert_eq!(cert.kind, CertificateKind::Sos);
cert.verify().expect("re-expands exactly");
assert!(cert.num_squares() >= 1);
}
#[test]
fn sum_of_even_powers_is_certified() {
let (pool, x, y) = setup();
let p = pool.add(vec![
pool.mul(vec![x, x, x, x]),
pool.mul(vec![y, y, y, y]),
pool.integer(1_i32),
]);
let cert = sos_decompose(p, &[x, y], &pool, &SosOpts::default()).expect("certificate");
cert.verify().expect("re-expands exactly");
}
#[test]
fn negative_polynomial_returns_a_witness_not_a_refusal() {
let (pool, x, y) = setup();
let p = pool.add(vec![pool.mul(vec![x, x]), pool.integer(-1_i32)]);
let err = sos_decompose(p, &[x, y], &pool, &SosOpts::default()).expect_err("negative");
assert!(matches!(err, SosError::Negative(_)));
assert_eq!(err.code(), "E-SOS-003");
}
#[test]
fn motzkin_certifies_via_a_reznick_multiplier() {
let (pool, x, y) = setup();
let p = pool.add(vec![
pool.mul(vec![x, x, x, x, y, y]),
pool.mul(vec![x, x, y, y, y, y]),
pool.mul(vec![pool.integer(-3_i32), x, x, y, y]),
pool.integer(1_i32),
]);
let cert =
sos_decompose(p, &[x, y], &pool, &SosOpts::default()).expect("Motzkin now certifies");
assert_eq!(cert.kind, CertificateKind::Sos);
let sigma = cert
.multiplier()
.expect("Motzkin is not itself SOS, so this must be a multiplier certificate");
assert_eq!(sigma.mul(&cert.target), cert.expand());
cert.verify().expect("re-verifies exactly end to end");
let lean = cert
.to_lean()
.expect("multiplier certificates emit Lean too");
assert!(!lean.contains("sorry"));
assert!(!lean.contains("admit"));
assert!(lean.contains("alkahest_multiplier_factor"));
assert!(lean.contains("alkahest_rhs_nonneg"));
assert!(lean.contains("sq_pos_of_ne_zero"));
assert!(lean.contains("ring"));
}
#[test]
fn multiplier_search_reports_undecided_not_not_sos_when_out_of_budget() {
let (pool, x, y) = setup();
let p = pool.add(vec![
pool.mul(vec![x, x, x, x, y, y]),
pool.mul(vec![x, x, y, y, y, y]),
pool.mul(vec![pool.integer(-3_i32), x, x, y, y]),
pool.integer(1_i32),
]);
let target = RatPoly::from_expr(p, &[x, y], &pool).unwrap();
let mut log = Vec::new();
let out = multiplier_search(&target, 2, 0, 90, None, &mut log);
assert!(
out.is_none(),
"a zero-power budget must not manufacture a certificate"
);
assert!(
negativity_witness(&target, &[], 2).is_none(),
"Motzkin is non-negative, so a witness must not exist"
);
}
fn c7_copositivity_form() -> RatPoly {
let n = 7usize;
let adj = |i: usize, j: usize| -> i64 {
let d = (i + n - j) % n;
i64::from(d == 1 || d == n - 1)
};
let mut p = RatPoly::zero(n);
for i in 0..n {
for j in 0..n {
let m = 3 * (i64::from(i == j) + adj(i, j)) - 1;
if m == 0 {
continue;
}
let mut e = vec![0u32; n];
e[i] += 2;
e[j] += 2;
p = p.add(&RatPoly::monomial(n, e, Rational::from(m)));
}
}
p
}
#[test]
fn the_multiplier_budget_uses_the_basis_actually_searched() {
let p = c7_copositivity_form();
let q = p.mul(&RatPoly::sum_of_squares(7));
assert_eq!(gram::monomial_basis(7, 3).len(), 120);
assert!(gram::monomial_basis(7, 3).len() > MAX_MULTIPLIER_BASIS_LEN);
assert_eq!(psd::searched_basis_len(&q, 3), 84);
assert!(psd::searched_basis_len(&q, 3) <= MAX_MULTIPLIER_BASIS_LEN);
let mut log = Vec::new();
let out = multiplier_search(&p, 7, 1, MAX_MULTIPLIER_BASIS_LEN, None, &mut log);
assert!(out.is_none(), "C₇'s N=1 certificate is still out of reach");
let tried = log
.iter()
.find(|l| l.contains("trying σ"))
.unwrap_or_else(|| panic!("N=1 must no longer be rejected on basis size: {log:?}"));
assert!(
tried.contains("84 monomials") && tried.contains("120 before reduction"),
"the log must report the searched size, with the raw one for context: {tried}"
);
}
#[test]
fn a_refusal_reports_which_multiplier_powers_were_actually_searched() {
let (pool, _x, _y) = setup();
let vars: Vec<ExprId> = (0..5)
.map(|i| pool.symbol(format!("h{i}"), Domain::Real))
.collect();
let h = [
[1, -1, 1, 1, -1],
[-1, 1, -1, 1, 1],
[1, -1, 1, -1, 1],
[1, 1, -1, 1, -1],
[-1, 1, 1, -1, 1],
];
let mut terms = Vec::new();
for i in 0..5 {
for j in 0..5 {
terms.push(pool.mul(vec![
pool.integer(h[i][j]),
vars[i],
vars[i],
vars[j],
vars[j],
]));
}
}
let p = pool.add(terms);
let err = sos_decompose(p, &vars, &pool, &SosOpts::default()).expect_err("still refused");
assert_eq!(err.code(), "E-SOS-002");
let msg = err.to_string();
assert!(
msg.contains("what the search actually did:"),
"the refusal must carry a trace of what ran: {msg}"
);
assert!(
msg.contains("NOT SEARCHED"),
"a budget that fired must be reported as such, not folded into the same \
undifferentiated refusal an exhausted search produces: {msg}"
);
assert!(
msg.contains("that was actually searched"),
"the message must not imply multiplier powers were tried when they were not: {msg}"
);
let trace = err.search_trace().expect("NoCertificate carries a trace");
assert!(trace.contains("NOT SEARCHED"));
assert!(
err.hit_a_search_ceiling(),
"this refusal is weaker than an exhausted search and must say so"
);
let neg = SosError::Negative("witness".into());
assert!(neg.search_trace().is_none());
assert!(!neg.hit_a_search_ceiling());
}
#[test]
fn basis_degree_is_not_ignored_on_the_multiplier_path() {
let (pool, x, y) = setup();
let p = pool.add(vec![
pool.mul(vec![x, x, x, x, y, y]),
pool.mul(vec![x, x, y, y, y, y]),
pool.mul(vec![pool.integer(-3_i32), x, x, y, y]),
pool.integer(1_i32),
]);
let target = RatPoly::from_expr(p, &[x, y], &pool).unwrap();
let mut default_log = Vec::new();
multiplier_search(
&target,
2,
1,
MAX_MULTIPLIER_BASIS_LEN,
None,
&mut default_log,
);
let mut raised_log = Vec::new();
multiplier_search(
&target,
2,
1,
MAX_MULTIPLIER_BASIS_LEN,
Some(5),
&mut raised_log,
);
let degree_of = |log: &[String]| -> String {
log.iter()
.find(|l| l.contains("trying σ"))
.expect("N=1 is searched either way")
.split("degree-")
.nth(1)
.and_then(|t| t.split(' ').next())
.expect("the log names the basis degree")
.to_string()
};
assert_eq!(degree_of(&default_log), "4");
assert_eq!(
degree_of(&raised_log),
"5",
"basis_degree must reach the multiplier path, or E-SOS-002 must stop recommending it"
);
}
#[test]
fn non_polynomial_is_refused() {
let (pool, x, y) = setup();
let p = pool.func("sin", vec![x]);
let err = sos_decompose(p, &[x, y], &pool, &SosOpts::default()).expect_err("not a poly");
assert_eq!(err.code(), "E-SOS-001");
}
#[test]
fn handelman_certifies_on_a_box() {
let (pool, x, _y) = setup();
let g1 = x;
let g2 = pool.add(vec![
pool.integer(1_i32),
pool.mul(vec![pool.integer(-1_i32), x]),
]);
let p = pool.add(vec![x, pool.mul(vec![pool.integer(-1_i32), x, x])]);
let cert =
prove_nonneg(p, &[g1, g2], &[x], &pool, &SosOpts::default()).expect("certificate");
assert_eq!(cert.kind, CertificateKind::Handelman);
cert.verify().expect("re-expands exactly");
}
#[test]
fn handelman_finds_the_witness_when_the_claim_is_false() {
let (pool, x, _y) = setup();
let g1 = x;
let p = pool.add(vec![x, pool.rational(-1_i32, 2_i32)]);
let err = prove_nonneg(p, &[g1], &[x], &pool, &SosOpts::default()).expect_err("negative");
assert!(matches!(err, SosError::Negative(_)));
}
}