|
| 1 | +# Quadrotor with cable-suspended load |
| 2 | + |
| 3 | + |
| 4 | +```@example QUAD |
| 5 | +using Multibody |
| 6 | +using ModelingToolkit |
| 7 | +using ModelingToolkitStandardLibrary.Blocks |
| 8 | +using LinearAlgebra |
| 9 | +using Plots |
| 10 | +using JuliaSimCompiler |
| 11 | +using OrdinaryDiffEq |
| 12 | +using Test |
| 13 | +t = Multibody.t |
| 14 | +D = Differential(t) |
| 15 | +
|
| 16 | +world = Multibody.world |
| 17 | +
|
| 18 | +num_arms = 4 # Number of arms of the rotor craft. |
| 19 | +angle_between_arms = 2*pi/num_arms # Angle between the arms, assuming they are evenly spaced. |
| 20 | +arm_length = 0.2 # Length of each arm. |
| 21 | +arm_outer_diameter = 0.03 |
| 22 | +arm_inner_diameter = 0.02 |
| 23 | +arm_density = 800 # Density of the arm [kg/m³]. |
| 24 | +body_mass = 0.2 # Mass of the body. |
| 25 | +load_mass = 0.1 # Mass of the load. |
| 26 | +cable_length = 1 # Length of the cable. |
| 27 | +cable_mass = 0.1 # Mass of the cable. |
| 28 | +cable_diameter = 0.01 # Diameter of the cable. |
| 29 | +number_of_links = 5 # Number of links in the cable. |
| 30 | +kalt = 1 |
| 31 | +Tialt = 3 |
| 32 | +Tdalt = 3 |
| 33 | +
|
| 34 | +kroll = 0.02 |
| 35 | +Tiroll = 100 |
| 36 | +Tdroll = 1 |
| 37 | +
|
| 38 | +kpitch = 0.02 |
| 39 | +Tipitch = 100 |
| 40 | +Tdpitch = 1 |
| 41 | +
|
| 42 | +@mtkmodel Thruster begin |
| 43 | + @components begin |
| 44 | + frame_b = Frame() |
| 45 | + thrust3d = WorldForce(resolve_frame = :frame_b, scale=0.1, radius=0.02) |
| 46 | + thrust = RealInput() |
| 47 | + end |
| 48 | + @variables begin |
| 49 | + u(t), [state_priority=1000] |
| 50 | + end |
| 51 | + @equations begin |
| 52 | + thrust3d.force.u[1] ~ 0 |
| 53 | + thrust3d.force.u[2] ~ thrust.u |
| 54 | + thrust3d.force.u[3] ~ 0 |
| 55 | + thrust.u ~ u |
| 56 | + connect(frame_b, thrust3d.frame_b) |
| 57 | + end |
| 58 | +end |
| 59 | +
|
| 60 | +rou(x) = round(x, digits=3) |
| 61 | +
|
| 62 | +function RotorCraft(; cl = true, addload=true) |
| 63 | + arms = [ |
| 64 | + BodyCylinder( |
| 65 | + r = rou.([arm_length*cos(angle_between_arms*(i-1)), 0, arm_length*sin(angle_between_arms*(i-1))]), |
| 66 | + diameter = arm_outer_diameter, |
| 67 | + inner_diameter = arm_inner_diameter, |
| 68 | + density = arm_density, |
| 69 | + name=Symbol("arm$i"), |
| 70 | + ) for i = 1:num_arms |
| 71 | + ] |
| 72 | +
|
| 73 | + thrusters = [Thruster(name = Symbol("thruster$i")) for i = 1:num_arms] |
| 74 | +
|
| 75 | + @parameters Galt[1:4] = ones(4) |
| 76 | + @parameters Groll[1:4] = [1,0,-1,0] |
| 77 | + @parameters Gpitch[1:4] = [0,1,0,-1] |
| 78 | +
|
| 79 | + @named Calt = PID(; k=kalt, Ti=Tialt, Td=Tdalt) |
| 80 | + @named Croll = PID(; k=kroll, Ti=Tiroll, Td=Tdroll) |
| 81 | + @named Cpitch = PID(; k=kpitch, Ti=Tipitch, Td=Tdpitch) |
| 82 | +
|
| 83 | +
|
| 84 | + @named body = Body(m = body_mass, state_priority = 0, I_11=0.01, I_22=0.01, I_33=0.01, air_resistance=1) |
| 85 | + @named load = Body(m = load_mass, air_resistance=1) |
| 86 | + @named freemotion = FreeMotion(state=true, isroot=true, quat=false, state_priority=1000, neg_w=false) |
| 87 | +
|
| 88 | + @named cable = Rope( |
| 89 | + l = cable_length, |
| 90 | + m = cable_mass, |
| 91 | + n = number_of_links, |
| 92 | + c = 0, |
| 93 | + d = 0, |
| 94 | + air_resistance = 0.5, |
| 95 | + d_joint = 0.1, |
| 96 | + radius = cable_diameter/2, |
| 97 | + color = [0.5, 0.4, 0.4, 1], |
| 98 | + dir = [0.0, -1, 0] |
| 99 | + ) |
| 100 | +
|
| 101 | + connections = [ |
| 102 | + connect(world.frame_b, freemotion.frame_a) |
| 103 | + connect(freemotion.frame_b, body.frame_a) |
| 104 | + [connect(body.frame_a, arms[i].frame_a) for i = 1:num_arms] |
| 105 | + [connect(arms[i].frame_b, thrusters[i].frame_b) for i = 1:num_arms] |
| 106 | + ] |
| 107 | + systems = [world; arms; body; thrusters; freemotion] |
| 108 | + if addload |
| 109 | + push!(systems, load) |
| 110 | + push!(systems, cable) |
| 111 | + |
| 112 | + push!(connections, connect(body.frame_a, cable.frame_a)) |
| 113 | + push!(connections, connect(cable.frame_b, load.frame_a)) |
| 114 | + end |
| 115 | + if cl |
| 116 | +
|
| 117 | + uc = Galt*Calt.ctr_output.u + Groll*Croll.ctr_output.u + Gpitch*Cpitch.ctr_output.u |
| 118 | + uc = collect(uc) |
| 119 | + append!(connections, [thrusters[i].u ~ uc[i] for i = 1:num_arms]) |
| 120 | +
|
| 121 | + append!(connections, [ |
| 122 | + Calt.err_input.u ~ -body.r_0[2] |
| 123 | + Croll.err_input.u ~ freemotion.phi[3] |
| 124 | + Cpitch.err_input.u ~ -freemotion.phi[1] |
| 125 | + ]) |
| 126 | + append!(systems, [Calt; Croll; Cpitch]) |
| 127 | +
|
| 128 | + #= |
| 129 | + # append!(connections, [thrusters[i].thrust.u ~ feedback_gain.output.u[i] for i = 1:num_arms]) |
| 130 | + # append!(connections, [feedback_gain.input.u[i] ~ arms[i].frame_b.r_0[2] for i = 1:num_arms ]) # Connect positions to controller |
| 131 | + # append!(connections, [feedback_gain.input.u[i+num_arms] ~ D(arms[i].frame_b.r_0[2]) for i = 1:num_arms]) # Connect velocities to controller |
| 132 | + # append!(connections, [feedback_gain.input.u[i+2num_arms] ~ Ie[i] for i = 1:num_arms]) # |
| 133 | + # append!(connections, [feedback_gain.input.u[i] ~ freemotion.phi[[1,3][i]] for i = 1:2 ]) # Connect positions to controller |
| 134 | + # append!(connections, [feedback_gain.input.u[i+2] ~ freemotion.phid[[1,3][i]] for i = 1:2]) # Connect velocities to controller |
| 135 | + # push!(systems, feedback_gain) |
| 136 | + =# |
| 137 | + end |
| 138 | + @named model = ODESystem(connections, t; systems) |
| 139 | + complete(model) |
| 140 | +end |
| 141 | +model = RotorCraft(cl=true, addload=true) |
| 142 | +ssys = structural_simplify(IRSystem(model)) |
| 143 | +
|
| 144 | +op = [ |
| 145 | + model.body.v_0[1] => 0; |
| 146 | + # collect(model.freemotion.phi) .=> 0.1; |
| 147 | + collect(model.cable.joint_2.phi) .=> 0.03; |
| 148 | + model.world.g => 2; |
| 149 | + # model.body.frame_a.render => true |
| 150 | + # model.body.frame_a.radius => 0.01 |
| 151 | + # model.body.frame_a.length => 0.1 |
| 152 | +] |
| 153 | +
|
| 154 | +prob = ODEProblem(ssys, op, (0, 20)) |
| 155 | +sol = solve(prob, FBDF(autodiff=false), reltol=1e-8, abstol=1e-8) |
| 156 | +@test SciMLBase.successful_retcode(sol) |
| 157 | +
|
| 158 | +plot(sol, idxs=[model.arm1.frame_b.r_0[2], model.arm2.frame_b.r_0[2], model.arm3.frame_b.r_0[2], model.arm4.frame_b.r_0[2]], layout=4, framestyle=:zerolines) |
| 159 | +``` |
| 160 | + |
| 161 | +```@example QUAD |
| 162 | +import GLMakie |
| 163 | +render(model, sol, 0:0.1:sol.t[end], x=-3, z=-3, y=-1, lookat=[0,-1,0], show_axis=false, filename="quadrotor.gif", framerate=25) |
| 164 | +nothing # hide |
| 165 | +``` |
| 166 | + |
| 167 | + |
| 168 | + |
| 169 | + |
0 commit comments