Repository navigation
ConvexOptimization: lower quadratic atoms through the rotated SOC - #1368
Merged
ChrisRackauckas merged 6 commits intoSep 23, 2026
Merged
ChrisRackauckas merged 6 commits into
ChrisRackauckas merged 6 commits into
Conversation
abs2(w), w^2, sum(abs2.(w)) / sum(w .^ 2) / sum(abs2, w), quad_form(u, P) and u' * P * u are certified-convex atoms that were not in LOWERABLE_ATOMS, so least squares and QPs could not be solved. They now lower through MOI.RotatedSecondOrderCone: ||w||^2 <= tau is (tau, 1/2, w). u' * P * u / quad_form(u, P) requires a constant numeric P: sym(P) = L'L is factored once at build time via the eigendecomposition (tolerating singular and asymmetric P, since the form only sees the symmetric part), lowering to ||L u||^2 <= tau. Non-PSD and parameter-dependent P are rejected. The residual objective needed one new pass: c' * u traces to a scalar `*` term over array arguments that Symbolics.scalarize cannot merge into a scalar sum, so leftover array `*` products are evaluated on their scalarized arguments after atom substitution. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Devin <noreply@cognition.ai> Agent-Harness: Devin CLI 3000.11.1 Agent-Model: swe-2-high Agent-Session: local session, transcript at /home/crackauc/sandbox/agent-jobs/Optimization.jl/jobs/1365-devin/log.txt on amdci2.julia.csail.mit.edu
Least squares through every supported spelling (sum(abs2.(w)), sum(w .^ 2), sum(abs2, w), scalar abs2(w) / w^2) is checked against the direct A \ b oracle; equality-constrained least squares and the two quadratic-form spellings (u' * P * u, SymbolicAnalysis.quad_form) are checked against hand-computed KKT systems, all at 1e-6 under tightened Clarabel tolerances (flat objectives otherwise leave ~1e-4 in u). Rejection tests pin the sound cases: -abs2 under MinSense, abs2 under MaxSense, non-affine atom arguments, nested norm(w)^2, indefinite and parameter-dependent P, and cubic powers. A reinit! test moves the least-squares offset through p and matches cold solve(remake(...)) at every theta, and sol.dual is asserted to contain only user constraints. Two pre-existing "non-affine" rejection fixtures used u[1]^2 and p[1]^2, which are now correctly lowerable quadratic atoms; they are updated to the still-non-affine cubic forms. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Devin <noreply@cognition.ai> Agent-Harness: Devin CLI 3000.11.1 Agent-Model: swe-2-high Agent-Session: local session, transcript at /home/crackauc/sandbox/agent-jobs/Optimization.jl/jobs/1365-devin/log.txt on amdci2.julia.csail.mit.edu
- Lift `p[i]^2` / `abs2(p[i])` with no optimization variable to implicit parameter columns evaluated at each theta, instead of forming epigraph atoms that trip the sign guard on `-p[i]^2`. - Match the `v' * v` self-product (including the flattened `adjoint(A*u) * A * u` trace) as a sum of squares. - Reject `u' * P * u` when the smallest eigenvalue of `sym(P)` is below `-n * eps(Float64) * lambda_max`, and warn when a strictly negative eigenvalue inside that tolerance is clamped. - Tests: LP scalar products, self-products, parameter-only squares in objectives/constraints/atom args, asymmetric/singular/near-PSD and materially indefinite P, `reinit!` epigraph sign flip, rejection cases in a loop with message matches. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Devin <noreply@cognition.ai> Agent-Harness: Devin CLI 3000.11.1 Agent-Model: swe-2-high Agent-Session: local session, transcript at /home/crackauc/sandbox/agent-jobs/Optimization.jl/jobs/1365-devin/log.txt on amdci2.julia.csail.mit.edu
…ation Round-2 review follow-ups: - Lifted parameter-only squares are now evaluated by compiled functions (Symbolics.build_function over p) instead of substitute+fold, so non-polynomial arguments like exp(p1)^2 or abs2(sin(p1)) work instead of crashing in _tofloat. - _quad_form_parts strips numeric scalar factors folded into a product by Symbolics (*(2, adjoint(u), u) for u'*(2u), (2u)'*u, u'*u*2) and accepts (1,)-shaped terms, so u'*A'*A*u alone lowers too. A negative or p-dependent scale fails the PSD/numeric-matrix check as before. - Products that cannot be traced to a scalar objective (u'*A*B*u - u1) throw a clear "could not be traced" error naming the parenthesized spellings, instead of leaking the internal shape ArgumentError. - Near-PSD clamp logs at debug level: rank-deficient B'B inputs hit that band routinely and must not warn. - M2-style primal checks use the sqrt(dobj/λmin) bound with a tightly asserted objective; shared 1e-12 Clarabel attributes factored into ALG_TIGHTEST. Core: 308/308. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Devin <noreply@cognition.ai> Agent-Harness: Devin CLI 3000.11.1 Agent-Model: swe-2-high Agent-Session: local session, transcript at /home/crackauc/sandbox/agent-jobs/Optimization.jl/jobs/1365-devin/log.txt on amdci2.julia.csail.mit.edu
…-consistency guard - Fold numeric scalar factors stripped from v'*v / v'*P*v products into the PSD-checked matrix instead of dropping them, fixing a wrong-objective bug where (A*u)'*(2*A*u) reported the optimum of u'*(A'A)*u. Negative scales now reach the PSD check and are rejected; N-argument products strip leading/trailing numeric factors. - Add solve_checked/solve!_checked: every successful solve in core_tests.jl asserts f(sol.u, p) ≈ sol.objective (the trace-count testset keeps raw solve! since the check itself calls f). - Tests: sweep scalar factor 0.5/2/-1 on left factor, right factor, outside, and inside P; fused-matrix cross terms and negative scales are refused with clear messages. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Agent-Harness: Devin CLI 3000.11.1 Agent-Model: SWE-2 High Agent-Session: /home/crackauc/.local/share/devin/cli/summaries/history_11b25520a0184738.md
Conflicts resolved to keep both feature sets: the piecewise-linear atoms (abs, max/min, maximum/minimum, sum(abs.())) from SciML#1367 and the quadratic atoms (abs2, ^2, sum-of-squares, quad_form, u'Pu, v'v self-products) here. _mapreducer_ dispatch now tries the piecewise-linear check first and falls through to the sum-of-squares check; _atom_lowering dispatches Mapreducer terms between _reducer_lowering and the RSOC sum-of-squares path. _check_no_unsupported_reducer runs on the raw traced objective (before _scalar drops dims/init) and on constraint values, unchanged from master. - SciML#1367 solves routed through solve_checked / solve!_checked (f(u) ~ obj). - One SciML#1367 expected message updated: -abs2(u1) is a recognized atom now, so it is refused by the epigraph-sign check ("nondecreasing in it") rather than the DCP gate ("not certified convex"); still a rejection. - New testset: quadratic + piecewise-linear atoms in one objective (sum(abs2)+l1, maximum+abs2, u'Pu+abs-u1) against analytic optima. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Agent-Harness: Devin CLI 3000.11.1 Agent-Model: SWE-2 High Agent-Session: /home/crackauc/.local/share/devin/cli/summaries/history_11b25520a0184738.md
ChrisRackauckas
marked this pull request as ready for review
September 23, 2026 12:31
This was referenced Sep 26, 2026
Merged
ChrisRackauckas
referenced
this pull request
Sep 29, 2026
Agent-Harness: Devin CLI 3000.11.3 (Mac) Agent-Model: Fusion (claude-opus-5-5 medium + swe-2 medium) Agent-Session: local transcript ~/.local/share/devin/cli/transcripts/helpful-exception.json Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ConvexMOIcould not solve least squares or QPs: SymbolicAnalysis certifies the quadratic atoms convex, but they were missing fromLOWERABLE_ATOMS, so no epigraph was ever formed (andu'Pusurfaced asUnknownCurvature,sum(abs2.(w))as an internalinvalid indexerror). This PR addsabs2(w),w^2,sum(abs2.(w))/sum(w .^ 2)/sum(abs2, w),SymbolicAnalysis.quad_form(u, P),u' * P * u, and self-productsv' * v(u'*u,(A*u)'*(A*u),(A*u-b)'*(A*u-b)) as lowerable atoms throughMOI.RotatedSecondOrderCone(‖w‖² <= τis(τ, 1/2, w);u'Puis factored assym(P) = LᵀLonce at build time). Unsound cases are rejected with clear errors: non-PSD or parameter-dependentP, non-affine atom arguments,-abs2underMinSense, nestednorm(w)^2, and non-square powers.The quadratic-form matcher (
_quad_form_parts) also handles how Symbolics flattens real input: a numeric scalar folded anywhere in the product (u'*(2u),(2u)'*u,u'*u*2,(A*u)'*(2*A*u),u'*A'*A*ustandalone) is stripped and folded intoP— soP = k·Ifor self-products andP = k·∏Mᵢfor quad forms, which routes both magnitude and sign through the PSD check (negative scale → rejected as non-PSD). Ap-dependent scale correctly fails the numeric check. Products that cannot be traced to a scalar objective at all (u'*A*B*u - u1) are caught and re-thrown as a "could not be traced" error naming the supported spellings instead of leaking the internal shapeArgumentError.A residual-objective pass (
_expand_scalar_products) evaluates leftover scalar*terms over array arguments, so pure LPs such asc'*u,c'*M*u,(M*u)'*c,dot(c,u),sum(c .* u), andc'*(u .- 1)also solve (they previously failed withUnknownCurvature).Parameter-only squares (
p[1]^2,abs2(p[1])with no optimization variable in the argument) are not epigraph atoms: each distinct one is lifted to an implicit parameter column evaluated at everyθby a compiled function (Symbolics.build_functionoverp), so non-polynomial arguments (exp(p1)^2,abs2(sin(p1)),(p1/p2)^2) work too.u[1] + p[1]^2solves,u[1]^2 - p[1]^2gets the right constant with the right sign (rather than a misleading epigraph-sign error), they work inside atom arguments and constraints, andreinit!recomputes them.sym(P)is accepted only when its smallest eigenvalue is above-n*eps(Float64)*λmax; a strictly negative eigenvalue inside that band is clamped with a@debuglog (rank-deficientBᵀBinputs hit the band routinely, so it must not warn), and anything more negative (e.g.[1e6 0; 0 -1e-6]) is rejected as non-convex.Wrong-answer bug found in review, fixed
Round-3 review caught
(A*u)' * (2*A*u) - c'*ureporting-2.1667— the optimum ofu'Mu - c'u, i.e. the factor 2 was dropped. Root cause: in_quad_form_parts, the self-product branch (restmultiplies back tov) returnedmid = nothing, discarding the scalarscalethat_strip_scalar_mulhad extracted, while thev'*P*vbranch folded it in. Fix: the self-product branch returnsmid = [scale](unlessscale == 1), soP = scale·Iand the PSD check sees the sign. N-argument products (adjoint(2*A*u)contains*(2, A, u)after Symbolics refactors2Aback out) are now stripped too. Every successful solve incore_tests.jlnow goes throughsolve_checked/solve!_checked, which assertsf(sol.u, p) ≈ sol.objective— the guard that would have caught this (~120 assertions added).Merge with #1367 (piecewise-linear atoms)
This branch merges
origin/masterataaf82fe33(ConvexOptimization v0.1.2, #1367 merged) — a merge commit, not a rebase, so no force-push. Both families coexist:LOWERABLE_ATOMSlistsabs/max/minalongsideabs2/quad_form;Mapreducerterms dispatch to the piecewise-linear check first, then the sum-of-squares check;_atom_loweringsends sum-of-squares reducers to RSOC and the rest to_reducer_lowering;_check_no_unsupported_reducerruns on the raw traced objective (before_scalarcan dropdims/init). All #1367 solves route through thef(sol.u,p) ≈ sol.objectiveguard. One #1367 expected message changed:-abs2(u1)is now a recognized atom, so it is refused by the epigraph-sign check ("nondecreasing in it for MinSense") instead of the DCP gate ("not certified convex") — still a rejection, just from a later stage with a more specific reason. Mixed-family coverage added:sum(abs2.(u-b)) + sum(abs.(u))→ 1.75,maximum(u) + abs2(u1-1)→ 0.75,u'P2u + |u1-0.5| - u1→ -0.125, all matching analytic optima.Verification
Run in
lib/ConvexOptimization/withJULIA_PKG_SERVER="", Julia 1.12.4:→
RUNIC_CHECK_EXIT=0.typos src test→TYPOS_EXIT=0.Before/after on the same problems (parent commit
90a74c375):Both now solve to the analytic optimum within 1e-6.
Scalar-factor sweep (all observed this round)
A = [1 2; 0 1; 1 1],c = [1,-2],M = A'A,P2 = [2 1; 1 2], box[-10,10]².f(u)is the user objective evaluated at the returned point; "consistent" meansf(u) == sol.objective.Atom-level scales (all consistent):
2*sum(abs2.(u)) - c'u→ -0.625,0.5*sum(abs2.(u))→ -2.5,2*sum(abs2,u)→ -0.625,2*quad_form(u,P2)→ -0.5833,quad_form(u,2P2)→ -0.5833,2*(u1^2+u2^2)→ -0.625,sum(abs2.(u))*2→ -0.625,sum(abs2.(u))/2→ -2.5,(u'u)*0.5→ -2.5,(u-b)'(u-b) - c'u→ -1.25,2*((u-b)'(u-b)) - c'u→ -0.625,-2*sum(abs2.(u))→ rejected. Reviewer reproducer(A*u)'*(2*A*u) - c'*u→ -1.0833 = -c'(M\c)/(4·2) ✓ (was -2.1667 before the fix).(2*A*u)'*(A*u)→ -1.0833,u'*(3u) - c'u→ -0.4167 = -‖c‖²/12,2*((A*u)'*(A*u))→ -1.0833.(A*u)'*((2A)*u)(scalar pre-fused into a numeric matrix, av'*M*wcross term) is refused withUnknownCurvature— sound.New coverage in
test/core_tests.jl: LP scalar products (-3, -11, -11, -2 on the unit box),v'*vself-products vsA\b/ analytic optima, parameter-only squares in objectives/constraints/atom args and acrossreinit!including non-polynomial arguments (exp(p1)^2,abs2(sin(p1))), asymmetricP([2 1; -1 2]uses onlysym(P) = 2I), singular-PSDP, near-PSD eigenvalue clamped with a@debuglog (@test_logsatDebuglevel, deterministic diagonalP), materially indefinite[1e6 0; 0 -1e-6]rejected, scalar-scaled and multi-matrix products (u'*(2u),(2u)'*P*u,u'*A'*A*u, indefiniteu'*I*Bi*u,k∈{0.5,2}on both factors and outside,kinsideP), trace-shape error messages,reinit!flipping the sign ofponabs2(u1)*p1rejected while the cache still solves atp = 3, the rejection suite collapsed to a(message, f, u0, p, kwargs)loop with message matches, and a file-widef(sol.u,p) ≈ sol.objectiveconsistency guard. Flat-optimum primal checks assert the objective tightly and boundubysqrt(dobj/λmin)instead of a magic tolerance.Cross-check against Convex.jl 0.16.7 + Clarabel 0.11.1 in a scratch environment (same solver, tight tolerances):
The constrained-QP gap is Convex.jl's own solver error: against the independent KKT oracle, ConvexMOI is off by 3.1e-7 while Convex.jl is off by 2.6e-6.
Not verified
Coretest group was run; the docs build and other groups were not exercised (this change adds no docs pages and no new dependencies).A \ band hand-computed KKT systems instead.u'*A*B*uinside a larger expression (e.g.- u[1]) still cannot be traced: Symbolics builds a(1,)-shaped term that cannot be added to scalar terms, inside the user'sf. It now throws a clear "could not be traced" error namingu' * (A * B) * uand(A*u)'*(B*u)as supported spellings; standalone(1,)-shaped products do lower.(v)'*(w)withv ≠ w(e.g.(A*u)'*(B*u)withsym(A'B)PSD) is not decomposed into a quadratic form and is rejected as uncertified — a sound refusal, not a wrong answer. A scalar pre-fused into a matrix ((A*u)'*((2A)*u)) refuses the same way.Reviewer pushback
λmin >= -n*eps(Float64)*λmaxonsym(P)— check that is the right boundary; anything below is rejected, anything negative inside is clamped with a@debuglog.v' * vmatching compares the factors afteradjoint(v)tovstructurally (isequalon materialized arrays); check for spellings that miss this and silently fall back to the generic rejection.Symbolics.value(...) isa Number; ap-dependent factor correctly fails that check, but probe whether any other non-numeric symbolic slips through.MaxSenseand for atoms scaled bypthat change sign betweenreinit!calls.Closes #1365.
Please ignore this draft until reviewed by @ChrisRackauckas.
🤖 Generated with Devin — Devin CLI 3000.11.1, model swe-2-high; local session, transcript at /home/crackauc/.local/share/devin/cli/summaries/history_11b25520a0184738.md