Control of a sliding block

using ComponentArrays
using DifferentialEquations
using Interact: @manipulate
using Parameters: @unpack
using Plots

Problem Setup

const g = 9.80665

maybe_apply(f::Function, x, p, t) = f(x, p, t)
maybe_apply(f, x, p, t) = f

# Applies functions of form f(x,p,t) to be applied and passed in as inputs
function simulator(func; kwargs...)
    simfun(dx, x, p, t) = func(dx, x, p, t; map(f->maybe_apply(f, x, p, t), (;kwargs...))...)
    simfun(x, p, t) = func(x, p, t; map(f->maybe_apply(f, x, p, t), (;kwargs...))...)
    return simfun
end

softsign(x) = tanh(1e3x)

Component Functions

A sliding block with two different friction models

# Sliding block with viscous friction
function viscous_block!(D, vars, p, t; u=0.0)
    @unpack m, c, k = p
    @unpack v, x = vars

    D.x = v
    D.v = (-c*v + k*(u-x))/m
    return x
end

# Sliding block with coulomb friction
function coulomb_block!(D, vars, p, t; u=0.0)
    @unpack m, μ, k = p
    @unpack v, x = vars

    D.x = v
    a = -μ*g*softsign(v) + k*(u-x)/m
    D.v = abs(a)<1e-3 && abs(v)<1e-3 ? -10v : a #deadzone to help the simulation
    return x
end

PID feedback control

function PID_controller!(D, vars, p, t; err=0.0, v=0.0)
    @unpack kp, ki, kd = p
    @unpack x = vars

    D.x = ki*err
    return x + kp*err + kd*v
end

function feedback_sys!(D, components, p, t; ref=0.0)
    @unpack ctrl, plant = components

    u = p.ctrl.fun(D.ctrl, ctrl, p.ctrl.params, t; err=ref-plant.x, v=-plant.v)
    return p.plant.fun(D.plant, plant, p.plant.params, t; u=u)
end

step_input(;time=1.0, mag=1.0) = (x,p,t) -> t>time ? mag : 0
sine_input(;mag=1.0, period=10.0) = (x,p,t) -> mag*sin(t*2π/period)

# Equivalent viscous damping coefficient taken from:
# https://engineering.purdue.edu/~deadams/ME563/lecture2010.pdf
visc_equiv(μ, N, ω, mag) = 4*μ*N/(π*ω*mag)

Open-Loop Response

To see the open-loop response of the coulomb system, let's set the input to 5 and plot the results.

const tspan = (0.0, 30.0)
const m = 50.0
const μ = 0.1
const k = 50.0

p = (m=m, μ=μ, k=k)
ic = ComponentArray(v=0, x=0)

ODEProblem(simulator(coulomb_block!, u=5), ic, tspan, p) |> solve |> plot

Closed-Loop Response

For the closed-loop response, let's make an interactive GUI. Since we are using ComponentArrays, we don't have to change anything about our plant model to incorporate it in the overall system simulation.

p = (
    ctrl = (
        params = (kp=13, ki=12, kd=5),
        fun = PID_controller!,
    ),
    plant = (
        params = plant_p,
        fun = coulomb_block!,
    ),
)

ic = ComponentArray(ctrl=(;x=0), plant=plant_ic)

sol = ODEProblem(simulator(feedback_sys!, ref=10), ic, tspan, p) |> solve
plot(sol, vars=3)
## Interactive GUI for switching out plant models and varying PID gains
@manipulate for kp in 0:0.01:15,
                ki in 0:0.01:15, 
                kd in 0:0.01:15,
                damping in Dict(
                    "Coulomb" => coulomb_block!,
                    "Viscous" => viscous_block!,
                ),
                reference in Dict(
                    "Sine" => sine_input,
                    "Step" => step_input,
                ),
                magnitude in 0:0.01:10, # pop-pop!
                period in 1:0.01:30,
                plot_v in false
    
    # Inputs
    tspan = (0.0, 30.0)

    ctrl_fun = PID_controller!
    # plant_fun = coulomb_block!
    
    ref = if reference==sine_input
        reference(period=period, mag=magnitude)
        else
        reference(mag=magnitude)
    end
    
    m = 50.0
    μ = 0.1
    ω = 2π/period
    c = 4*μ*m*g/(π*ω*magnitude) # Viscous equivalent damping
    k = 50.0

    plant_p = (m=m, μ=μ, c=c, k=k) # We'll just put everything for both models in here
    ctrl_p = (kp=kp, ki=ki, kd=kd)

    plant_ic = (v=0, x=0)
    ctrl_ic = (;x=0)



    # Set up and solve
    sys_p = (
        ctrl = (
            params = ctrl_p,
            fun = ctrl_fun,
        ),
        plant = (
            params = plant_p,
            fun = damping,
        ),
    )
    sys_ic = ComponentArray(ctrl=ctrl_ic, plant=plant_ic)
    sys_fun = ODEFunction(simulator(feedback_sys!, ref=ref), syms=[:u, :v, :x])
    sys_prob = ODEProblem(sys_fun, sys_ic, tspan, sys_p)

    sol = solve(sys_prob, Tsit5())


    # Plot
    t = tspan[1]:0.1:tspan[2]
    lims = magnitude*[-1, 1]
    plotvars = plot_v ? [3, 2] : [3]
    strip = plot(t, ref.(0, 0, t), ylim=1.2lims, label="r(t)")
    plot!(strip, sol, vars=plotvars)
    phase = plot(ref.(0, 0, t), map(x->x.plant.x, sol(t).u),
        xlim=lims,
        ylim=1.2lims,
        legend=false,
        xlabel="r(t)",
        ylabel="x(t)",
    )
    plot(strip, phase, layout=(2, 1), size=(700, 800))

end