Discrete continuation

By using the warm start option, it is easy to implement a basic discrete continuation method, in which a sequence of problems is solved by using each solution as the initial guess for the next problem. This approach typically leads to faster and more reliable convergence than solving each problem with the same initial guess and is particularly useful for problems that require a good initial guess to converge.

Continuation on parametric OCP

The most concise way to perform discrete continuation is to define a function that returns the optimal control problem for a given value of the continuation parameter, and then solve a sequence of such problems. We illustrate this using a simple double integrator problem, where the fixed final time is gradually increased.

First we load the required packages:

using DataFrames
using OptimalControl
using NLPModelsIpopt
using Printf
using Plots

and write a function that returns the OCP for a given final time:

function problem(T)

    ocp = @def begin

        t ∈ [0, T], time
        x ∈ R², state
        u ∈ R, control

        q = x₁
        v = x₂

        q(0) == 0
        v(0) == 0
        q(T) == 1
        v(T) == 0
        ẋ(t) == [v(t), u(t)]

        ∫(u(t)^2) → min

    end

    return ocp
end

Then we perform the continuation with a simple for loop, using each solution to initialize the next problem.

init = ()
data = DataFrame(T=Float64[], Objective=Float64[], Iterations=Int[])
for T ∈ range(1, 2, length=5)
    ocp = problem(T)
    sol = solve(ocp; init=init, display=false)
    global init = sol
    push!(data, (T=T, Objective=objective(sol), Iterations=iterations(sol)))
end
println(data)
5×3 DataFrame
 Row │ T        Objective  Iterations
     │ Float64  Float64    Int64
─────┼────────────────────────────────
   1 │    1.0    12.0008            2
   2 │    1.25    6.14439           3
   3 │    1.5     3.55578           3
   4 │    1.75    2.23921           3
   5 │    2.0     1.5001            3

Continuation on global variable

As a second example, we show how to avoid redefining a new optimal control problem at each step by modifying the original one instead. More precisely, we solve a Goddard problem with a decreasing maximum thrust. By storing the value of Tmax in a global variable, we can simply update this variable and reuse the same problem throughout the continuation.

Let us first define the Goddard problem. Note that the formulation below illustrates all types of constraints, and the problem could be written more compactly.

# Parameters
r0 = 1
v0 = 0
m0 = 1
mf = 0.6
x0 = [r0, v0, m0]
vmax = 0.1

# Goddard problem definition
@def goddard begin

    tf ∈ R, variable
    t ∈ [0, tf], time
    x ∈ R^3, state
    u ∈ R, control

    0.01 ≤ tf ≤ Inf

    r = x[1]
    v = x[2]
    m = x[3]
    x(0) == x0
    m(tf) == mf
    r0 ≤ r(t) ≤ r0 + 0.1
    v0 ≤ v(t) ≤ vmax
    mf ≤ m(t) ≤ m0
    0 ≤ u(t) ≤ 1
    ẋ(t) == F0(x(t)) + u(t) * F1(x(t))

    r(tf) → max

end

# Dynamics
function F0(x)
    r, v, m = x
    D = Cd * v^2 * exp(-β*(r - 1))
    return [ v, -D/m - 1/r^2, 0 ]
end
function F1(x)
    r, v, m = x
    return [ 0, Tmax/m, -b*Tmax ]
end

# Parameters for the dynamics
Cd = 310
β = 500
b = 2
Tmax_0 = 3.5
Tmax_f = 1.0

# Solve the problem with a reference value of Tmax
Tmax = Tmax_0
sol0 = solve(goddard; display=false)
@printf("Objective for reference solution: %.6f\n", objective(sol0))
Objective for reference solution: 1.012576

Then, we perform the continuation on the maximal thrust.

sol = sol0 # Initialize the solution with the reference solution
data = DataFrame(Tmax=Float64[], Objective=Float64[], Iterations=Int[])
for Tmax_local ∈ range(Tmax_0, Tmax_f, length=6)
    global Tmax = Tmax_local # Update the global variable Tmax
    global sol = solve(goddard; init=sol, display=false)
    push!(data, (Tmax=Tmax, Objective=objective(sol), Iterations=iterations(sol)))
end
println(data)
6×3 DataFrame
 Row │ Tmax     Objective  Iterations
     │ Float64  Float64    Int64
─────┼────────────────────────────────
   1 │     3.5    1.01258          14
   2 │     3.0    1.01237          17
   3 │     2.5    1.01202          20
   4 │     2.0    1.01126          19
   5 │     1.5    1.00917          18
   6 │     1.0    1.00359          19

We plot now the objective with respect to the maximal thrust, as well as both solutions for Tmax=3.5 and Tmax=1.

using Plots.PlotMeasures # for leftmargin

plt_obj = plot(data.Tmax, data.Objective;
    seriestype=:scatter,
    title="Goddard problem",
    label="r(tf)",
    xlabel="Maximal thrust (Tmax)",
    ylabel="Maximal altitude r(tf)")

plt_sol = plot(sol0; label="Tmax="*string(data.Tmax[1]))
plot!(plt_sol, sol;  label="Tmax="*string(data.Tmax[end]))

layout = grid(2, 1, heights=[0.2, 0.8])
plot(plt_obj, plt_sol; layout=layout, size=(800, 1000), leftmargin=5mm)
Example block output