Skip to content
LimPIDTest.md

LimPIDTest

Test bench for a limited PID controller connected to a plant model with step input.

This test component connects a limited PID controller to a plant model and applies a step input as setpoint and a constant feedforward signal. The PID controller includes derivative, integral, and proportional actions with anti-windup and output limitations. The system response can be observed through the plant output and controller signals.

Usage

LimPIDTest()

Behavior

signal.y(t)=pid.u_s(t)plant.y(t)=pid.u_m(t)pid.y(t)=plant.u(t)pid.u_ff(t)=signal_ff.y(t)pid.u_s(t)=pid.add_p.u1(t)pid.u_s(t)=pid.add_i.u1(t)pid.u_s(t)=pid.add_d.u1(t)pid.u_m(t)=pid.add_p.u2(t)pid.u_m(t)=pid.add_i.u2(t)pid.u_m(t)=pid.add_d.u2(t)pid.u_ff(t)=pid.add_ff.u2(t)pid.y(t)=pid.limiter.y(t)pid.add_p.y(t)=pid.proportional.u(t)pid.add_d.y(t)=pid.derivative.u(t)pid.add_i.y(t)=pid.integrator.u(t)pid.proportional.y(t)=pid.add_pid.u1(t)pid.derivative.y(t)=pid.add_pid.u2(t)pid.integrator.y(t)=pid.add_pid.u3(t)pid.add_pid.y(t)=pid.gain_pid.u(t)pid.gain_pid.y(t)=pid.add_ff.u1(t)pid.add_ff.y(t)=pid.add_sat.u2(t)pid.add_ff.y(t)=pid.limiter.u(t)pid.limiter.y(t)=pid.add_sat.u1(t)pid.add_sat.y(t)=pid.gain_track.u(t)pid.gain_track.y(t)=pid.add_i.u3(t)pid.add_p.y(t)=pid.add_p.k1pid.add_p.u1(t)+pid.add_p.k2pid.add_p.u2(t)pid.add_d.y(t)=pid.add_d.k1pid.add_d.u1(t)+pid.add_d.k2pid.add_d.u2(t)pid.add_i.y(t)=pid.add_i.k1pid.add_i.u1(t)+pid.add_i.k2pid.add_i.u2(t)+pid.add_i.k3pid.add_i.u3(t)pid.proportional.y(t)=pid.proportional.kpid.proportional.u(t)dpid.derivative.x(t)dt=pid.derivative.u(t)pid.derivative.x(t)pid.derivative.Tpid.derivative.y(t)=pid.derivative.k(pid.derivative.u(t)pid.derivative.x(t))pid.derivative.Tdpid.integrator.x(t)dt=pid.integrator.kpid.integrator.u(t)pid.integrator.y(t)=pid.integrator.x(t)pid.add_pid.y(t)=pid.add_pid.k1pid.add_pid.u1(t)+pid.add_pid.k2pid.add_pid.u2(t)+pid.add_pid.k3pid.add_pid.u3(t)pid.gain_pid.y(t)=pid.gain_pid.kpid.gain_pid.u(t)pid.add_ff.y(t)=pid.add_ff.k1pid.add_ff.u1(t)+pid.add_ff.k2pid.add_ff.u2(t)pid.limiter.y(t)=clamp(pid.limiter.u(t),pid.limiter.y_min,pid.limiter.y_max)pid.add_sat.y(t)=pid.add_sat.k1pid.add_sat.u1(t)+pid.add_sat.k2pid.add_sat.u2(t)pid.gain_track.y(t)=pid.gain_track.kpid.gain_track.u(t)dplant.x1(t)dt=plant.x2(t)dplant.x2(t)dt=plant.u(t)plant.x1(t)0.5plant.x2(t)plant.y(t)=0.5plant.x1(t)+plant.x2(t)signal.y(t)=ifelse(tsignal.start_time,signal.height+signal.offset,signal.offset)signal_ff.y(t)=signal_ff.k

Source

dyad
# Test bench for a limited PID controller connected to a plant model with step input.
#
# This test component connects a limited PID controller to a plant model and applies a step input as
# setpoint and a constant feedforward signal. The PID controller includes derivative, integral, and
# proportional actions with anti-windup and output limitations. The system response can be observed
# through the plant output and controller signals.
test component LimPIDTest
  # Limited PID controller with configurable parameters
  pid = LimPID(Td=0.1, Ti=0.5, y_max=1, y_min=-1, wp=1, wd=0, Nd=10, Ni=0.9, k_ff=1)
  # Plant model to be controlled
  plant = Plant()
  # Step input signal used as setpoint for the controller
  signal = Step(height=1)
  # Constant signal for feedforward control
  signal_ff = Constant(k=1)
relations
  # Initial condition for the first state of the plant
  initial plant.x1 = 0
  # Initial condition for the plant output
  initial plant.y = 0
  # Connect step signal to controller setpoint input
  connect(signal.y, pid.u_s)
  # Connect plant output to controller measurement input
  connect(plant.y, pid.u_m)
  # Connect controller output to plant input
  connect(pid.y, plant.u)
  # Connect feedforward signal to controller feedforward input
  connect(pid.u_ff, signal_ff.y)
metadata {
  "Dyad": {"tests": {"case1": {"stop": 10, "expect": {"signals": ["plant.y", "pid.y"]}}}}
}
end
Flattened Source
dyad
# Test bench for a limited PID controller connected to a plant model with step input.
#
# This test component connects a limited PID controller to a plant model and applies a step input as
# setpoint and a constant feedforward signal. The PID controller includes derivative, integral, and
# proportional actions with anti-windup and output limitations. The system response can be observed
# through the plant output and controller signals.
test component LimPIDTest
  # Limited PID controller with configurable parameters
  pid = LimPID(Td=0.1, Ti=0.5, y_max=1, y_min=-1, wp=1, wd=0, Nd=10, Ni=0.9, k_ff=1)
  # Plant model to be controlled
  plant = Plant()
  # Step input signal used as setpoint for the controller
  signal = Step(height=1)
  # Constant signal for feedforward control
  signal_ff = Constant(k=1)
relations
  # Initial condition for the first state of the plant
  initial plant.x1 = 0
  # Initial condition for the plant output
  initial plant.y = 0
  # Connect step signal to controller setpoint input
  connect(signal.y, pid.u_s)
  # Connect plant output to controller measurement input
  connect(plant.y, pid.u_m)
  # Connect controller output to plant input
  connect(pid.y, plant.u)
  # Connect feedforward signal to controller feedforward input
  connect(pid.u_ff, signal_ff.y)
metadata {
  "Dyad": {"tests": {"case1": {"stop": 10, "expect": {"signals": ["plant.y", "pid.y"]}}}}
}
end


Test Cases

This is setup code, that must be run before each test case.

julia
using BlockComponents
using ModelingToolkit, OrdinaryDiffEqDefault
using Plots
using CSV, DataFrames

snapshotsdir = joinpath(dirname(dirname(pathof(BlockComponents))), "test", "snapshots")
"/home/actions-runner-10/.julia/packages/BlockComponents/77kIK/test/snapshots"

Test Case case1

julia
@mtkbuild model_case1 = LimPIDTest()
u0_case1 = []
prob_case1 = ODEProblem(model_case1, u0_case1, (0, 10))
sol_case1 = solve(prob_case1)
retcode: Success
Interpolation: 3rd order Hermite
t: 86-element Vector{Float64}:
  0.0
  9.999999999999999e-5
  0.0007898409479729185
  0.002382770193108327
  0.004751068667448694
  0.007780693817692643
  0.011692321861095097
  0.016475879752552797
  0.022281100929348874
  0.02917895918228197

  7.986777510405082
  8.312713121814559
  8.626533965732229
  8.928956241215829
  9.219739535790502
  9.496153756543485
  9.752464040930143
  9.973384108613464
 10.0
u: 86-element Vector{Vector{Float64}}:
 [0.0, 0.0, 0.0, 0.0]
 [0.009949667913340626, -0.002233009890180421, 4.999916663541713e-9, 9.999749987500361e-5]
 [0.07591499122717368, -0.017030301839583182, 3.1188328761528195e-7, 0.0007896849242136763]
 [0.21175301108916228, -0.04745435207235992, 2.837668523025337e-6, 0.0023813491047853146]
 [0.3772121326812567, -0.08439654308415043, 1.1277373817524526e-5, 0.004745412117132361]
 [0.538334468395275, -0.12017556995912926, 3.023023074010697e-5, 0.007765500272696214]
 [0.6845961157152598, -0.15234486524216392, 6.822140763486207e-5, 0.011657945137695152]
 [0.79908486936738, -0.1770725491902926, 0.0001353523084694002, 0.016407459732111458]
 [0.8789089530548703, -0.19367283370619195, 0.0002472942845341522, 0.02215561538985055]
 [0.9262324028042066, -0.2026344305981294, 0.0004236130552919612, 0.02896302733719781]

 [0.40345843393851594, -0.03517337060033048, 0.9543512921308936, 0.11983440364960231]
 [0.39437456093332796, -0.03618809982986992, 0.9919127312839331, 0.10976096812929569]
 [0.39656580987701995, -0.03828775365829082, 1.0235720025599007, 0.09142167969068128]
 [0.40720353296961087, -0.04102627684177765, 1.0477903357597615, 0.06843032050935582]
 [0.42339981309966734, -0.04401377817686572, 1.0641340884251962, 0.043896277488906774]
 [0.4423400062304767, -0.04691818164690934, 1.0730127807802918, 0.02042560157178203]
 [0.4614158655975168, -0.04947811513537006, 1.0756108980224117, 2.2654674292469307e-5]
 [0.47797522574006596, -0.05146993176939205, 1.0738644860994067, -0.015647611889539854]
 [0.47993961791637146, -0.05169282884718191, 1.0734248108266613, -0.017387945008224753]
julia
df_case1 = DataFrame(:t => sol_case1[:t], :actual => sol_case1[model_case1.plant.y])
dfr_case1 = try CSV.read(joinpath(snapshotsdir, "LimPIDTest_case1_sig0.ref"), DataFrame); catch e; nothing; end
plt = plot(sol_case1, idxs=[model_case1.plant.y], width=2, label="Actual value of plant.y")
if !isnothing(dfr_case1)
  scatter!(plt, dfr_case1.t, dfr_case1.expected, mc=:red, ms=3, label="Expected value of plant.y")
end

plt

julia
df_case1 = DataFrame(:t => sol_case1[:t], :actual => sol_case1[model_case1.pid.y])
dfr_case1 = try CSV.read(joinpath(snapshotsdir, "LimPIDTest_case1_sig1.ref"), DataFrame); catch e; nothing; end
plt = plot(sol_case1, idxs=[model_case1.pid.y], width=2, label="Actual value of pid.y")
if !isnothing(dfr_case1)
  scatter!(plt, dfr_case1.t, dfr_case1.expected, mc=:red, ms=3, label="Expected value of pid.y")
end

plt