Trajectory Optimization - Lecture 2

using Optim
using LinearAlgebra
using SparseArrays
using CairoMakie
using Piccolo

# let's define a shorthand for kron
const ⊗ = kron
kron (generic function with 61 methods)

Getting started

Review

  • Gradient descent
  • Newton's method and KKT conditions
  • Regularization
  • Newton approximations
  • Line search

Goals

  • Introduce trajectory optimization
  • Solve the LQR problem three ways
  • Describe nonlinear trajectory optimization

I. Trajectory optimization

  • The solution is a definite trajectory, not a feedback policy.

\[\begin{align} \min_{x_{1:N}, u_{1:N}} &\quad J(x_{1:N}, u_{1:N}) = \sum_{n=1}^N \ell_n(x_n, u_n) + \ell_N(x_N, u_N) \\ \text{s.t.} &\quad x_{n+1} = f(x_n, u_n, n) \end{align}\]

Named Trajectories

Terminology

  • Snapshot matrix, $Z = \begin{bmatrix} | & | & & | \\ z_1 & z_2 & & z_T \\ | & | & & | \end{bmatrix}$
  • Knot point, $z_1 = \begin{bmatrix} x_1 \\ u_1 \end{bmatrix}$
N = 10 # Number of knot points
traj = rand(NamedTrajectory, N)
T = 10, (x = 1:3, u = 4:5, → Δt = 6:6)
traj.x
3×10 view(reshape(view(::Vector{Float64}, :), 6, 10), 1:3, :) with eltype Float64:
  0.457533  -2.35182    0.660083  -0.856307   …  -1.35003  0.941149  1.75569
  1.69218    0.409696   0.766585   0.0217461     -1.2624   1.09609   0.555132
 -0.704936  -0.0920414  0.351501   0.58511        1.05494  0.186944  0.186521
traj.u
2×10 view(reshape(view(::Vector{Float64}, :), 6, 10), 4:5, :) with eltype Float64:
 1.24957  1.44903   -0.554347  -0.66381  …  0.740205  -0.492164  -0.433807
 0.38033  0.291519  -0.638417  -2.01442     1.78874   -0.143572   0.263034
plot(traj, :x)
Example block output

II. Linear Quadratic Regulator

  • LQR is the "simple harmonic oscillator" of control

\[\begin{align} \min_{x_{1:N}, u_{1:N{-}1}} &\quad J = \sum_{n=1}^{N-1} \tfrac{1}{2} x_n^T Q_n x_n + \tfrac{1}{2} u_n^T R_n u_n + \tfrac{1}{2} x_N^T Q_N x_N \\ \text{s.t.} &\quad x_{n+1} = A_n x_n + B_n u_n \\ &\quad Q_n \succeq 0,\, R_n \succ 0 \end{align}\]

  • Quick check: Why does $R$ need to be positive definite?

Linear systems

Zero-order hold

  • Zero-order hold can be used to convert continuous, linear, time-invariant (LTI) systems to discrete LTI systems.

\[\begin{align} \dot{x}(t) &= A x(t) + B u(t) \\ \overset{h}{\longrightarrow} x(t+h) &= A_h x(t) + B_h u(t) \\ &= \left( \sum_{n\ge0} \tfrac{1}{n!} A^n h^n \right) x + \left( \sum_{n\ge1} \tfrac{1}{n!} A^{n-1} B h^n \right) u \\ &\approx (I + h A) x + h B u \end{align}\]

  • Matrix exponential trick:

\[\exp\left(\begin{bmatrix} A & B \\ 0 & 0 \end{bmatrix} h \right) = \begin{bmatrix} A_h & B_h \\ 0 & I \end{bmatrix}\]

# Define continuous LTI system matrices
A = [0.0 1.0; -1.0 -0.1]
B = [0.0; 1.0]
h = 0.1  # Time step

function continuous_to_discrete(A, B, h)
    # Construct augmented matrix for matrix exponential
    augmented_matrix = [
        A B;
        zeros(size(B, 2), size(A, 1)) zeros(size(B, 2), size(B, 2))
    ]

    # Compute matrix exponential
    exp_matrix = exp(augmented_matrix * h)

    # Extract discrete LTI system matrices
    A_h = exp_matrix[1:size(A, 1), 1:size(A, 2)]
    B_h = exp_matrix[1:size(A, 1), size(A, 2)+1:end]

    return A_h, B_h
end

# Extract discrete LTI system matrices
A_h, B_h = continuous_to_discrete(A, B, h);
([0.9950207737420776 0.09933590957864684; -0.09933590957864684 0.9850871827842128], [0.004979226257922404; 0.09933590957864684;;])
A_h
2×2 Matrix{Float64}:
  0.995021   0.0993359
 -0.0993359  0.985087
I + A * h
2×2 Matrix{Float64}:
  1.0  0.1
 -0.1  0.99
B_h
2×1 Matrix{Float64}:
 0.004979226257922404
 0.09933590957864684
B * h
2-element Vector{Float64}:
 0.0
 0.1

Double Integrator

  • Double integrator (Newton's second law, $F = ma$)

\[m \frac{d}{dt} \begin{bmatrix} q \\ \dot{q} \end{bmatrix} = \begin{bmatrix} 0 & m \\ 0 & 0 \end{bmatrix} \begin{bmatrix} q \\ \dot{q} \end{bmatrix} + \begin{bmatrix} 0 \\ u \end{bmatrix}\]

function double_integrator(m)
    A_c = [0.0 1.0; 0.0 0.0]
    B_c = [0.0; 1.0 / m]
    return A_c, B_c
end

# simulate a discrete LTI system
function simulate_dlti(u::AbstractMatrix, A, B, x1)
    N = size(u, 2) + 1
    x = zeros(size(A, 2), N)
    x[:, 1] = x1
    for k in 1:N-1
        x[:, k + 1] = A * x[:, k] + B * u[:, k]
    end
    return x
end

function simulate_dlti(u::AbstractVector, A, B, x1)
    simulate_dlti(reshape(u, 1, length(u)), A, B, x1)
end
simulate_dlti (generic function with 2 methods)
m = 2.0 # Mass
A_c , B_c = double_integrator(m)
h = 0.05  # Time step
A, B = continuous_to_discrete(A_c, B_c, h);
([1.0 0.05; 0.0 1.0], [0.0006250000000000001; 0.025;;])
A ≈ I + A_c * h
true
B ≈ B_c * h + [h^2 / 2m; 0]
true

Indirect optimal control: Naive way

  • Indirect optimal control is also known as "single shooting"

  • The naive way is to perform gradient descent without any problem structure.

\[\min_{u_{1:N{-}1}} \quad J(u_{1:N{-}1}) = \sum_{n=1}^{N-1} \ell_n(x_n(u_{1:n{-}1}), u_n) + \ell_N(x_N(u_{1:N{-}1}), u_N)\]

  • We will start with the double integrator and solve the LQR problem,

\[\begin{align} \min_{u_{1:N{-}1}} &\quad J(u_{1:{N{-}1}}) = \sum_{n=1}^{N-1} \tfrac{1}{2} x_n(u_{1:n{-}1})^T Q_n x_n(u_{1:n{-}1}) + \tfrac{1}{2} u_n^T R_n u_n + \tfrac{1}{2} x_N(u_{1:N{-}1})^T Q_N x_N(u_{1:N{-}1}) \\ \text{s.t.} &\quad Q_n \succeq 0,\, R_n \succ 0 \end{align}\]

m = 0.1 # Mass
A_c, B_c = double_integrator(m)
h = 0.1  # Time step
A, B = continuous_to_discrete(A_c, B_c, h)
x1 = [1.0; 2.0]  # Initial state

Q = 1e-4I
R = 1e-1I
QN = 1e2I

function J(
    x::AbstractMatrix,
    u::AbstractVecOrMat;
    Q = 1e-2I,
    R = 1e-4I,
    QN = 1e2I
)
    u = isa(u, AbstractMatrix) ? u : reshape(u, 1, length(u))

    N = size(u, 2) + 1
    J = 0.0
    for n in 1:N-1
        xₙ = x[:, n]
        uₙ = u[:, n]
        J += 1/2 * (xₙ' * Q * xₙ + uₙ' * R * uₙ)
    end
    J += 1/2 * (x[:, N]' * QN * x[:, N])
    return J
end

function J(u::AbstractVecOrMat; A=A, B=B, x1=x1, kwargs...)
    x = simulate_dlti(u, A, B, x1)
    return J(x, u; kwargs...)
end
J (generic function with 2 methods)

Gradient descent

N = 40
u0 = randn(N - 1)
J(u0; A=A, B=B, x1=x1, Q=Q, R=R, QN=QN)
1097.8158832101492
fig, ax = series(simulate_dlti(u0, A, B, x1), labels=["q", "q̇"])
axislegend(ax)
fig
Example block output
res = optimize(u -> J(u; A=A, B=B, x1=x1, Q=Q, R=R, QN=QN), u0, GradientDescent(),
               Optim.Options(iterations=50))
 * Status: failure (reached maximum number of iterations)

 * Candidate solution
    Final objective value:     1.641849e+00

 * Found with
    Algorithm:     Gradient Descent

 * Convergence measures
    |x - x'|               = 2.27e-04 ≰ 0.0e+00
    |x - x'|/|x'|          = 9.14e-05 ≰ 0.0e+00
    |f(x) - f(x')|         = 1.47e-04 ≰ 0.0e+00
    |f(x) - f(x')|/|f(x')| = 8.95e-05 ≰ 0.0e+00
    |g(x)|                 = 7.91e-01 ≰ 1.0e-08

 * Work counters
    Seconds run:   0  (vs limit Inf)
    Iterations:    50
    f(x) calls:    151
    ∇f(x) calls:   151
fig, ax = series(simulate_dlti(res.minimizer, A, B, x1), labels=["q", "q̇"])
axislegend(ax)
fig
Example block output
stairs(res.minimizer)
Example block output

Newton's method

res_newton = optimize(u -> J(u; A=A, B=B, x1=x1, Q=Q, R=R, QN=QN), u0, Newton(),
                     Optim.Options(iterations=20))
 * Status: success

 * Candidate solution
    Final objective value:     3.418396e-02

 * Found with
    Algorithm:     Newton's Method

 * Convergence measures
    |x - x'|               = 1.45e-03 ≰ 0.0e+00
    |x - x'|/|x'|          = 5.44e-03 ≰ 0.0e+00
    |f(x) - f(x')|         = 3.02e-07 ≰ 0.0e+00
    |f(x) - f(x')|/|f(x')| = 8.83e-06 ≰ 0.0e+00
    |g(x)|                 = 6.88e-12 ≤ 1.0e-08

 * Work counters
    Seconds run:   0  (vs limit Inf)
    Iterations:    2
    f(x) calls:    7
    ∇f(x) calls:   7
    ∇²f(x) calls:  2
fig, ax = series(simulate_dlti(res_newton.minimizer, A, B, x1), labels=["q", "q̇"])
axislegend(ax)
fig
Example block output
stairs(res_newton.minimizer)
Example block output

Indirect optimal control: Pontryagin

Tagline

"optimize, then discretize"

Ideas

  • We can do this in a smart way by using the temporal structure of the problem.

\[\begin{align} \min_{x_{1:N}, u_{1:{N{-}1}}} & \quad J(x_{1:N}, u_{1:{N{-}1}}) = \sum_{n=1}^{N-1} \tfrac{1}{2} x_n^T Q_n x_n + \tfrac{1}{2} u_n^T R_n u_n + \tfrac{1}{2} x_N^T Q_N x_N \\ \text{s.t.} &\quad x_{n+1} = A_n x_n + B_n u_n \\ &\quad Q_n \succeq 0,\, R_n \succ 0 \end{align}\]

  • Lagrangian:

\[L(x_{1:N}, u_{1:{N{-}1}}, \lambda_{2:N}) = \sum_{n=1}^{N-1} \tfrac{1}{2} x_n^T Q_n x_n + \tfrac{1}{2} u_n^T R_n u_n + \lambda_{n+1}^T(A_n x_n + B_n u_n - x_{n+1}) + \tfrac{1}{2} x_N^T Q_N x_N\]

  • KKT conditions:

\[\begin{align} \frac{\partial L}{\partial \lambda_n} &= (A_n x_n + B_n u_n - x_{n+1})^T \overset{!}{=} 0 \\ \frac{\partial L}{\partial x_n} &= x_n^T Q_n + \lambda_{n+1}^T A_n - \lambda_{n}^T \overset{!}{=} 0 \\ \frac{\partial L}{\partial x_N} &= x_N^T Q_N - \lambda_{N}^T \overset{!}{=} 0 \\ \frac{\partial L}{\partial u_n} &= u_n^T R_n + \lambda_{n+1}^T B_n \overset{!}{=} 0 \end{align}\]

  • Rewrite:

\[\begin{align} x_{n+1} &= A_n x_n + B_n u_n \\ \lambda_{n} &= A_n^T \lambda_{n+1} + Q_n x_n \\ \lambda_N &= Q_N x_N \\ u_n &= -R_n^{-1} B_n^T \lambda_{n+1} \end{align}\]

  • There is a forward equation and a backward equation. This computation is backpropagation through time.
  • We inherit many of the same problems, e.g., vanishing / exploding gradients.
N = 60
m = 1.0 # Mass
A_c, B_c = double_integrator(m)
h = 0.1  # Time step
A, B = continuous_to_discrete(A_c, B_c, h)
x1 = [1.0; 1.0]  # Initial state

Q = 1e-4I
R = 1e-2I
QN = 1e1I

# Initial guess
u = zeros(1, N - 1)
Δu = ones(1, N - 1)
x = simulate_dlti(u, A, B, x1)
λ = zeros(size(x, 1), N)

# Line search parameters
α = 1.0 # Step size
b = 1e-2 # Tolerance
α_min = 1e-16 # Minimum step size
loop = 0
max_iterations = 20

# Verbose
verbose = false

while maximum(abs, Δu) > 1e-2 && α > α_min && loop < max_iterations
    verbose ? println("Iteration: ", loop) : nothing

    # Backward pass to compute λ and Δu
    λ[:, N] .= QN * x[:, N]
    for n = N-1:-1:1
        Δu[:, n] .= - R\B' * λ[:, n+1] - u[:, n]
        λ[:, n] .= Q * x[:, n] + A' * λ[:, n+1]
    end

    # Forward pass (with line search) to compute x
    global α = 1.0
    b = 1e-2 # Tolerance
    unew = u + α .* Δu
    xnew = simulate_dlti(unew, A, B, x1)

    while J(xnew, unew) > J(x, u) - b * α * norm(Δu)^2
        α = 0.5 * α
        unew = u + α .* Δu
        xnew = simulate_dlti(unew, A, B, x1)

        if verbose && α < α_min
            println("\tLine search failed to find a suitable step size")
            break
        end
    end

    u .= unew
    x .= xnew #added this dot so that global x is modified (https://discourse.julialang.org/t/local-and-global-variables-with-the-same-name-error-undefvarerror-x-not-defined/53951)
    verbose ? println("\tα = ", α) : nothing

    global loop = loop + 1
end
┌ Warning: Assignment to `b` in soft scope is ambiguous because a global variable by the same name exists: `b` will be treated as a new local. Disambiguate by using `local b` to suppress this warning or `global b` to assign to the existing global variable.
@ 7_trajectory_optimization.md:358
series(simulate_dlti(u, A, B, x1))
Example block output
stairs(u[1, :])
Example block output

Exercise: Neural ODEs

  • Check out this SciML note. Can we connect what we learned about Pontryagin to this work?

Direct optimal control

Tagline

"discretize, then optimize"

Ideas

  • Package the optimization variables into a trajectory
  • Set up a quadratic program defined over the trajectory
  • Observe the presence of sparsity

\[\begin{align} \min_{x_{1:N}, u_{1:{N{-}1}}} &\quad J(x_{1:N}, u_{1:{N{-}1}}) = \sum_{n=1}^{N-1} \tfrac{1}{2} x_n^T Q_n x_n + \tfrac{1}{2} u_n^T R_n u_n + \tfrac{1}{2} x_N^T Q_N x_N \\ \text{s.t.} &\quad x_{n+1} = A_n x_n + B_n u_n \\ &\quad Q_n \succeq 0,\, R_n \succ 0 \end{align}\]

m = 0.1 # Mass
A_c, B_c = double_integrator(m)
h = 0.1  # Time step
A, B = sparse.(continuous_to_discrete(A_c, B_c, h))
x1 = [1.0; 1.0]  # Initial state

N = 100
x_dim = size(A, 2)
u_dim = size(B, 2)

Q = sparse(1e-4I(x_dim))
R = sparse(1e-2I(u_dim))
QN = sparse(1e2I(x_dim))
2×2 SparseArrays.SparseMatrixCSC{Float64, Int64} with 2 stored entries:
 100.0     ⋅ 
    ⋅   100.0

Define $\begin{align} Z &= \begin{bmatrix} x_1 & x_2 & \cdots & x_N \\ u_1 & u_2 & \cdots & u_N \end{bmatrix} \\ \Rightarrow \vec{Z} &= \begin{bmatrix} x_1 \\ u_1 \\ x_2 \\ u_2 \\ \vdots \\ x_N \\ u_N \end{bmatrix} \end{align}$

and drop the first state (known) and last control (not used), $z = \vec{Z}[\text{length}(x_1){:}\text{end}{-}\text{length}(u_N)]$

z_dim = (N-2) * (x_dim + u_dim) + u_dim + x_dim
297

Write the cost function as $J = \tfrac{1}{2} z^T H z$, where $H = \begin{bmatrix} R_1 & 0 & 0 & & 0 \\ 0 & Q_2 & 0 & \cdots & 0 \\ 0 & 0 & R_2 & & 0 \\ & \vdots & & \ddots & \vdots \\ 0 & 0 & 0 & \cdots & Q_N \\ \end{bmatrix}$

# Recall our shorthand for kron
H = blockdiag(sparse(R), I(N-2) ⊗ blockdiag(sparse(Q), sparse(R)), sparse(QN))
297×297 SparseArrays.SparseMatrixCSC{Float64, Int64} with 297 stored entries:
⎡⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎤
⎢⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⎥
⎣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⎦

Write the dynamics constraint, $Cz = d$, where $C = \begin{bmatrix} B_1 & -I & 0 & 0 & & 0 \\ 0 & A_2 & B_2 & -I & \cdots & 0 \\ \vdots & \vdots & \ddots & \ddots & \ddots & 0 \\ 0 & 0 & \cdots & A_{N-1} & B_{N-1} & -I \end{bmatrix}, \qquad d = \begin{bmatrix} -A_1 x_1 \\ 0 \\ 0 \\ \vdots \\ 0 \end{bmatrix}$

C = I(N-1) ⊗ [B -I(x_dim)]
for k = 1:N-2
    C[(k * x_dim) .+ (1:x_dim), (k * (x_dim + u_dim) - x_dim) .+ (1:x_dim)] = A
end
C
198×297 SparseArrays.SparseMatrixCSC{Float64, Int64} with 690 stored entries:
⎡⠛⢦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎤
⎢⠀⠀⠈⠙⢦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠈⠙⢦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⢶⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠶⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠳⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠲⣤⡀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠳⣤⡀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠲⣤⡀⎥
⎣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⎦
# Check the structure of C
k = 6
(
    C[(k * x_dim) .+ (1:x_dim), (k * (x_dim + u_dim) - x_dim) .+ (1:(2x_dim + u_dim))]
    == [A B -I(x_dim)]
)
true
d = [-A * x1; zeros((N-2) * x_dim)];
198-element Vector{Float64}:
 -1.1
 -1.0
  0.0
  0.0
  0.0
  0.0
  0.0
  0.0
  0.0
  0.0
  ⋮
  0.0
  0.0
  0.0
  0.0
  0.0
  0.0
  0.0
  0.0
  0.0

Putting it all together, $\begin{align} \min_z &\quad \tfrac{1}{2} z^T H z \\ \text{s.t.}&\quad C z = d \end{align}$

  • Lagrangian:

\[L(z, \lambda) = \tfrac{1}{2} z^T H z + \lambda^T (C z - d)\]

  • KKT conditions:

\[\begin{align} & \nabla_z L = H z + C^T \lambda \overset{!}{=} 0 \\ & \nabla_\lambda L = Cz - d \overset{!}{=} 0 \\ \end{align}\]

  • Matrix form:

\[\Rightarrow \begin{bmatrix} H & C^T \\ C & 0 \end{bmatrix} \begin{bmatrix} z \\ \lambda \end{bmatrix} = \begin{bmatrix} 0 \\ d \end{bmatrix}\]

  • Quick check: How many iterations will this take to solve?
P = [H C'; C zeros(size(C, 1), size(C, 1))]

res_qp = P \ [zeros(z_dim); d]

# Extract the minimizer
z_minimizer = res_qp[1:z_dim]
u_minimizer = z_minimizer[1:x_dim + u_dim:end];
99-element Vector{Float64}:
 -0.25526724211741625
 -0.22151609383946136
 -0.1910990418733456
 -0.1637649977921076
 -0.13927451626825543
 -0.11740014001102642
 -0.09792657992806568
 -0.08065075505435546
 -0.06538171417559757
 -0.051940458672337986
  ⋮
 -0.00013339034369839073
 -0.0001425785228803702
 -0.00015260480385746703
 -0.0001637046927068341
 -0.00017610914426376446
 -0.00019004575590729015
 -0.00020573979739887867
 -0.0002234150642871416
 -0.00024329454086428334
fig, ax = series(simulate_dlti(u_minimizer, A, B, x1), labels=["q", "q̇"])
axislegend(ax)
fig
Example block output
stairs(u_minimizer)
Example block output
  • Quick check: What about the temporal structure?