Skip to content

Commit ed63b35

Browse files
authored
Add support for setting MOI.VariablePrimalStart (#588)
1 parent a58c79b commit ed63b35

File tree

4 files changed

+137
-25
lines changed

4 files changed

+137
-25
lines changed

docs/src/manual/advanced.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,31 @@ p.constraints[1].dual
3434
constraint.dual
3535
```
3636

37+
## Warmstarting
38+
39+
If you're solving the same problem many times with different values of a
40+
parameter, Convex.jl can initialize many solvers with the solution to the
41+
previous problem, which sometimes speeds up the solution time. This is called a
42+
**warm start**.
43+
44+
To use this feature, pass the optional argument `warmstart=true` to the `solve!`
45+
method.
46+
47+
```julia
48+
using Convex, SCS
49+
n = 1_000
50+
y = rand(n)
51+
x = Variable(n)
52+
lambda = Variable(Positive())
53+
fix!(lambda, 100)
54+
problem = minimize(sumsquares(y - x) + lambda * sumsquares(x - 10))
55+
@time solve!(problem, SCS.Optimizer)
56+
# Now warmstart. If the solver takes advantage of warmstarts, this run will be
57+
# faster
58+
fix!(lambda, 105)
59+
@time solve!(problem, SCS.Optimizer; warmstart = true)
60+
```
61+
3762
## Fixing and freeing variables
3863

3964
Convex.jl allows you to fix a variable `x` to a value by calling the `fix!`

src/solution.jl

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,56 @@ function add_variables!(model, var::AbstractVariable)
77
return MOI.add_variables(model, length(var))
88
end
99

10+
function _primal_start(
11+
::Type{T},
12+
x::Vector{MOI.VariableIndex},
13+
start::AbstractArray,
14+
) where {T}
15+
return convert(Vector{T}, real.(reshape(start, length(x))))
16+
end
17+
18+
function _primal_start(
19+
::Type{T},
20+
x::Tuple{Vector{MOI.VariableIndex},Vector{MOI.VariableIndex}},
21+
start::AbstractArray,
22+
) where {T}
23+
y = reshape(start, length(x[1]))
24+
return convert(Vector{T}, real.(y)), convert(Vector{T}, imag.(y))
25+
end
26+
27+
function _primal_start(::Type{T}, x::Vector, start::Number) where {T}
28+
return _primal_start(T, x, fill(start, length(x)))
29+
end
30+
31+
function _primal_start(::Type{T}, x::Tuple, start::Number) where {T}
32+
return _primal_start(T, x, fill(start, length(x[1])))
33+
end
34+
35+
function _add_variable_primal_start(context::Convex.Context{T}) where {T}
36+
attr = MOI.VariablePrimalStart()
37+
for (id, moi_indices) in context.var_id_to_moi_indices
38+
x = context.id_to_variables[id]
39+
if x.value === nothing
40+
continue
41+
elseif moi_indices isa Tuple # Variable is complex
42+
start_re, start_im = _primal_start(T, moi_indices, x.value)
43+
MOI.set(context.model, attr, moi_indices[1], start_re)
44+
MOI.set(context.model, attr, moi_indices[2], start_im)
45+
else
46+
@assert moi_indices isa Vector{MOI.VariableIndex}
47+
start = _primal_start(T, moi_indices, x.value)
48+
MOI.set(context.model, attr, moi_indices, start)
49+
end
50+
end
51+
return
52+
end
53+
1054
"""
1155
solve!(
1256
problem::Problem,
1357
optimizer_factory;
1458
silent_solver = false,
59+
warmstart::Bool = true,
1560
)
1661
1762
Solves the problem, populating `problem.optval` with the optimal value, as well
@@ -22,15 +67,27 @@ Optional keyword arguments:
2267
2368
* `silent_solver`: whether the solver should be silent (and not emit output or
2469
logs) during the solution process.
70+
* `warmstart` (default: `false`): whether the solver should start the
71+
optimization from a previous optimal value (according to the current primal
72+
value of the variables in the problem, which can be set by [`set_value!`](@ref).
2573
"""
26-
function solve!(p::Problem, optimizer_factory; silent_solver = false)
74+
function solve!(
75+
p::Problem,
76+
optimizer_factory;
77+
silent_solver = false,
78+
warmstart::Bool = false,
79+
)
2780
if problem_vexity(p) in (ConcaveVexity(), NotDcp())
2881
throw(DCPViolationError())
2982
end
3083
context = Context(p, optimizer_factory)
3184
if silent_solver
3285
MOI.set(context.model, MOI.Silent(), true)
3386
end
87+
attr = MOI.VariablePrimalStart()
88+
if warmstart && MOI.supports(context.model, attr, MOI.VariableIndex)
89+
_add_variable_primal_start(context)
90+
end
3491
if context.detected_infeasible_during_formulation[]
3592
p.status = MOI.INFEASIBLE
3693
else

test/test_problem_depot.jl

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import Clarabel
77
# import ECOS
88
import GLPK
99
import MathOptInterface as MOI
10-
# import SCS
1110

1211
function runtests()
1312
for name in names(@__MODULE__; all = true)
@@ -40,29 +39,22 @@ end
4039
test_problem_depot_load_Float64() = _test_problem_depot_load(Float64)
4140
test_problem_depot_load_BigFloat() = _test_problem_depot_load(BigFloat)
4241

43-
# function test_SCS_with_warmstarts()
44-
# Convex.ProblemDepot.run_tests(
45-
# exclude = [
46-
# r"mip",
47-
# # TODO(odow): investigate
48-
# r"sdp_lieb_ando",
49-
# # Tolerance issue with SCS 3.0
50-
# r"sdp_Real_Variables_with_complex_equality_constraints",
51-
# ],
52-
# ) do p
53-
# return solve!(
54-
# p,
55-
# MOI.OptimizerWithAttributes(
56-
# SCS.Optimizer,
57-
# "verbose" => 0,
58-
# "eps_rel" => 1e-6,
59-
# "eps_abs" => 1e-6,
60-
# );
61-
# warmstart = true,
62-
# )
63-
# end
64-
# return
65-
# end
42+
function test_Clarabel_warmstarts()
43+
# `sdp_quantum_relative_entropy3_lowrank` failed on CI for Ubuntu with
44+
# Expression: ≈(p.optval, evaluate(quantum_relative_entropy(B, A)), atol = atol, rtol = rtol)
45+
# Evaluated: -4.887297347885561e-6 ≈ Inf (atol=0.001, rtol=0.0)
46+
Convex.ProblemDepot.run_tests(;
47+
exclude = [r"mip", r"sdp_quantum_relative_entropy3_lowrank"],
48+
) do p
49+
return solve!(
50+
p,
51+
Clarabel.Optimizer;
52+
silent_solver = true,
53+
warmstart = true,
54+
)
55+
end
56+
return
57+
end
6658

6759
function test_Clarabel()
6860
# `sdp_quantum_relative_entropy3_lowrank` failed on CI for Ubuntu with

test/test_utilities.jl

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,6 +1246,44 @@ function test_set_value_complex()
12461246
return
12471247
end
12481248

1249+
function test_variable_primal_start()
1250+
x1 = Variable()
1251+
x2 = Variable(2)
1252+
x2.value = [1.0, 2.0]
1253+
x3 = Variable(3, 3)
1254+
x3.value = 3.0
1255+
x4 = ComplexVariable()
1256+
x4.value = 1 + 2im
1257+
x5 = ComplexVariable(2, 2)
1258+
x5.value = [1 2; 3im 4]
1259+
problem = satisfy([x1 >= 0, x2 <= 0, x3 <= 1, x4 == 5, x5 == 0])
1260+
context = Convex.Context(
1261+
problem,
1262+
() ->
1263+
MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
1264+
)
1265+
Convex._add_variable_primal_start(context)
1266+
for (key, query) in (
1267+
x1 => [nothing],
1268+
x2 => [1.0, 2.0],
1269+
x3 => fill(3.0, 9),
1270+
x4 => ([1.0], [2.0]),
1271+
x5 => ([1.0, 0.0, 2.0, 4.0], [0.0, 3.0, 0.0, 0.0]),
1272+
)
1273+
variable = context.var_id_to_moi_indices[key.id_hash]
1274+
start = if variable isa Tuple
1275+
(
1276+
MOI.get(context.model, MOI.VariablePrimalStart(), variable[1]),
1277+
MOI.get(context.model, MOI.VariablePrimalStart(), variable[2]),
1278+
)
1279+
else
1280+
MOI.get(context.model, MOI.VariablePrimalStart(), variable)
1281+
end
1282+
@test start == query
1283+
end
1284+
return
1285+
end
1286+
12491287
end # TestUtilities
12501288

12511289
TestUtilities.runtests()

0 commit comments

Comments
 (0)