From 6bfdd0350e75515dffd584ffab6bcf7a600004f5 Mon Sep 17 00:00:00 2001 From: Tamas Hakkel Date: Tue, 8 Sep 2026 11:06:14 +0200 Subject: [PATCH 1/3] Move OSQP to a weak dependency (package extension) Loading OSQP and its dependencies accounted for ~2s of ProximalOperators' load time, yet OSQP is only used by IndPolyhedralOSQP (the default backend of IndPolyhedral). This implements "Option 2" from #163: OSQP becomes a weak dependency and the solver-backed implementation lives in the new extension ProximalOperatorsOSQPExt. - Project.toml: OSQP moved from [deps] to [weakdeps]; register ProximalOperatorsOSQPExt = "OSQP". - src/functions/indPolyhedralOSQP.jl: keeps the IndPolyhedralOSQP type (now parametric on the model type), the function-value and prox_naive methods, and a stub constructor that raises an informative error when OSQP is not loaded. An explicit inner constructor suppresses the auto-generated 4-arg outer constructor that would otherwise shadow the extension's (l, A, xmin, xmax) method. - ext/ProximalOperatorsOSQPExt.jl: OSQP-backed constructors and prox!. - Users must `using OSQP` before constructing IndPolyhedral(...; solver=:osqp); documented in the IndPolyhedral docstring. - test: add OSQP to the test env and `using OSQP` in test_indPolyhedral.jl. - Bump version to 0.18.0 (minor breaking API change). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01X9Xuox9ELuSiEbxwJq84vJ --- Project.toml | 5 ++- ext/ProximalOperatorsOSQPExt.jl | 57 ++++++++++++++++++++++++ src/functions/indPolyhedral.jl | 3 ++ src/functions/indPolyhedralOSQP.jl | 70 +++++++++--------------------- test/Project.toml | 1 + test/test_indPolyhedral.jl | 1 + 6 files changed, 85 insertions(+), 52 deletions(-) create mode 100644 ext/ProximalOperatorsOSQPExt.jl diff --git a/Project.toml b/Project.toml index 9c1a305c..aacaab94 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ProximalOperators" uuid = "a725b495-10eb-56fe-b38b-717eba820537" -version = "0.17.0" +version = "0.18.0" [workspace] projects = ["docs", "test", "benchmark"] @@ -8,15 +8,16 @@ projects = ["docs", "test", "benchmark"] [deps] IterativeSolvers = "42fd0dbc-a981-5370-80f2-aaf504508153" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -OSQP = "ab2f91bb-94b4-55e3-9ba0-7f65df51de79" ProximalCore = "dc4f5ac2-75d1-4f31-931e-60435d74994b" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" TSVD = "9449cd9e-2762-5aa3-a617-5413e99d722e" [weakdeps] +OSQP = "ab2f91bb-94b4-55e3-9ba0-7f65df51de79" RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" [extensions] +ProximalOperatorsOSQPExt = "OSQP" RecursiveArrayToolsExt = "RecursiveArrayTools" [compat] diff --git a/ext/ProximalOperatorsOSQPExt.jl b/ext/ProximalOperatorsOSQPExt.jl new file mode 100644 index 00000000..0365c75e --- /dev/null +++ b/ext/ProximalOperatorsOSQPExt.jl @@ -0,0 +1,57 @@ +module ProximalOperatorsOSQPExt + +using LinearAlgebra +using SparseArrays +using OSQP + +using ProximalOperators +using ProximalOperators: IndPolyhedralOSQP +import ProximalCore: prox! + +# constructors + +function ProximalOperators.IndPolyhedralOSQP( + l::AbstractVector{R}, A::AbstractMatrix{R}, u::AbstractVector{R} +) where R + m, n = size(A) + if !all(l .<= u) + error("function is improper (are some bounds inverted?)") + end + mod = OSQP.Model() + OSQP.setup!(mod; P=SparseMatrixCSC{R}(I, n, n), l=l, A=sparse(A), u=u, verbose=false, + eps_abs=eps(R), eps_rel=eps(R), + eps_prim_inf=eps(R), eps_dual_inf=eps(R)) + return IndPolyhedralOSQP{R, typeof(mod)}(l, A, u, mod) +end + +ProximalOperators.IndPolyhedralOSQP( + l::AbstractVector{R}, A::AbstractMatrix{R}, u::AbstractVector{R}, + xmin::AbstractVector{R}, xmax::AbstractVector{R} +) where R = + IndPolyhedralOSQP([l; xmin], [A; I], [u; xmax]) + +ProximalOperators.IndPolyhedralOSQP( + l::AbstractVector{R}, A::AbstractMatrix{R}, args... +) where R = + IndPolyhedralOSQP( + l, SparseMatrixCSC(A), R(Inf).*ones(R, size(A, 1)), args... + ) + +ProximalOperators.IndPolyhedralOSQP( + A::AbstractMatrix{R}, u::AbstractVector{R}, args... +) where R = + IndPolyhedralOSQP( + R(-Inf).*ones(R, size(A, 1)), SparseMatrixCSC(A), u, args... + ) + +# prox + +function prox!(y, f::IndPolyhedralOSQP, x, gamma) + R = eltype(x) + OSQP.update!(f.mod; q=-x) + results = OSQP.solve!(f.mod) + y .= results.x + return R(0) +end + +end # module ProximalOperatorsOSQPExt diff --git a/src/functions/indPolyhedral.jl b/src/functions/indPolyhedral.jl index 8405c2c3..74b7a836 100644 --- a/src/functions/indPolyhedral.jl +++ b/src/functions/indPolyhedral.jl @@ -14,6 +14,9 @@ S = \\{ x : x_\\min \\leq x \\leq x_\\max, l \\leq Ax \\leq u \\}. ``` Matrix `A` is a mandatory argument; when any of the bounds is not provided, it is assumed to be (plus or minus) infinity. + +The default `solver=:osqp` backend is provided by a package extension: load it +with `using OSQP` before constructing the object, otherwise an error is raised. """ function IndPolyhedral(args...; solver=:osqp) if solver == :osqp diff --git a/src/functions/indPolyhedralOSQP.jl b/src/functions/indPolyhedralOSQP.jl index 6fb67d7f..6be6dce8 100644 --- a/src/functions/indPolyhedralOSQP.jl +++ b/src/functions/indPolyhedralOSQP.jl @@ -1,25 +1,20 @@ # IndPolyhedral: OSQP implementation +# +# The solver-backed constructors and `prox!` live in the package extension +# `ext/ProximalOperatorsOSQPExt.jl`, which is only loaded once the OSQP package +# is available (`using OSQP`). Without OSQP loaded, constructing an +# `IndPolyhedralOSQP` (directly or via `IndPolyhedral(...; solver=:osqp)`) +# raises an informative error. -using OSQP - -struct IndPolyhedralOSQP{R} <: IndPolyhedral +struct IndPolyhedralOSQP{R, M} <: IndPolyhedral l::AbstractVector{R} A::AbstractMatrix{R} u::AbstractVector{R} - mod::OSQP.Model - function IndPolyhedralOSQP{R}( - l::AbstractVector{R}, A::AbstractMatrix{R}, u::AbstractVector{R} - ) where R - m, n = size(A) - mod = OSQP.Model() - if !all(l .<= u) - error("function is improper (are some bounds inverted?)") - end - OSQP.setup!(mod; P=SparseMatrixCSC{R}(I, n, n), l=l, A=sparse(A), u=u, verbose=false, - eps_abs=eps(R), eps_rel=eps(R), - eps_prim_inf=eps(R), eps_dual_inf=eps(R)) - new(l, A, u, mod) - end + mod::M + # Explicit inner constructor: suppresses the auto-generated 4-positional-arg + # outer constructor, which would otherwise shadow the `(l, A, xmin, xmax)` + # constructor added by ProximalOperatorsOSQPExt. + IndPolyhedralOSQP{R, M}(l, A, u, mod) where {R, M} = new{R, M}(l, A, u, mod) end # properties @@ -27,31 +22,16 @@ end is_proximable(::Type{<:IndPolyhedralOSQP}) = false # constructors +# +# The real implementations are added to this function by +# ProximalOperatorsOSQPExt; this fallback only fires when OSQP is not loaded. -IndPolyhedralOSQP( - l::AbstractVector{R}, A::AbstractMatrix{R}, u::AbstractVector{R} -) where R = - IndPolyhedralOSQP{R}(l, A, u) - -IndPolyhedralOSQP( - l::AbstractVector{R}, A::AbstractMatrix{R}, u::AbstractVector{R}, - xmin::AbstractVector{R}, xmax::AbstractVector{R} -) where R = - IndPolyhedralOSQP([l; xmin], [A; I], [u; xmax]) - -IndPolyhedralOSQP( - l::AbstractVector{R}, A::AbstractMatrix{R}, args... -) where R = - IndPolyhedralOSQP( - l, SparseMatrixCSC(A), R(Inf).*ones(R, size(A, 1)), args... - ) - -IndPolyhedralOSQP( - A::AbstractMatrix{R}, u::AbstractVector{R}, args... -) where R = - IndPolyhedralOSQP( - R(-Inf).*ones(R, size(A, 1)), SparseMatrixCSC(A), u, args... +function IndPolyhedralOSQP(args...; kwargs...) + error( + "IndPolyhedralOSQP requires the OSQP package: run `using OSQP` before " * + "constructing IndPolyhedralOSQP(...) or IndPolyhedral(...; solver=:osqp)." ) +end # function evaluation @@ -61,16 +41,6 @@ function (f::IndPolyhedralOSQP)(x) return all(f.l .<= Ax .<= f.u) ? R(0) : Inf end -# prox - -function prox!(y, f::IndPolyhedralOSQP, x, gamma) - R = eltype(x) - OSQP.update!(f.mod; q=-x) - results = OSQP.solve!(f.mod) - y .= results.x - return R(0) -end - # naive prox # we want to compute the projection p of a point x diff --git a/test/Project.toml b/test/Project.toml index 07c19fe7..639790d4 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -2,6 +2,7 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +OSQP = "ab2f91bb-94b4-55e3-9ba0-7f65df51de79" ProximalCore = "dc4f5ac2-75d1-4f31-931e-60435d74994b" ProximalOperators = "a725b495-10eb-56fe-b38b-717eba820537" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" diff --git a/test/test_indPolyhedral.jl b/test/test_indPolyhedral.jl index 0d0a0bb0..4322f52d 100644 --- a/test/test_indPolyhedral.jl +++ b/test/test_indPolyhedral.jl @@ -1,4 +1,5 @@ using ProximalOperators +using OSQP # loads ProximalOperatorsOSQPExt, enabling IndPolyhedral(...; solver=:osqp) using Test @testset "IndPolyhedral" begin From b2ab103340b5cf37696db4a992e085ec28221c8d Mon Sep 17 00:00:00 2001 From: Tamas Hakkel Date: Tue, 8 Sep 2026 11:32:10 +0200 Subject: [PATCH 2/3] Inline IndPolyhedralOSQP stub into indPolyhedral.jl; move eval + prox_naive to ext Follow-up to the previous commit: - Delete src/functions/indPolyhedralOSQP.jl. The IndPolyhedralOSQP struct, its inner constructor, the is_proximable trait and the not-loaded stub constructor are now inlined into src/functions/indPolyhedral.jl, so the whole non-OSQP surface of IndPolyhedral lives in one file. - Move the function evaluation `(f::IndPolyhedralOSQP)(x)` and `prox_naive` into ext/ProximalOperatorsOSQPExt.jl. The extension now holds everything that operates on an IndPolyhedralOSQP instance; the main package only defines the type and the error stub. No behavioural change: an instance can only exist once OSQP is loaded, so moving its methods to the extension is transparent. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01X9Xuox9ELuSiEbxwJq84vJ --- ext/ProximalOperatorsOSQPExt.jl | 39 +++++++++++++++- src/functions/indPolyhedral.jl | 32 +++++++++++++- src/functions/indPolyhedralOSQP.jl | 71 ------------------------------ 3 files changed, 68 insertions(+), 74 deletions(-) delete mode 100644 src/functions/indPolyhedralOSQP.jl diff --git a/ext/ProximalOperatorsOSQPExt.jl b/ext/ProximalOperatorsOSQPExt.jl index 0365c75e..3280d9eb 100644 --- a/ext/ProximalOperatorsOSQPExt.jl +++ b/ext/ProximalOperatorsOSQPExt.jl @@ -6,7 +6,7 @@ using OSQP using ProximalOperators using ProximalOperators: IndPolyhedralOSQP -import ProximalCore: prox! +import ProximalCore: prox, prox! # constructors @@ -44,6 +44,14 @@ ProximalOperators.IndPolyhedralOSQP( R(-Inf).*ones(R, size(A, 1)), SparseMatrixCSC(A), u, args... ) +# function evaluation + +function (f::IndPolyhedralOSQP)(x) + R = eltype(x) + Ax = f.A * x + return all(f.l .<= Ax .<= f.u) ? R(0) : Inf +end + # prox function prox!(y, f::IndPolyhedralOSQP, x, gamma) @@ -54,4 +62,33 @@ function prox!(y, f::IndPolyhedralOSQP, x, gamma) return R(0) end +# naive prox + +# we want to compute the projection p of a point x +# +# primal problem is: minimize_p (1/2)||p-x||^2 + g(Ap) +# where g is the indicator of the box [l, u] +# +# dual problem is: minimize_y (1/2)||-A'y||^2 - x'A'y + g*(y) +# can solve with (fast) dual proximal gradient method + +function ProximalOperators.prox_naive(f::IndPolyhedralOSQP, x, gamma) + R = eltype(x) + y = zeros(R, size(f.A, 1)) # dual vector + y1 = y + g = IndBox(f.l, f.u) + gstar = Conjugate(g) + gstar_y = R(0) + stepsize = R(1)/opnorm(Matrix(f.A*f.A')) + for it = 1:1e6 + w = y + (it-1)/(it+2)*(y - y1) + y1 = y + z = w - stepsize * (f.A * (f.A'*w - x)) + y, = prox(gstar, z, stepsize) + if norm(y-w)/(1+norm(w)) <= 1e-12 break end + end + p = -f.A'*y + x + return p, R(0) +end + end # module ProximalOperatorsOSQPExt diff --git a/src/functions/indPolyhedral.jl b/src/functions/indPolyhedral.jl index 74b7a836..989db847 100644 --- a/src/functions/indPolyhedral.jl +++ b/src/functions/indPolyhedral.jl @@ -26,6 +26,34 @@ function IndPolyhedral(args...; solver=:osqp) end end -# including concrete types +# IndPolyhedral: OSQP implementation +# +# The struct below is defined here so that the `solver=:osqp` dispatch above can +# refer to it, but everything that actually needs OSQP -- the constructors, +# `prox!`, the function evaluation and `prox_naive` -- lives in the package +# extension `ext/ProximalOperatorsOSQPExt.jl`, loaded once OSQP is available +# (`using OSQP`). Without OSQP loaded, constructing an `IndPolyhedralOSQP` +# (directly or via `IndPolyhedral(...; solver=:osqp)`) raises an informative +# error. -include("indPolyhedralOSQP.jl") +struct IndPolyhedralOSQP{R, M} <: IndPolyhedral + l::AbstractVector{R} + A::AbstractMatrix{R} + u::AbstractVector{R} + mod::M + # Explicit inner constructor: suppresses the auto-generated 4-positional-arg + # outer constructor, which would otherwise shadow the `(l, A, xmin, xmax)` + # constructor added by ProximalOperatorsOSQPExt. + IndPolyhedralOSQP{R, M}(l, A, u, mod) where {R, M} = new{R, M}(l, A, u, mod) +end + +is_proximable(::Type{<:IndPolyhedralOSQP}) = false + +# The real constructors are added to this function by ProximalOperatorsOSQPExt; +# this fallback only fires when OSQP is not loaded. +function IndPolyhedralOSQP(args...; kwargs...) + error( + "IndPolyhedralOSQP requires the OSQP package: run `using OSQP` before " * + "constructing IndPolyhedralOSQP(...) or IndPolyhedral(...; solver=:osqp)." + ) +end diff --git a/src/functions/indPolyhedralOSQP.jl b/src/functions/indPolyhedralOSQP.jl deleted file mode 100644 index 6be6dce8..00000000 --- a/src/functions/indPolyhedralOSQP.jl +++ /dev/null @@ -1,71 +0,0 @@ -# IndPolyhedral: OSQP implementation -# -# The solver-backed constructors and `prox!` live in the package extension -# `ext/ProximalOperatorsOSQPExt.jl`, which is only loaded once the OSQP package -# is available (`using OSQP`). Without OSQP loaded, constructing an -# `IndPolyhedralOSQP` (directly or via `IndPolyhedral(...; solver=:osqp)`) -# raises an informative error. - -struct IndPolyhedralOSQP{R, M} <: IndPolyhedral - l::AbstractVector{R} - A::AbstractMatrix{R} - u::AbstractVector{R} - mod::M - # Explicit inner constructor: suppresses the auto-generated 4-positional-arg - # outer constructor, which would otherwise shadow the `(l, A, xmin, xmax)` - # constructor added by ProximalOperatorsOSQPExt. - IndPolyhedralOSQP{R, M}(l, A, u, mod) where {R, M} = new{R, M}(l, A, u, mod) -end - -# properties - -is_proximable(::Type{<:IndPolyhedralOSQP}) = false - -# constructors -# -# The real implementations are added to this function by -# ProximalOperatorsOSQPExt; this fallback only fires when OSQP is not loaded. - -function IndPolyhedralOSQP(args...; kwargs...) - error( - "IndPolyhedralOSQP requires the OSQP package: run `using OSQP` before " * - "constructing IndPolyhedralOSQP(...) or IndPolyhedral(...; solver=:osqp)." - ) -end - -# function evaluation - -function (f::IndPolyhedralOSQP)(x) - R = eltype(x) - Ax = f.A * x - return all(f.l .<= Ax .<= f.u) ? R(0) : Inf -end - -# naive prox - -# we want to compute the projection p of a point x -# -# primal problem is: minimize_p (1/2)||p-x||^2 + g(Ap) -# where g is the indicator of the box [l, u] -# -# dual problem is: minimize_y (1/2)||-A'y||^2 - x'A'y + g*(y) -# can solve with (fast) dual proximal gradient method - -function prox_naive(f::IndPolyhedralOSQP, x, gamma) - R = eltype(x) - y = zeros(R, size(f.A, 1)) # dual vector - y1 = y - g = IndBox(f.l, f.u) - gstar = Conjugate(g) - gstar_y = R(0) - stepsize = R(1)/opnorm(Matrix(f.A*f.A')) - for it = 1:1e6 - w = y + (it-1)/(it+2)*(y - y1) - y1 = y - z = w - stepsize * (f.A * (f.A'*w - x)) - y, = prox(gstar, z, stepsize) - if norm(y-w)/(1+norm(w)) <= 1e-12 break end - end - p = -f.A'*y + x - return p, R(0) -end From 0c17c73d2ac38479a2e92f20beb7cdfef69bbd96 Mon Sep 17 00:00:00 2001 From: Tamas Hakkel Date: Tue, 8 Sep 2026 12:48:09 +0200 Subject: [PATCH 3/3] Revert version bump Leave the version at 0.17.0; the maintainers can decide the release number. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01X9Xuox9ELuSiEbxwJq84vJ --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index aacaab94..898241a9 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ProximalOperators" uuid = "a725b495-10eb-56fe-b38b-717eba820537" -version = "0.18.0" +version = "0.17.0" [workspace] projects = ["docs", "test", "benchmark"]