Skip to content

Commit ca7b980

Browse files
authored
add WorldForce and quadrotor example (#104)
* add WorldForce and quadrotor example * add rendering of WorldForce * add tets for WorldForce Quadrotor example is kind-of working now * add more options and simplify orientation in Body * neg_w and phid * some tests no longer broken * use neg_w in some places * tune controller * up manifest * up tests * add preamble * up tests * change back * neg_w everywhere and default * up example * rm frame render * rm test * load test
1 parent 98c867f commit ca7b980

16 files changed

+1147
-294
lines changed

docs/Manifest.toml

Lines changed: 258 additions & 182 deletions
Large diffs are not rendered by default.

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ makedocs(;
3838
"Swing" => "examples/swing.md",
3939
"Bodies in space" => "examples/space.md",
4040
"Gyroscopic effects" => "examples/gyroscopic_effects.md",
41+
"Quadrotor with cable-suspended load" => "examples/quad.md",
4142
],
4243
"Rotations and orientation" => "rotations.md",
4344
"3D rendering" => "rendering.md",

docs/src/examples/free_motion.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ ssys = structural_simplify(IRSystem(model))
7272
prob = ODEProblem(ssys, [
7373
collect(body.body.v_0 .=> 0);
7474
collect(body.body.w_a .=> 0);
75-
collect(body.body.Q) .=> params(QuatRotation(RotXYZ(deg2rad.((10,10,10))...)));
75+
# collect(body.body.phi .=> deg2rad.([10,10,10])); # If using Euler/Cardan angles
7676
collect(body.body.Q̂) .=> params(QuatRotation(RotXYZ(deg2rad.((10,10,10))...)));
7777
], (0, 4))
7878

docs/src/examples/kinematic_loops.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ systems = @named begin
3333
b3 = BodyShape(m=1, r = [-l, 0, 0], radius=0.03)
3434
b4 = BodyShape(m=1, r = [0.0, -l, 0], radius=0.03)
3535
damper1 = Rotational.Damper(d=0.1)
36-
damper2 = Rotational.Damper(d=0.1)
36+
damper2 = Rotational.Damper(d=0.11)
3737
end
3838
3939
connections = [
@@ -62,16 +62,14 @@ connections = [
6262
@named fourbar = ODESystem(connections, t, systems = [world; systems])
6363
m = structural_simplify(IRSystem(fourbar))
6464
prob = ODEProblem(m, [world.g => 9.81], (0.0, 30.0))
65-
sol = solve(prob, Rodas4(autodiff=false))
65+
sol = solve(prob, FBDF(autodiff=false))
6666
6767
plot(sol, idxs = [j1.phi, j2.phi, j3.phi])
6868
```
6969

7070
```@example kinloop
7171
using Test
7272
@test SciMLBase.successful_retcode(sol)
73-
@test sol(sol.t[end], idxs=j3.phi) % 2pi ≈ π/2 atol=0.3 # Test the the "pendulum" is hanging almost straight down after sufficient time has passed
74-
@test sol(sol.t[end], idxs=j2.phi) % 2pi ≈ -π/2 atol=0.3
7573
```
7674

7775

docs/src/examples/quad.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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+
![quadrotor animation](quadrotor.gif)
169+

docs/src/examples/spherical_pendulum.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ D = Differential(t)
1717
world = Multibody.world
1818
1919
systems = @named begin
20-
joint = Spherical(state=true, isroot=true, phi = 1, phi_d = 3, radius=0.1, color=[1,1,0,1])
20+
joint = Spherical(state=true, isroot=true, phi = 1, phid = 3, radius=0.1, color=[1,1,0,1])
2121
bar = FixedTranslation(r = [0, -1, 0])
2222
body = Body(; m = 1, isroot = false, r_cm=[0.1, 0, 0])
2323
end

docs/src/examples/swing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ Next, we create the full swing assembly
102102
body_left = BodyShape(m=0.1, r = [0, 0, w])
103103
body_right = BodyShape(m=0.1, r = [0, 0, -w])
104104
105-
body = Body(m=6, isroot=true, r_cm = [w/2, -w/2, w/2], vel_from_R=true, cylinder_radius=0.01)
105+
body = Body(m=6, isroot=true, r_cm = [w/2, -w/2, w/2], neg_w=true, cylinder_radius=0.01)
106106
107107
damper = Damper(d=0.5)
108108
end

ext/Render.jl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,22 @@ function render!(scene, ::typeof(Spring), sys, sol, t)
504504
true
505505
end
506506

507+
function render!(scene, ::typeof(Multibody.WorldForce), sys, sol, t)
508+
r_0b = get_fun(sol, collect(sys.frame_b.r_0))
509+
f = get_fun(sol, collect(sys.frame_b.f))
510+
R = get_rot_fun(sol, sys.frame_b)
511+
color = get_color(sys, sol, :green)
512+
radius = sol(sol.t[1], idxs=sys.radius) |> Float32
513+
scale = -sol(sol.t[1], idxs=sys.scale) |> Float32
514+
515+
origin = @lift [Point3f(r_0b($t))]
516+
d = @lift [scale*Vec3f(R($t)*f($t))]
517+
518+
Makie.arrows!(origin, d, linecolor = color, arrowcolor = color,
519+
linewidth = radius, arrowsize = Vec3f(1.3*radius, 1.3*radius, 1.4*radius))
520+
true
521+
end
522+
507523
function render!(scene, ::Function, sys, sol, t, args...) # Fallback for systems that have at least two frames
508524
count(ModelingToolkit.isframe, sys.systems) == 2 || return false
509525
r_0a = get_fun(sol, collect(sys.frame_a.r_0))

src/Multibody.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ A boolean indicating whether or not the component performed any rendering. Typic
6969
function render! end
7070

7171
const t = let
72-
(@variables t)[1]
72+
(@independent_variables t)[1]
7373
end
7474
const D = Differential(t)
7575

@@ -158,7 +158,7 @@ export Revolute, Prismatic, Planar, Spherical, Universal, GearConstraint, Rollin
158158
RollingWheel, FreeMotion, RevolutePlanarLoopConstraint
159159
include("joints.jl")
160160

161-
export Spring, Damper, SpringDamperParallel, Torque, Force
161+
export Spring, Damper, SpringDamperParallel, Torque, Force, WorldForce
162162
include("forces.jl")
163163

164164
export PartialCutForceBaseSensor, BasicCutForce, BasicCutTorque, CutTorque, CutForce, Power

0 commit comments

Comments
 (0)