Show HN: Sostactic – 使用求和平方在Lean中解决多项式不等式
Show HN: Sostactic – polynomial inequalities using sums-of-squares in Lean

原始链接: https://github.com/mmaaz-git/sostactic

## Sostactic:在Lean 4中证明多项式不等式 Sostactic是一系列Lean 4战术,利用Python后端通过求和次方(SOS)分解来证明多项式不等式,其能力超越了现有的Lean战术,如`nlinarith`和`positivity`。 **主要特性:** * **证明:** 多项式的全局非负性,在半代数集上的非负性(由多项式不等式定义),以及半代数集的空集性。 * **战术:** `sos_decomp`(用于全局非负性),`putinar_decomp`/`schmudgen_decomp`(用于约束问题),`putinar_empty`/`schmudgen_empty`(用于证明集合为空)。 * **后端:** 利用`cvxpy`(一个凸优化求解器)通过Python查找SOS证书,然后在Lean中验证它们。 * **灵活性:** 可以作为Python API、CLI,或直接在Lean中使用。支持预生成证书以提高性能。 **用法:** 像`sos_decomp`和`putinar_decomp`这样的战术会自动尝试寻找证明。可以调整参数,如`order`(松弛阶数)和`degree`(分母阶数上限)。如果证明失败,系统会提供诊断和建议(例如,增加阶数,使用分母)来指导用户。 **安装:** 需要Lean 4,Python 3.10+,以及通过`lake`和`pip`安装的依赖项。 更多细节和示例可在[README](链接到README)中找到。

对不起。
相关文章

原文

This is a collection of Lean4 tactics for proving polynomial inequalities via sum-of-squares (SOS) decompositions, powered by a Python backend. You can use it via Python or Lean.

These tactics are significantly more powerful than nlinarith and positivity -- i.e., they can prove inequalities they cannot. In theory, they can be used to prove any of the following types of statements

  • prove that a polynomial is nonnegative globally
  • prove that a polynomial is nonnegative over a semialgebraic set (i.e., defined by a set of polynomial inequalities)
  • prove that a semialgebraic set is empty, i.e., that a system of polynomial inequalities is infeasible

To see more about how it works, read the blog post. Full documentation is provided towards the end of this README; feel free to point your coding agent at it.

import Sostactic

-- AM-GM inequality
example (x y : ℝ) : 2 * x * y ≤ x^2 + y^2 := by
  sos_decomp

-- Motzkin polynomial: ratio of SOS
example (x y z : ℝ) :
    0 ≤ x^4 * y^2 + x^2 * y^4 + z^6 - 3 * x^2 * y^2 * z^2 := by
  sos_decomp (degree := 2)

-- 4x³ - 3x + 1 ≥ 0 on [0, 1] (touches 0 at x = 1/2)
-- linarith, positivity, and nlinarith all fail
example (x : ℝ) (h1 : 0 ≤ x) (h2 : 01 - x) :
    04 * x^3 - 3 * x + 1 := by
  putinar_decomp (order := 2)

-- two disjoint disks centered at (0,0) and (3,3)
-- linarith and nlinarith fail
example : ¬∃ x y : ℝ, 01 - x^2 - y^201 - (x - 3)^2 - (y - 3)^2 := by
  rintro ⟨x, y, h1, h2⟩
  schmudgen_empty (order := 1)

Look at Sostactic/Examples.lean for more interesting examples!

  • elan (Lean 4 toolchain manager)
  • Python 3.10+

If your project uses lakefile.toml:

[[require]]
name = "sostactic"
git = "https://github.com/mmaaz-git/sostactic.git"
rev = "main"

Or if your project uses lakefile.lean:

require sostactic from git
  "https://github.com/mmaaz-git/sostactic.git" @ "main"

Then fetch and build:

lake update sostactic
lake build

Note: Sostactic tracks Mathlib v4.29.0-rc8. If your project uses a different Mathlib version, you may need to align them. When creating a new project, the default Mathlib version should match.

2. Set up the Python environment

Create a virtual environment in your project root and install the dependencies:

python3 -m venv .venv
.venv/bin/python3 -m ensurepip
.venv/bin/python3 -m pip install -r .lake/packages/Sostactic/python/requirements.txt

That's it. Sostactic auto-discovers .venv/bin/python3 in your project root.

Tip: Add .venv/ to your .gitignore.

Advanced: custom Python path

If your Python lives elsewhere, set the SOSTACTIC_PYTHON environment variable:

export SOSTACTIC_PYTHON=/path/to/your/python3

This package has 3 possible ways to use it: as a Python API, as a Python CLI, and as a Lean package. The Lean tactics call the Python CLI to generate the certificate, and then the certificate is checked inside Lean. The Python program uses cvxpy, a convex optimization solver, due to the well-established correspondence between SOS polynomials and semidefinite programming (SDP). One issue is that the SDP solution is numerical: we use a rationalization and projecting step, with some additional numerical tricks, to get an exact answer, which is then checked in Lean.

A caveat is that, due to numerical issues, exactification may fail, for a variety of reasons. In the spirit of interactive theorem provers, the solver provides suggestions about how to fix the solution by passing additional arguments.

For more details on the theory and algorithms, see the blog post.

The rest of this README introduces the Python API, the Python CLI, and then the Lean tactics.

The entire Python program is contained in python/sos.py, with tests in python/test_sos.py. There are 5 main functions:

sos_decomp(p, denom_degree_bound=0, denom_template=None)

Prove that a polynomial p is globally nonnegative by finding an exact SOS certificate.

from sympy import symbols, Poly
from python.sos import sos_decomp

x, y = symbols('x y')
result = sos_decomp(Poly(x**2 + y**2, x, y))
print(result["success"])  # True
print(result["exact_sos"])  # weighted SOS terms: [(weight, poly), ...]

With denom_degree_bound=0 (default), finds a pure SOS decomposition: p = w₁s₁² + w₂s₂² + ...

With denom_degree_bound > 0, finds SOS polynomials d and n such that d·p = n, handling polynomials like Motzkin's that are nonneg but not themselves SOS:

x, y, z = symbols('x y z')
motzkin = Poly(x**4*y**2 + x**2*y**4 + z**6 - 3*x**2*y**2*z**2, x, y, z)
result = sos_decomp(motzkin, denom_degree_bound=2)
print(result["exact_num_sos"])  # numerator SOS decomposition
print(result["exact_den_sos"])  # denominator SOS decomposition

You can also pass an explicit denom_template as a sympy expression with free parameters (e.g., a*x**2 + b*y**2 + c*z**2).

On failure, result["diagnostics"] contains rank info and result["suggestion"] has a hint for what to try next.

putinar(target, constraints, order, basis_overrides=None)

Prove that target >= 0 on the semialgebraic set {x : g₁(x) >= 0, ..., gₘ(x) >= 0} using Putinar's Positivstellensatz.

from python.sos import putinar

x = symbols('x')
result = putinar(
    target=4*x**3 - 3*x + 1,
    constraints=[x, 1 - x],
    order=2
)
print(result["success"])  # True
print(result["exact_certificate_blocks"])  # certificate blocks with multipliers and SOS

The order is the global relaxation order r. Each block with multiplier h is truncated by deg(h * sigma) <= 2r, so sigma uses monomials of degree at most floor((2r - deg(h)) / 2).

schmudgen(target, constraints, order, basis_overrides=None)

Same interface as putinar, but uses Schmüdgen's Positivstellensatz, which uses products of all subsets of constraints as multipliers. More powerful than Putinar (works on any compact set), but the SDP grows exponentially in the number of constraints.

from python.sos import schmudgen

x = symbols('x')
result = schmudgen(
    target=x*(1 - x),
    constraints=[x, 1 - x],
    order=1
)

putinar_empty(constraints, order, basis_overrides=None)

Prove that {x : g₁(x) >= 0, ..., gₘ(x) >= 0} is empty. Equivalent to putinar(-1, constraints, order=order).

from python.sos import putinar_empty

x = symbols('x')
result = putinar_empty(constraints=[x, -x - 1], order=1)
print(result["success"])  # True — the set {x >= 0, x <= -1} is empty

schmudgen_empty(constraints, order, basis_overrides=None)

Same as putinar_empty, using Schmüdgen's Positivstellensatz.

All functions return a dict with at least:

  • "success": whether an exact certificate was found
  • "suggestion": on failure, a hint for what to try (e.g., increase degree, use a template)

On success, the result also contains the exact certificate data (SOS terms, Gram matrices, certificate blocks). On failure, "diagnostics" or "block_diagnostics" contain per-block exactification diagnostics useful for tuning basis_overrides. The backend also returns a heuristic basis_override_suggestion and a top-level "suggestion" string you can paste into the CLI or Lean tactic. Suggestions are derived from the failed numeric solution itself; they do not trigger additional hidden SDP solves.

The CLI provides the same 3 commands (sos_decomp, putinar, schmudgen) and outputs JSON certificates.

python python/sos.py sos_decomp --poly "x**2 + y**2" --vars "x y"

# with denominator search
python python/sos.py sos_decomp --poly "x**4*y**2 + x**2*y**4 + z**6 - 3*x**2*y**2*z**2" \
    --vars "x y z" --denom-degree-bound 2

# with explicit denominator template
python python/sos.py sos_decomp --poly "x**4*y**2 + x**2*y**4 + z**6 - 3*x**2*y**2*z**2" \
    --vars "x y z" --denom-degree-bound 2 --denom-template "a*x**2 + b*y**2 + c*z**2"

# save certificate to file
python python/sos.py sos_decomp --poly "x**2 + y**2" --vars "x y" --out cert.json
# prove 4x^3 - 3x + 1 >= 0 on [0, 1]
python python/sos.py putinar --poly "4*x**3 - 3*x + 1" --constraints "x, 1-x" \
    --vars "x" --order 2

# prove emptiness (--poly defaults to -1)
python python/sos.py putinar --constraints "x, -x-1" --vars "x" --order 1
# prove x(1-x) >= 0 on [0, 1]
python python/sos.py schmudgen --poly "x*(1-x)" --constraints "x, 1-x" \
    --vars "x" --order 1

# prove emptiness
python python/sos.py schmudgen --constraints "1-x**2-y**2, x**2+y**2-2" \
    --vars "x y" --order 1
Argument Description
--poly Target polynomial (default "-1" for putinar/schmudgen)
--constraints Comma-separated list of constraint polynomials
--vars Space-separated list of variable names
--denom-degree-bound Denominator degree bound (for sos_decomp)
--denom-template Explicit denominator template (for sos_decomp)
--order Global relaxation order for putinar/schmudgen
--basis-overrides Per-block SOS degree caps, e.g. "0:2,3:0"
--out Path to write JSON certificate

The CLI outputs a JSON certificate. For a successful sos_decomp:

{
  "success": true,
  "kind": "sos",
  "status": "optimal",
  "exact_sos": [
    {"weight": "1", "poly": {"expr": "y", "gens": ["x", "y"]}},
    {"weight": "1", "poly": {"expr": "x", "gens": ["x", "y"]}}
  ],
  "exact_identity": {"expr": "0", "gens": ["x", "y"]},
  "exact_residual": {"expr": "0", "gens": ["x", "y"]},
  ...
}

This says x² + y² = 1·y² + 1·x². For Positivstellensatz certificates, the result includes exact_certificate_blocks with multiplier/SOS pairs for each constraint block.

On failure, the output includes diagnostics and a suggestion:

{
  "success": false,
  "status": "infeasible",
  "suggestion": "not SOS — try adding a denominator with denom_degree_bound=2"
}

sos_decomp — Global nonnegativity

Proves that a polynomial f is nonnegative globally by finding an SOS decomposition, optionally with a denominator to decompose it into a ratio of SOS polynomials. By Artin's theorem, any globally nonnegative polynomial can be decomposed into a ratio of SOS.

Goals can be in any of these forms: 0 ≤ f, f ≥ 0, a ≤ b, or a ≥ b.

-- plain SOS
example (x y : ℝ) : 0 ≤ x^2 + y^2 := by sos_decomp

-- with denominator search (degree bound for the denominator)
example (x y z : ℝ) :
    0 ≤ x^4 * y^2 + x^2 * y^4 + z^6 - 3 * x^2 * y^2 * z^2 := by
  sos_decomp (degree := 2)

-- with explicit denominator template
example (x y z : ℝ) :
    0 ≤ x^4 * y^2 + x^2 * y^4 + z^6 - 3 * x^2 * y^2 * z^2 := by
  sos_decomp (degree := 2) (denom_template := "a*x^2 + b*y^2 + c*z^2")

-- also handles a ≤ b and a ≥ b goals
example (x y : ℝ) : 2 * x * y ≤ x^2 + y^2 := by sos_decomp

putinar_decomp / schmudgen_decomp — Nonnegativity on semialgebraic sets

Proves 0 ≤ f given polynomial inequality hypotheses, using Putinar's or Schmüdgen's Positivstellensatz. Hypotheses can be in any of these forms: 0 ≤ g, g ≥ 0, a ≤ b, or a ≥ b.

-- Putinar: requires Archimedean (compact) constraints
example (x : ℝ) (h1 : 0 ≤ x) (h2 : x ≤ 1) :
    04 * x^3 - 3 * x + 1 := by
  putinar_decomp (order := 2)

-- Schmüdgen: requires compact constraints, uses products of gᵢ
example (x : ℝ) (h1 : 0 ≤ x) (h2 : x ≤ 1) :
    0 ≤ x * (1 - x) := by
  schmudgen_decomp (order := 1)

putinar_empty / schmudgen_empty — Emptiness of semialgebraic sets

Proves False from contradictory polynomial inequality hypotheses, showing the feasible set is empty.

-- {x ≥ 0, x ≤ -1} is empty
example (x : ℝ) (h1 : 0 ≤ x) (h2 : x ≤ -1) : False := by
  putinar_empty (order := 1)

-- disjoint disks
example (x y : ℝ) (h1 : x^2 + y^21) (h2 : x^2 + y^22) : False := by
  schmudgen_empty (order := 1)
Parameter Description
order := n Relaxation order for putinar_decomp / schmudgen_decomp / emptiness tactics
degree := n Degree bound for sos_decomp
denom_template := "..." Explicit denominator template (for sos_decomp)
basis_overrides := "0:2,3:0" Per-block SOS degree caps used to shrink exactification search space
cert := "path.json" Load a pre-generated certificate from file

The SDP solver can be slow for large problems. You can pre-generate certificates and load them from JSON files, so the solver doesn't re-run on every file reload:

example (x y z : ℝ) :
    0 ≤ x^4 * y^2 + x^2 * y^4 + z^6 - 3 * x^2 * y^2 * z^2 := by
  sos_decomp (cert := "certs/motzkin.json")

Lean still independently verifies the certificate — the JSON file is untrusted.

If one of the sub-goals does not close automatically, the tactic leaves it as a named goal for you to close interactively. For sos_decomp and the Positivstellensatz decomp tactics, the goals are:

  • sos_identity / pstv_identity — the polynomial identity (normally closed by ring_nf)
  • sos_nonneg / pstv_nonneg — the nonnegativity claim (normally closed by positivity)

For sos_decomp with a denominator, the goals are hD_ne, hmul, hD_nonneg, and hN_nonneg.

You can close them with case:

example (x y : ℝ) : 0 ≤ x^2 + y^2 := by
  sos_decomp
  case sos_identity => ring
  case sos_nonneg => positivity

Pre-generating certificates for large problems

The SDP solver can be slow for large polynomials. You can use the Python CLI to generate a certificate once (e.g., overnight), save it to a JSON file, and then load it in Lean so the solver doesn't re-run on every file reload:

# generate the certificate
python python/sos.py sos_decomp \
    --poly "x**4*y**2 + x**2*y**4 + z**6 - 3*x**2*y**2*z**2" \
    --vars "x y z" --denom-degree-bound 2 --out certs/motzkin.json

# or for Positivstellensatz certificates
python python/sos.py putinar --poly "4*x**3 - 3*x + 1" \
    --constraints "x, 1-x" --vars "x" --order 2 --out certs/chebyshev.json

Then in Lean:

example (x y z : ℝ) :
    0 ≤ x^4 * y^2 + x^2 * y^4 + z^6 - 3 * x^2 * y^2 * z^2 := by
  sos_decomp (cert := "certs/motzkin.json")

example (x : ℝ) (h1 : 0 ≤ x) (h2 : x ≤ 1) :
    04 * x^3 - 3 * x + 1 := by
  putinar_decomp (cert := "certs/chebyshev.json")

Lean still independently verifies the certificate — the JSON file is untrusted.

The backend can fail either because the relaxation is too small (the SDP is infeasible at the chosen parameters) or because a numerical SDP solution does not exactify cleanly. In both cases, it returns diagnostics and suggestions.

Increase the relaxation order. If the solver returns infeasible, the truncation may be too small. Try increasing the order:

-- order 1 might not be enough
example ... := by putinar_decomp (order := 2)
-- try order 3
example ... := by putinar_decomp (order := 3)

Use a denominator. For sos_decomp, if the polynomial is nonneg but not SOS (like Motzkin's), you need a denominator:

-- fails without denominator
-- example ... := by sos_decomp
-- works with denominator degree 2
example ... := by sos_decomp (degree := 2)

Use basis overrides. When exactification fails after the SDP is already feasible, the solver suggests basis_overrides to shrink selected SOS blocks. Run once without them, then rerun explicitly with the suggested override:

example ... := by putinar_decomp (order := 3) (basis_overrides := "1:2,2:3")

The format is "block:degree,block:degree,..." where each pair restricts a specific block to a smaller SOS degree cap. For example, 0:2 means block 0 may only use monomials of degree at most 1.

Use a denominator template. For sos_decomp with a denominator, you can guide the solver by specifying the form of the denominator with free parameters:

example (x y z : ℝ) :
    0 ≤ x^4 * y^2 + x^2 * y^4 + z^6 - 3 * x^2 * y^2 * z^2 := by
  sos_decomp (degree := 2) (denom_template := "a*x^2 + b*y^2 + c*z^2")

Try Schmüdgen instead of Putinar. For constrained problems, Schmüdgen's theorem is more powerful (it uses products of all subsets of constraints). If Putinar fails, Schmüdgen may succeed:

-- putinar might fail here
-- example ... := by putinar_decomp (order := 2)
-- schmudgen uses product multipliers
example ... := by schmudgen_decomp (order := 2)

Pre-generate the certificate. If the solver is too slow to run interactively, generate the certificate with the Python CLI and load it in Lean (see Pre-generating certificates above).

Heartbeat and timeout issues in Lean

For large polynomials, Lean's proof reconstruction can hit the default heartbeat limit. You can increase it locally:

set_option maxHeartbeats 800000 in
example ... := by sos_decomp (degree := 2)

If one of the sub-goals does not close automatically, it will be left as a named goal (e.g., sos_identity or sos_nonneg) for you to close manually. See Interactive goals.

Lean: Mathlib (multivariate polynomials, positivity tactic)

Python: sympy, numpy, cvxpy, clarabel

MIT

联系我们 contact @ memedata.com