Skip to content

Commit 2f0ed3a

Browse files
committed
add PID design for multibody model
1 parent ef60a43 commit 2f0ed3a

2 files changed

Lines changed: 366 additions & 31 deletions

File tree

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
# cd(joinpath(@__DIR__, ".."))
2+
# using Pkg
3+
# Pkg.activate(".")
4+
include("planar_multibody.jl")
5+
6+
7+
using OrdinaryDiffEq
8+
using DyadControlSystems, ControlSystemsBase, ControlSystemsMTK
9+
using Plots
10+
import DyadControlSystems as JSC
11+
using LinearAlgebra
12+
connect = ModelingToolkit.connect
13+
14+
# @named plant = FlatDyadBot()
15+
# plant = complete(plant)
16+
# inputs = [plant.control_input.u]
17+
# outputs = [plant.theta_output.u]
18+
19+
20+
# P0 = named_ss(plant, inputs, outputs; op = Dict([inputs .=> 0; plant.b_trans=>0; plant.b_rot=>0; plant.x => big(0.0)]), allow_input_derivatives=true)
21+
22+
# Pt = named_ss(plant, inputs, outputs; op = Dict([inputs .=> 0; plant.b_trans=>1; plant.b_rot=>0; plant.x => big(0.0)]), allow_input_derivatives=true)
23+
24+
# Pr = named_ss(plant, inputs, outputs; op = Dict([inputs .=> 0; plant.b_trans=>0; plant.b_rot=>1; plant.x => big(0.0)]), allow_input_derivatives=true)
25+
26+
# Ptr = named_ss(plant, inputs, outputs; op = Dict([inputs .=> 0; plant.b_trans=>1; plant.b_rot=>1; plant.x => big(0.0)]), allow_input_derivatives=true)
27+
28+
# Ps = [P0, Pt, Pr, Ptr]
29+
30+
# nyquistplot(8 .* Ps)
31+
32+
33+
#=
34+
The loop closed around the angle only has a zero in the origin corresponding to a constant wheel rotation and non-zero input exactly matching the friction torque. This zero attracts the RHP pole and the RHP pole will never move into the LHP unless we move the zero away from the origin by adding integral action. If the friction is zero, the zero cancels with a pole in the origin.
35+
=#
36+
37+
@component function AngleControlledPlanarSegway(; name)
38+
pars = @parameters begin
39+
theta_ref = deg2rad(0) # Reference angle (upright)
40+
end
41+
42+
systems = @named begin
43+
plant = PlanarMultibodybot()
44+
controller = Blocks.LimPID(k=22.3659, Ti=false, Td=0.0632787, Nd=54.6269)#, u_max=15)
45+
ref = Blocks.Constant(k=theta_ref)
46+
end
47+
48+
eqs = [
49+
connect(ref.output, :r, controller.reference)
50+
connect(plant.theta_output, :y, controller.measurement)
51+
connect(controller.ctr_output, :u, plant.control_input)
52+
]
53+
54+
System(eqs, t, [], pars; systems, name)
55+
end
56+
57+
@named segway_model = AngleControlledPlanarSegway()
58+
ssys = multibody(segway_model)
59+
60+
x0 = [
61+
ssys.plant.body.phi => deg2rad(-10) # Adjust for offset
62+
ssys.plant.body.w => 0.0 # Adjust for offset
63+
]
64+
65+
prob = ODEProblem(ssys, x0, (0.0, 10.0))
66+
sol = solve(prob, Rodas5P())
67+
plot(sol, idxs=[ssys.plant.theta_output.u, ssys.plant.x_output.u, ssys.plant.tau]); hline!([0], l=(:dash, :black), primary=false)
68+
69+
import GLMakie
70+
Multibody.render(segway_model, sol, 0.0, lookat=[0,0.1,0], x=0, y=0.1, z=-0.5)[1]
71+
72+
73+
##
74+
75+
L = -get_named_looptransfer(segway_model, segway_model.y; Multibody.linsys...) |> sminreal
76+
PS = named_ss(segway_model, segway_model.u, segway_model.y; loop_openings = [], Multibody.linsys...) |> sminreal
77+
lsys = named_ss(segway_model, segway_model.u, segway_model.y; loop_openings = [segway_model.y, segway_model.u], Multibody.linsys...) |> sminreal
78+
79+
C = named_ss(segway_model, segway_model.r, segway_model.u; loop_openings = [segway_model.y], Multibody.linsys...) |> sminreal
80+
81+
82+
83+
"""
84+
zero_direction(lsys, z)
85+
86+
Compute the zero direction of the linear system `lsys` at the zero `z`. Returns the nullspace of the Rosenbrock matrix at `z`, where the first `nx` entries correspond to the state direction and the last `nu` entries correspond to the input direction. If `z` is not a zero, the nullspace will be empty.
87+
"""
88+
function zero_direction(lsys, z)
89+
(; A,B,C,D) = lsys
90+
R = [z*I-A -B; C D]
91+
nullspace(R)
92+
end
93+
94+
95+
S = get_named_sensitivity(segway_model, segway_model.y; Multibody.linsys...) |> sminreal
96+
Ms, ws = hinfnorm2(S)
97+
bodeplot(S, title="\$S(s)\$ angle controlled", plotphase=false, legend=:bottomright)
98+
hline!([Ms], l=(:dash, :black), label="\$M_S = \$$(round(Ms, digits=2))")
99+
##
100+
# PID Autotuning Analysis
101+
using DyadControlSystems
102+
import DyadControlSystems as JSC
103+
104+
@named tuning_model = AngleControlledPlanarSegway()
105+
106+
spec = JSC.PIDAutotuningAnalysisSpec(;
107+
name = :SegwayTuning,
108+
model = tuning_model,
109+
measurement = "y",
110+
control_input = "u",
111+
step_input = "u",
112+
step_output = "y",
113+
Ts = 0.01, # Sample time
114+
duration = 2.0, # Simulation duration
115+
Ms = 1.6, # Sensitivity peak constraint
116+
Mt = 1.6, # Complementary sensitivity peak constraint
117+
Mks = 100.0, # Control sensitivity constraint
118+
wl = 1e-2, # Lower frequency bound
119+
wu = 1e3, # Upper frequency bound
120+
ki_ub = 0.0, # Tune PD controller
121+
num_frequencies = 200,
122+
soft = true,
123+
# homotopy = true,
124+
# auto_resolve = true,
125+
)
126+
127+
# Run the autotuning analysis
128+
asol = JSC.run_analysis(spec)
129+
130+
plot(asol.sol)
131+
132+
# Visualize results
133+
Splot = JSC.artifacts(asol, :SensitivityFunctions)
134+
response_plot = JSC.artifacts(asol, :OptimizedResponse)
135+
nyquist_plot = JSC.artifacts(asol, :NyquistPlot)
136+
optimized_params = JSC.artifacts(asol, :OptimizedParameters)
137+
138+
display(optimized_params)
139+
140+
# ==============================================================================
141+
##
142+
# ==============================================================================
143+
144+
# Cascade control: outer velocity loop + inner angle loop
145+
@component function CascadeControlledPlanarSegway(; name)
146+
pars = @parameters begin
147+
x_ref = 0.15 # Reference velocity
148+
end
149+
150+
systems = @named begin
151+
plant = PlanarMultibodybot()
152+
# Inner loop: angle controller
153+
inner_controller = Blocks.LimPID(k=22.3659, Ti=false, Td=0.0632787, Nd=54.6269)
154+
# Outer loop: velocity controller
155+
outer_controller = Blocks.LimPID(k=0.0463128, Ti=3.34679, Td=2.23125, Nd=2.8527, wd=1, wp=1, u_max=deg2rad(25.0))
156+
neg_gain = Blocks.Gain(k=1)
157+
ref = Blocks.Step(height=x_ref, start_time=10)
158+
# Add pi offset to inner loop reference
159+
pi_offset = Blocks.Constant(k=0)
160+
add_pi = Blocks.Add(k1=1, k2=1)
161+
ref_filter = Blocks.FirstOrder(T=2)
162+
end
163+
164+
eqs = [
165+
# Outer loop: velocity reference -> angle reference
166+
connect(ref.output, ref_filter.input)
167+
connect(ref_filter.output, :r2, outer_controller.reference)
168+
connect(plant.x_output, neg_gain.input)
169+
connect(neg_gain.output, :y2, outer_controller.measurement)
170+
171+
# Add pi to outer controller output for inner loop reference
172+
connect(outer_controller.ctr_output, :u2, add_pi.input1)
173+
connect(pi_offset.output, add_pi.input2)
174+
175+
# Inner loop: angle reference -> torque
176+
connect(add_pi.output, inner_controller.reference)
177+
connect(plant.theta_output, :y, inner_controller.measurement)
178+
connect(inner_controller.ctr_output, :u, plant.control_input)
179+
]
180+
181+
System(eqs, t, [], pars; systems, name)
182+
end
183+
184+
@named cascade_model = CascadeControlledPlanarSegway()
185+
cascade_ssys = multibody(cascade_model)
186+
187+
x0 = [
188+
cascade_ssys.plant.body.phi => deg2rad(5)
189+
# cascade_ssys.plant.wheelJoint.color => [0,0,0,0.5]
190+
cascade_ssys.plant.wheelJoint.frame_a.render => true
191+
cascade_ssys.plant.wheelJoint.frame_a.length => 0.12
192+
cascade_ssys.plant.wheelJoint.frame_a.radius => 0.004
193+
]
194+
195+
cascade_prob = ODEProblem(cascade_ssys, x0, (0.0, 20.0))
196+
cascade_sol = solve(cascade_prob, Rodas5P())
197+
plot(cascade_sol, idxs=[cascade_ssys.plant.theta_output.u, cascade_ssys.plant.x_output.u, cascade_ssys.outer_controller.ctr_output.u]); hline!([0 0.15], l=(:dash, :black), primary=false, ylims=(-1, 3.5), size=(800,1600))
198+
199+
Multibody.render(cascade_model, cascade_sol, 0.0, lookat=[0,0.4,0], x=0, y=0.4, z=-1.0)[1]
200+
# Multibody.render(cascade_model, cascade_sol, lookat=[0,0.4,0], x=0, y=0.4, z=-1.0, timescale=0.5)
201+
# plot(cascade_sol, idxs=[cascade_ssys.pi_offset.output.u])
202+
203+
##
204+
205+
lsys = named_ss(cascade_model, cascade_model.u2, cascade_model.y2; loop_openings = [cascade_model.u2, cascade_model.y2], Multibody.linsys...) |> sminreal
206+
207+
208+
209+
##
210+
# PID Autotuning for outer velocity loop
211+
@named cascade_tuning_model = CascadeControlledPlanarSegway()
212+
213+
cascade_spec = JSC.PIDAutotuningAnalysisSpec(;
214+
name = :CascadeVelocityTuning,
215+
model = cascade_tuning_model,
216+
measurement = "y2",
217+
control_input = "u2",
218+
step_input = "u2",
219+
step_output = "y2",
220+
ref = 0.0,
221+
Ts = 0.01,
222+
duration = 15.0,
223+
Ms = 1.4,
224+
Mt = 1.9,
225+
Mks = 1000.0,
226+
wl = 1e-2,
227+
wu = 1e3,
228+
num_frequencies = 300,
229+
# kd_ub = 0.0, # Tune PI controller
230+
# kp_guess = 0.55,
231+
# ki_guess = 0.55,
232+
# kd_guess = 1e-3,
233+
soft = false,
234+
exact_hessian = false,
235+
scale = true,
236+
auto_resolve = true,
237+
homotopy = true,
238+
)
239+
240+
cascade_asol = JSC.run_analysis(cascade_spec)
241+
242+
plot(cascade_asol.sol)
243+
244+
cascade_Splot = JSC.artifacts(cascade_asol, :SensitivityFunctions)
245+
# cascade_response_plot = JSC.artifacts(cascade_asol, :OptimizedResponse)
246+
cascade_nyquist_plot = JSC.artifacts(cascade_asol, :NyquistPlot)
247+
cascade_optimized_params = JSC.artifacts(cascade_asol, :OptimizedParameters)
248+
249+
display(cascade_optimized_params)
250+
251+
##
252+
253+
S2 = get_named_sensitivity(cascade_tuning_model, [cascade_tuning_model.y, cascade_tuning_model.y2]; Multibody.linsys...)
254+
Ms2, ws2 = hinfnorm2(S2)
255+
sigmaplot(S2); hline!([Ms2], l=(:dash, :black), label="\$M_S = \$$(round(Ms2, digits=2))")
256+
# bodeplot(S2, plotphase=false)
257+
258+
L2 = inv(S2) - ss(I(2))
259+
bodeplot(L2)
260+
261+
## Swms = freqresp(S2, ws2)
262+
263+
Si2 = get_named_sensitivity(cascade_tuning_model, [cascade_tuning_model.u])
264+
w = exp10.(LinRange(-1, 3, 1000))
265+
Msi2, ws2 = hinfnorm2(Si2)
266+
bodeplot(Si2, w, plotphase=false); hline!([Msi2], l=(:dash, :black), label="\$M_S = \$$(round(Msi2, digits=2))")
267+
268+
Li2 = minreal(inv(Si2) - 1, 1e-12)
269+
Li2 = convert(StateSpace{Continuous, Float64}, Li2)
270+
nyquistplot(Li2)
271+
marginplot(Li2, w, adjust_phase_start=true)
272+
273+
274+
Li22 = get_named_looptransfer(cascade_tuning_model, [cascade_tuning_model.u]) |> minreal
275+
276+
dmi2 = diskmargin(Li2)
277+
plot(dmi2)
278+
279+
dmi22 = diskmargin(Li22)
280+
plot(dmi22)

0 commit comments

Comments
 (0)