Hi @odow
Thank you very much for your response. Yes, indeed, it is OSQP.Solve!. I ran the following benchmark code:
# ============================================================
# Reproducible example: QP projection with equality +
# inequality constraints using OSQP.jl
# ============================================================
using OSQP, SparseArrays, LinearAlgebra, Random, BenchmarkTools, Profile
struct ModelParams
n_data::Int
window_time::Float64
clipping_threshold::Float64
G_normal::Matrix{Float64}
end
function classify_data_points(data::Vector{Float64}, p::ModelParams)
upper_idx = findall(x -> x >= p.clipping_threshold, data)
lower_idx = findall(x -> x <= -p.clipping_threshold, data)
trusted_idx = findall(x -> abs(x) < p.clipping_threshold && x != 0.0, data)
return trusted_idx, upper_idx, lower_idx
end
function project_onto_ellipsoid(x::Vector{Float64}, p::ModelParams)
energy = x' * p.G_normal * x
energy <= p.n_data && return x
return x * sqrt(p.window_time / energy)
end
function build_osqp_solver(
A_eq::Matrix{Float64},
A_upper::Matrix{Float64},
A_lower::Matrix{Float64},
n::Int,
p::ModelParams
)
n_eq = size(A_eq, 1)
n_upper = size(A_upper, 1)
n_lower = size(A_lower, 1)
n_con = n_eq + n_upper + n_lower
P = sparse(2.0 * I, n, n)
if n_con == 0
A_con = spzeros(0, n)
else
blocks = Matrix{Float64}[]
n_eq > 0 && push!(blocks, A_eq)
n_upper > 0 && push!(blocks, A_upper)
n_lower > 0 && push!(blocks, A_lower)
A_con = sparse(vcat(blocks...))
end
l = zeros(n_con)
u = zeros(n_con)
if n_upper > 0
l[n_eq+1 : n_eq+n_upper] .= p.clipping_threshold
u[n_eq+1 : n_eq+n_upper] .= Inf
end
if n_lower > 0
l[n_eq+n_upper+1 : end] .= -Inf
u[n_eq+n_upper+1 : end] .= -p.clipping_threshold
end
solver = OSQP.Model()
OSQP.setup!(solver;
P = P,
q = zeros(n),
A = A_con,
l = l,
u = u,
warm_starting = true,
verbose = false,
eps_abs = 1e-6,
eps_rel = 1e-6,
max_iter = 10000,
polish = true
)
return solver, n_eq, l, u
end
function project_single(
x0::Vector{Float64},
b_eq::Vector{Float64},
solver::OSQP.Model,
n_eq::Int,
l::Vector{Float64},
u::Vector{Float64},
p::ModelParams
)
x_ellipsoid = project_onto_ellipsoid(x0, p)
q = -2.0 .* x_ellipsoid
if n_eq > 0
l[1:n_eq] .= b_eq
u[1:n_eq] .= b_eq
OSQP.update!(solver; q=q, l=l, u=u)
else
OSQP.update!(solver; q=q)
end
res = OSQP.solve!(solver)
if res.info.status != :Solved && res.info.status != :Almost_Solved
@warn "OSQP status: $(res.info.status). Returning ellipsoid projection."
return x_ellipsoid
end
return res.x
end
function data_subspace_projection(
coefficients::Matrix{Float64},
data::Vector{Float64},
basis::Matrix{Float64},
p::ModelParams
)
trusted_idx, upper_idx, lower_idx = classify_data_points(data, p)
n = 2 * p.n_data
maxim_samples = size(coefficients, 1)
result = similar(coefficients)
if isempty(trusted_idx) && isempty(upper_idx) && isempty(lower_idx)
for j in 1:maxim_samples
result[j, :] = project_onto_ellipsoid(coefficients[j, :], p)
end
return result
end
A_eq = isempty(trusted_idx) ? Matrix{Float64}(undef, 0, n) : basis[trusted_idx, :]
A_upper = isempty(upper_idx) ? Matrix{Float64}(undef, 0, n) : basis[upper_idx, :]
A_lower = isempty(lower_idx) ? Matrix{Float64}(undef, 0, n) : basis[lower_idx, :]
b_eq = isempty(trusted_idx) ? Float64[] : data[trusted_idx]
solver, n_eq, l, u = build_osqp_solver(A_eq, A_upper, A_lower, n, p)
ellipsoid_violations = 0
for j in 1:maxim_samples
result[j, :] = project_single(coefficients[j, :], b_eq, solver, n_eq, l, u, p)
if result[j, :]' * p.G_normal * result[j, :] > p.n_data
ellipsoid_violations += 1
end
end
if ellipsoid_violations > 0
@warn "Ellipsoid constraint violated for $ellipsoid_violations / $maxim_samples samples."
end
return result
end
# ===============================================================
# MAIN
# ===============================================================
Random.seed!(42)
n_data = 320
n = 2 * n_data
maxim_samples = 2 * n_data + 1
clipping_threshold = 0.5
A_rand = randn(n, n)
G_normal = (A_rand' * A_rand) / n
p = ModelParams(n_data, 64.0 / 1000.0, clipping_threshold, G_normal)
basis = randn(n_data, n)
raw_signal = 0.8 .* randn(n_data)
data = clamp.(raw_signal, -clipping_threshold, clipping_threshold)
coefficients = 0.1 .* randn(maxim_samples, n)
println("Problem size : $maxim_samples samples × $n coefficients")
println("Trusted pts : ", count(x -> abs(x) < clipping_threshold && x != 0.0, data))
println("Upper clipped: ", count(x -> x >= clipping_threshold, data))
println("Lower clipped: ", count(x -> x <= -clipping_threshold, data))
# ---------------------------------------------------------------
# Explicitly build constraint matrices in main scope for benchmarking
# ---------------------------------------------------------------
trusted_idx, upper_idx, lower_idx = classify_data_points(data, p)
A_eq = isempty(trusted_idx) ? Matrix{Float64}(undef, 0, n) : basis[trusted_idx, :]
A_upper = isempty(upper_idx) ? Matrix{Float64}(undef, 0, n) : basis[upper_idx, :]
A_lower = isempty(lower_idx) ? Matrix{Float64}(undef, 0, n) : basis[lower_idx, :]
b_eq = isempty(trusted_idx) ? Float64[] : data[trusted_idx]
solver, n_eq, l, u = build_osqp_solver(A_eq, A_upper, A_lower, n, p)
x0 = coefficients[1, :]
# ---------------------------------------------------------------
# Warmup — avoid measuring compilation
# ---------------------------------------------------------------
data_subspace_projection(coefficients, data, basis, p)
project_single(x0, b_eq, solver, n_eq, l, u, p)
# ---------------------------------------------------------------
# 1. Coarse benchmarks
# ---------------------------------------------------------------
println("\n--- Coarse benchmarks ---")
print("build_osqp_solver : "); @btime build_osqp_solver($A_eq, $A_upper, $A_lower, $n, $p)
print("project_single : "); @btime project_single($x0, $b_eq, $solver, $n_eq, $l, $u, $p)
print("full loop : "); @btime data_subspace_projection($coefficients, $data, $basis, $p)
# ---------------------------------------------------------------
# 2. project_single internals
# ---------------------------------------------------------------
x_ellipsoid = project_onto_ellipsoid(x0, p)
q_vec = -2.0 .* x_ellipsoid
l[1:n_eq] .= b_eq
u[1:n_eq] .= b_eq
println("\n--- project_single breakdown ---")
print(" project_onto_ellipsoid : "); @btime project_onto_ellipsoid($x0, $p)
print(" OSQP.update! : "); @btime OSQP.update!($solver; q=$q_vec, l=$l, u=$u)
print(" OSQP.solve! : "); @btime OSQP.solve!($solver)
# ---------------------------------------------------------------
# 3. Profiler
# ---------------------------------------------------------------
Profile.clear()
@profile for _ in 1:10
data_subspace_projection(coefficients, data, basis, p)
end
Profile.print(mincount=5, sortedby=:count)
And for a single projection, the breakdown was:
project_onto_ellipsoid : 62.800 μs (3 allocations: 5.07 KiB)
OSQP.update! : 2.644 μs (0 allocations: 0 bytes)
OSQP.solve! : 201.039 ms (14 allocations: 15.50 KiB)
I am solving 641 of these problems, so I was wondering if there is a good QP solver out there that can speed up the computation, or if there are any tricks for the QP formulation that would speed up the process?