|
| 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