Cruise Control

Let's see how we can use SimulationLogs for a typical controls simulation.

First, we'll start with a simple model of a car using ComponentArrays.jl, DifferentialEquations.jl, and UnPack.jl.

using ComponentArrays
using DifferentialEquations
using UnPack

# Simple car with velocity-square drag
function car!(D, x, p, t; u=0.0)
    @unpack pos, vel = x
    @unpack c, m = p

    drag = c*vel^2
    D.pos = vel
    D.vel = (-drag*sign(vel) + u)/m
end

car_params = (
    m = 1000,
    c = 5,
)

car_ic = ComponentArray(
    pos = 0.0,
    vel = 0.0,
)

Now this is a pretty boring simulation; the car is just going to sit there. Let's add a cruise control system to the car so it will track a velocity setpoint. We'll use a proportional-integral (PI) controller to follow a reference step input.

# Car with cruise control
function cruise_car!(D, vars, p, t)
    @unpack ref, kp, ki = p.control
    @unpack ∫e, car = vars

    r = ref(t)
    e = r - car.vel
    u = kp*e + ki*∫e
    
    car!(D.car, car, p.car, t; u)
    D.∫e = e
end

# Parameters
p = (
    car = car_params,
    control = (
        ref = t -> 10*(t>1),
        kp = 800,
        ki = 40,
    )
)

# Initial conditions
ic = ComponentArray(
    car = car_ic,
    ∫e = 0.0,
)

prob = ODEProblem(cruise_car!, ic, (0.0, 20.0), p)
sol = solve(prob)

Now we can bring in the Plots.jl library to look at our velocity.

using Plots

plot(sol, vars=2, title="Velocity", legend=false)

Great. There's our velocity. But what if we want to plot the velocity against the reference signal r? Or what if we want to plot the reference tracking error e, control signal u, or even the drag force drag?

Enter SimulationLogs

using SimulationLogs

Using SimulationLogs, we can tag any variable we'd like to see with the @log macro. So adding that to our simulation functions would look like:

function car!(D, x, p, t; u=0.0)
    @unpack pos, vel = x
    @unpack c, m = p

    @log drag = c*vel^2
    D.pos = vel
    D.vel = (-drag*sign(vel) + u)/m
end

function cruise_car!(D, vars, p, t)
    @unpack ref, kp, ki = p.control
    @unpack ∫e, car = vars

    @log r = ref(t)
    @log e = r - car.vel
    @log u = kp*e + ki*∫e
    
    car!(D.car, car, p.car, t; u)
    D.∫e = e
end

Now we can re-run our simulation and use the scope function to plot the outputs.

sol = solve(prob)

p1 = plot(sol, vars=2, title="Reference Tracking")
scope!(sol, :r)

p2 = scope(sol, [:u, :drag], title="Forces")

p3 = plot(sol, vars=3, title="Errors")
scope!(sol, :e)

plot(p1, p2, p3, layout=(3,1), size=(600,600))

If we want to get our logged variables (not just plot them), we can use the get_log function.

julia> logsout = get_log(sol)
SimulationLog with signals:
  drag :: Float64
  e :: Float64
  u :: Float64
  r :: Int64


julia> logsout.drag
33-element Vector{Float64}:
   0.0
   0.0
   0.0
   0.0
   ⋮
 498.74065942087725
 498.94149067684566
 499.10812257810085
 499.1675529889568