Skip to content

Commit 58bc962

Browse files
authored
switch quaternion representation and add tests (#81)
* switch quaternion representation and add tests * move testset * docs improvement and clean up
1 parent 3e615f4 commit 58bc962

File tree

9 files changed

+421
-337
lines changed

9 files changed

+421
-337
lines changed

docs/make.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ makedocs(;
1010
authors = "JuliaHub Inc.",
1111
# strict = [:example_block, :setup_block, :eval_block],
1212
sitename = "Multibody.jl",
13-
warnonly = [:missing_docs, :cross_references, :example_block, :docs_block],
13+
warnonly = [:missing_docs, :cross_references, :docs_block],
1414
pagesonly = true,
1515
format = Documenter.HTML(;
1616
prettyurls = get(ENV, "CI", nothing) == "true",

docs/src/examples/free_motion.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,11 @@ nothing # hide
4949
```
5050

5151

52-
If we instead model a body suspended in springs without the presence of any joint, we need to give the body state variables. We do this by saying `isroot = true` when we create the body, we also use quaternions to represent angles using `quat = true`.
52+
## Body suspended in springs
53+
If we instead model a body suspended in springs without the presence of any joint, we need to _give the body state variables_. We do this by saying `isroot = true` when we create the body, we also use quaternions to represent angular state using `quat = true`.
5354

5455
```@example FREE_MOTION
56+
using Multibody.Rotations: QuatRotation, RotXYZ
5557
@named begin
5658
body = BodyShape(m = 1, I_11 = 1, I_22 = 1, I_33 = 1, r = [0.4, 0, 0],
5759
r_0 = [0.2, -0.5, 0.1], isroot = true, quat=true)
@@ -78,14 +80,16 @@ ssys = structural_simplify(IRSystem(model))
7880
prob = ODEProblem(ssys, [
7981
collect(body.body.v_0 .=> 0);
8082
collect(body.body.w_a .=> 0);
81-
collect(body.body.Q .=> Multibody.to_q([0.0940609, 0.0789265, 0.0940609, 0.987965]));
82-
collect(body.body.Q̂ .=> Multibody.to_q([0.0940609, 0.0789265, 0.0940609, 0.987965]));
83+
collect(body.body.Q) .=> vec(QuatRotation(RotXYZ(deg2rad.((10,10,10))...)));
84+
collect(body.body.Q̂) .=> vec(QuatRotation(RotXYZ(deg2rad.((10,10,10))...)));
8385
], (0, 4))
8486
8587
sol = solve(prob, Rodas5P())
8688
@assert SciMLBase.successful_retcode(sol)
8789
8890
plot(sol, idxs = [body.r_0...])
91+
# plot(sol, idxs = [body.body.Q...])
92+
8993
```
9094

9195
```@example FREE_MOTION

docs/src/examples/space.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ W(;kwargs...) = Multibody.world
3535
I_33=0.1,
3636
r_0=[0,0.6,0],
3737
isroot=true,
38+
# quat=true, # Activate to use quaternions as state instead of Euler angles
3839
v_0=[1,0,0])
3940
body2 = Body(
4041
m=1,
@@ -43,6 +44,7 @@ W(;kwargs...) = Multibody.world
4344
I_33=0.1,
4445
r_0=[0.6,0.6,0],
4546
isroot=true,
47+
# quat=true, # Activate to use quaternions as state instead of Euler angles
4648
v_0=[0.6,0,0])
4749
end
4850
end

docs/src/examples/three_springs.md

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ world = Multibody.world
2020
2121
systems = @named begin
2222
body1 = Body(m = 0.8, I_11 = 0.1, I_22 = 0.1, I_33 = 0.1, r_0 = [0.5, -0.3, 0],
23-
r_cm = [0, 0, 0], isroot=false, quat=false)
23+
r_cm = [0, -0.2, 0], isroot=true, quat=true)
2424
bar1 = FixedTranslation(r = [0.3, 0, 0])
2525
bar2 = FixedTranslation(r = [0, 0, 0.3])
26-
spring1 = Multibody.Spring(c = 20, m = 0, s_unstretched = 0.1,
26+
spring1 = Multibody.Spring(c = 20, m = 0, s_unstretched = 0.1, fixed_rotation_at_frame_b=true,
2727
r_rel_0 = [-0.2, -0.2, 0.2])
28-
spring2 = Multibody.Spring(c = 40, m = 0, s_unstretched = 0.1, fixed_rotation_at_frame_b=true, fixed_rotation_at_frame_a=true)
29-
spring3 = Multibody.Spring(c = 20, m = 0, s_unstretched = 0.1)
28+
spring2 = Multibody.Spring(c = 40, m = 0, s_unstretched = 0.1)
29+
spring3 = Multibody.Spring(c = 20, m = 0, s_unstretched = 0.1, fixed_rotation_at_frame_a=false)
3030
end
3131
eqs = [connect(world.frame_b, bar1.frame_a)
3232
connect(world.frame_b, bar2.frame_a)
@@ -40,11 +40,29 @@ eqs = [connect(world.frame_b, bar1.frame_a)
4040
ssys = structural_simplify(IRSystem(model))
4141
prob = ODEProblem(ssys, [], (0, 10))
4242
43-
sol = solve(prob, FBDF(), u0=prob.u0 .+ 1e-9*randn(length(prob.u0)))
43+
sol = solve(prob, FBDF(), u0=prob.u0 .+ 1e-12*randn(length(prob.u0)))
4444
@assert SciMLBase.successful_retcode(sol)
4545
4646
Plots.plot(sol, idxs = [body1.r_0...])
47-
# Plots.plot(sol, idxs = ori(body1.frame_a).R[1])
47+
```
48+
49+
```@setup
50+
# States selected by Dymola
51+
# Statically selected continuous time states
52+
# body1.frame_a.r_0[1]
53+
# body1.frame_a.r_0[2]
54+
# body1.frame_a.r_0[3]
55+
# body1.v_0[1]
56+
# body1.v_0[2]
57+
# body1.v_0[3]
58+
# body1.w_a[1]
59+
# body1.w_a[2]
60+
# body1.w_a[3]
61+
62+
# Dynamically selected continuous time states
63+
# There is one set of dynamic state selection.
64+
# There are 3 states to be selected from:
65+
# body1.Q[]
4866
```
4967

5068
## 3D animation

src/components.jl

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ Representing a body with 3 translational and 3 rotational degrees-of-freedom.
230230
I_31 = 0,
231231
I_32 = 0,
232232
isroot = false,
233+
state = false,
234+
vel_from_R = false,
233235
phi0 = zeros(3),
234236
phid0 = zeros(3),
235237
r_0 = 0,
@@ -283,13 +285,18 @@ Representing a body with 3 translational and 3 rotational degrees-of-freedom.
283285

284286
# DRa = D(Ra)
285287

288+
if state
289+
# @warn "Make the body have state variables by using isroot=true rather than state=true"
290+
isroot = true
291+
end
292+
286293
dvs = [r_0;v_0;a_0;g_0;w_a;z_a;]
287294
eqs = if isroot # isRoot
288295

289296
if quat
290297
@named frame_a = Frame(varw = false)
291298
Ra = ori(frame_a, false)
292-
nonunit_quaternion_equations(Ra, w_a);
299+
qeeqs = nonunit_quaternion_equations(Ra, w_a)
293300
else
294301
@named frame_a = Frame(varw = true)
295302
Ra = ori(frame_a, true)
@@ -303,8 +310,11 @@ Representing a body with 3 translational and 3 rotational degrees-of-freedom.
303310
phidd .~ D.(phid)
304311
Ra ~ ar
305312
Ra.w .~ w_a
306-
# w_a .~ angular_velocity2(ori(frame_a, false)) # This is required for FreeBody tests to pass, but the other one required for harmonic osciallator without joint to pass. FreeBody passes with quat=true so we use that instead
307-
w_a .~ ar.w # This one for most systems
313+
if vel_from_R
314+
w_a .~ angular_velocity2(ori(frame_a, false)) # This is required for FreeBody and ThreeSprings tests to pass, but the other one required for harmonic osciallator without joint to pass. FreeBody passes with quat=true so we use that instead
315+
else
316+
w_a .~ ar.w # This one for most systems
317+
end
308318
]
309319
end
310320
else

src/joints.jl

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -724,8 +724,10 @@ The relative position vector `r_rel_a` from the origin of `frame_a` to the origi
724724
(a_rel_a(t)[1:3] = a_rel_a), [description = "= der(v_rel_a)"]
725725
end
726726

727-
@named R_rel = NumRotationMatrix()
728-
@named R_rel_inv = NumRotationMatrix()
727+
@named R_rel_f = Frame()
728+
@named R_rel_inv_f = Frame()
729+
R_rel = ori(R_rel_f)
730+
R_rel_inv = ori(R_rel_inv_f)
729731

730732
eqs = [
731733
# Cut-forces and cut-torques are zero
@@ -750,7 +752,6 @@ The relative position vector `r_rel_a` from the origin of `frame_a` to the origi
750752
end
751753

752754
if quat
753-
# @named Q = NumQuaternion()
754755
append!(eqs, nonunit_quaternion_equations(R_rel, w_rel_b))
755756

756757
else
@@ -769,7 +770,11 @@ The relative position vector `r_rel_a` from the origin of `frame_a` to the origi
769770
R, angular_velocity1(frame_a)))
770771
end
771772
end
772-
compose(ODESystem(eqs, t; name), frame_a, frame_b)
773+
if state && !isroot
774+
compose(ODESystem(eqs, t; name), frame_a, frame_b, R_rel_f, R_rel_inv_f)
775+
else
776+
compose(ODESystem(eqs, t; name), frame_a, frame_b, R_rel_f, )
777+
end
773778
end
774779

775780
"""

src/orientation.jl

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ Create a new [`RotationMatrix`](@ref) struct with symbolic elements. `R,w` deter
3636
The primary difference between `NumRotationMatrix` and `RotationMatrix` is that the `NumRotationMatrix` constructor is used in the constructor of a [`Frame`](@ref) in order to introduce the frame variables, whereas `RorationMatrix` (the struct) only wraps existing variables.
3737
3838
- `varw`: If true, `w` is a variable, otherwise it is derived from the derivative of `R` as `w = get_w(R)`.
39+
40+
Never call this function directly from a component constructor, instead call `f = Frame(); R = ori(f)` and add `f` to the subsystems.
3941
"""
40-
function NumRotationMatrix(; R = collect(1.0I(3)), w = zeros(3), name, varw = false, state_priority=nothing)
42+
function NumRotationMatrix(; R = collect(1.0I(3)), w = zeros(3), name=:R, varw = false, state_priority=nothing)
43+
# The reason for not calling this directly is that all R vaiables have to have the same name since they are treated as connector variables (otherwise a connection error is thrown). A component with more than one rotation matrix will thus have two different R variables that overwrite each other
4144
R = at_variables_t(:R, 1:3, 1:3; default = R, state_priority) #[description="Orientation rotation matrix ∈ SO(3)"]
4245
# @variables w(t)[1:3]=w [description="angular velocity"]
4346
# R = collect(R)
@@ -251,9 +254,13 @@ end
251254

252255
Base.getindex(Q::Quaternion, i) = Q.Q[i]
253256

254-
function NumQuaternion(; Q = [0.0, 0, 0, 1.0], w = zeros(3), name, varw = false)
257+
"""
258+
Never call this function directly from a component constructor, instead call `f = Frame(); R = ori(f)` and add `f` to the subsystems.
259+
"""
260+
function NumQuaternion(; Q = [1.0, 0, 0, 0.0], w = zeros(3), name, varw = false)
261+
# The reason for not calling this directly is that all R vaiables have to have the same name since they are treated as connector variables (otherwise a connection error is thrown). A component with more than one rotation matrix will thus have two different R variables that overwrite each other
255262
# Q = at_variables_t(:Q, 1:4, default = Q) #[description="Orientation rotation matrix ∈ SO(3)"]
256-
@variables Q(t)[1:4] = [0.0,0,0,1.0]
263+
@variables Q(t)[1:4] = [1.0,0,0,0]
257264
if varw
258265
@variables w(t)[1:3]=w [description="angular velocity"]
259266
# w = at_variables_t(:w, 1:3, default = w)
@@ -274,23 +281,26 @@ orientation_constraint(q::Quaternion) = orientation_constraint(q.Q)
274281
# end
275282

276283
Base.:/(q::Rotations.Quaternions.Quaternion, x::Num) = Rotations.Quaternions.Quaternion(q.s / x, q.v1 / x, q.v2 / x, q.v3 / x)
277-
function from_Q(Q, w)
278-
Q2 = to_q(Q) # Due to different conventions
284+
function from_Q(Q2, w)
285+
# Q2 = to_q(Q) # Due to different conventions
279286
q = Rotations.QuatRotation(Q2)
280287
R = RotMatrix(q)
281288
RotationMatrix(R, w)
282289
end
283290

284-
to_q(Q) = SA[Q[4], Q[1], Q[2], Q[3]]
285-
to_mb(Q) = SA[Q[2], Q[3], Q[4], Q[1]]
291+
to_q(Q::AbstractVector) = SA[Q[4], Q[1], Q[2], Q[3]]
292+
to_q(Q::Rotations.QuatRotation) = to_q(vec(Q))
293+
to_mb(Q::AbstractVector) = SA[Q[2], Q[3], Q[4], Q[1]]
294+
to_mb(Q::Rotations.QuatRotation) = to_mb(vec(Q))
295+
Base.vec(Q::Rotations.QuatRotation) = SA[Q.q.s, Q.q.v1, Q.q.v2, Q.q.v3]
286296

287-
function angular_velocity1(Q, der_Q)
288-
2*([Q[4] -Q[3] Q[2] -Q[1]; Q[3] Q[4] -Q[1] -Q[2]; -Q[2] Q[1] Q[4] -Q[3]]*der_Q)
289-
end
297+
# function angular_velocity1(Q, der_Q)
298+
# 2*([Q[4] -Q[3] Q[2] -Q[1]; Q[3] Q[4] -Q[1] -Q[2]; -Q[2] Q[1] Q[4] -Q[3]]*der_Q)
299+
# end
290300

291-
function angular_velocity2(Q, der_Q)
292-
2*([Q[4] Q[3] -Q[2] -Q[1]; -Q[3] Q[4] Q[1] -Q[2]; Q[2] -Q[1] Q[4] -Q[3]]*der_Q)
293-
end
301+
# function angular_velocity2(Q, der_Q)
302+
# 2*([Q[4] Q[3] -Q[2] -Q[1]; -Q[3] Q[4] Q[1] -Q[2]; Q[2] -Q[1] Q[4] -Q[3]]*der_Q)
303+
# end
294304

295305

296306
## Euler
@@ -446,6 +456,8 @@ The rotation matrix returned, ``R_W^F``, is such that when a vector ``p_F`` expr
446456
p_W = R_W^F p_F
447457
```
448458
459+
The columns of ``R_W_F`` indicate are the basis vectors of the frame ``F`` expressed in the world coordinate frame.
460+
449461
See also [`get_trans`](@ref), [`get_frame`](@ref), [Orientations and directions](@ref) (docs section).
450462
"""
451463
function get_rot(sol, frame, t)
@@ -481,26 +493,25 @@ function get_frame(sol, frame, t)
481493
end
482494

483495
function nonunit_quaternion_equations(R, w)
484-
@variables Q(t)[1:4]=[0,0,0,1], [description="Unit quaternion with [i,j,k,w]"] # normalized
485-
@variables (t)[1:4]=[0,0,0,1], [description="Non-unit quaternion with [i,j,k,w]"] # Non-normalized
496+
@variables Q(t)[1:4]=[1,0,0,0], [description="Unit quaternion with [w,i,j,k]"] # normalized
497+
@variables (t)[1:4]=[1,0,0,0], [description="Non-unit quaternion with [w,i,j,k]"] # Non-normalized
486498
@variables n(t)=1 c(t)=0
487499
@parameters k = 0.1
488500
= collect(Q̂)
501+
Q = collect(Q)
502+
# w is used in Ω, and Ω determines D(Q̂)
503+
# This corresponds to modelica's
504+
# frame_a.R = from_Q(Q, angularVelocity2(Q, der(Q)));
505+
# where angularVelocity2(Q, der(Q)) = 2*([Q[4] Q[3] -Q[2] -Q[1]; -Q[3] Q[4] Q[1] -Q[2]; Q[2] -Q[1] Q[4] -Q[3]]*der_Q)
506+
# They also have w_a = angularVelocity2(frame_a.R) even for quaternions, so w_a = angularVelocity2(Q, der(Q)), this is their link between w_a and D(Q), while ours is D(Q̂) .~ (Ω * Q̂)
489507
Ω = [0 -w[1] -w[2] -w[3]; w[1] 0 w[3] -w[2]; w[2] -w[3] 0 w[1]; w[3] w[2] -w[1] 0]
490-
P = [
491-
0 0 0 1
492-
1 0 0 0
493-
0 1 0 0
494-
0 0 1 0
495-
]
496-
Ω = P'Ω*P
508+
# QR = from_Q(Q, angular_velocity2(Q, D.(Q)))
509+
QR = from_Q(Q, w)
497510
[
498511
n ~'
499-
# n ~ _norm(Q̂)^2
500512
c ~ k * (1 - n)
501-
D.(Q̂) .~* Q̂) ./ 2 + c *
513+
D.(Q̂) .~' * Q̂) ./ 2 + c * # We use Ω' which is the same as using -w to handle the fact that w coming in here is typically described frame_a rather than in frame_b, the paper is formulated with w being expressed in the rotating body frame (frame_b)
502514
Q .~./ sqrt(n)
503-
R ~ from_Q(Q, w)
504-
# R.w ~ w
515+
R ~ QR
505516
]
506517
end

0 commit comments

Comments
 (0)