# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors    #src
# This Source Code Form is subject to the terms of the Mozilla Public License   #src
# v.2.0. If a copy of the MPL was not distributed with this file, You can       #src
# obtain one at https://mozilla.org/MPL/2.0/.                                   #src

# # Primal and dual warm-starts

# This tutorial demonstrates how to write a helper function that sets primal
# and dual warm-starts from the current optimal solution of a conic model,
# which can improve solver performance when repeatedly solving related problems.
#
# **Learning intentions:**
# * Cache all solution values before modifying the model, because JuMP
#   forbids interleaving solution queries with model modifications
# * Set primal and dual warm-starts using [`set_start_value`](@ref) and
#   [`set_dual_start_value`](@ref) to reduce iterations on repeated solves
# * Handle solvers that do not support warm-starts gracefully using a
#   `try`-`catch` around each constraint type

# ## Required packages

# This tutorial uses the following packages:

using JuMP
import SCS

# ## A basic function

# The main component of this tutorial is the following function. The most
# important observation is that we cache all of the solution values first, and
# then we modify the model second. (Alternating between querying a value and
# modifying the model is not allowed in JuMP.)

function set_optimal_start_values(model::Model)
    ## Store a mapping of the variable primal solution
    variable_primal = Dict(x => value(x) for x in all_variables(model))
    ## In the following, we loop through every constraint and store a mapping
    ## from the constraint index to a tuple containing the primal and dual
    ## solutions.
    constraint_solution = Dict()
    for (F, S) in list_of_constraint_types(model)
        ## We add a try-catch here because some constraint types might not
        ## support getting the primal or dual solution.
        try
            for ci in all_constraints(model, F, S)
                constraint_solution[ci] = (value(ci), dual(ci))
            end
        catch
            @info("Something went wrong getting $F-in-$S. Skipping")
        end
    end
    ## Now we can loop through our cached solutions and set the starting values.
    for (x, primal_start) in variable_primal
        set_start_value(x, primal_start)
    end
    for (ci, (primal_start, dual_start)) in constraint_solution
        set_start_value(ci, primal_start)
        set_dual_start_value(ci, dual_start)
    end
    return
end

# ## Testing the function

# To test our function, we use the following linear program:

model = Model(SCS.Optimizer)
@variable(model, x[1:3] >= 0)
@constraint(model, sum(x) <= 1)
@objective(model, Max, sum(i * x[i] for i in 1:3))
optimize!(model)
assert_is_solved_and_feasible(model)

# By looking at the log, we can see that SCS took 75 iterations to find the optimal
# solution. Now we set the optimal solution as our starting point:

set_optimal_start_values(model)

# and we re-optimize:

optimize!(model)

# Now the optimization terminates after 0 iterations because our starting point
# is already optimal.

# ## Caveats

# Some solvers do not support setting some parts of the starting solution, for
# example, they may support only `set_start_value` for variables.

# If you encounter an `UnsupportedSupported` attribute error for
# [`MOI.VariablePrimalStart`](@ref), [`MOI.ConstraintPrimalStart`](@ref), or
# [`MOI.ConstraintDualStart`](@ref), comment out the corresponding part of the
# `set_optimal_start_values` function.
