Skip to content

Commit 4832876

Browse files
authored
Add Planar joint (#76)
* simplify introduction of cut joints * add cut joints for Spherical and Prismatic * add iscut to Prismatic * WIP planar joint * fix Force component * rm isroot * rm * add default color * up manifest * rm removed kwarg
1 parent 499c7bd commit 4832876

File tree

5 files changed

+176
-11
lines changed

5 files changed

+176
-11
lines changed

docs/src/examples/spring_damper_system.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ world = Multibody.world
2424
body2 = Body(; m = 1, isroot = false, r_cm = [0.0, -0.2, 0]) # This is not root since there is a joint parallel to the spring leading to this body
2525
bar1 = FixedTranslation(r = [0.3, 0, 0])
2626
bar2 = FixedTranslation(r = [0.6, 0, 0])
27-
p2 = Prismatic(n = [0, -1, 0], s0 = 0.1, axisflange = true, isroot = true)
27+
p2 = Prismatic(n = [0, -1, 0], s0 = 0.1, axisflange = true)
2828
spring2 = Multibody.Spring(c = 30, s_unstretched = 0.1)
2929
spring1 = Multibody.Spring(c = 30, s_unstretched = 0.1)
3030
damper1 = Multibody.Damper(d = 2)

src/Multibody.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ include("interfaces.jl")
153153
export World, world, Mounting1D, Fixed, FixedTranslation, FixedRotation, Body, BodyShape, BodyCylinder, Rope
154154
include("components.jl")
155155

156-
export Revolute, Prismatic, Spherical, Universal, GearConstraint, RollingWheelJoint,
156+
export Revolute, Prismatic, Planar, Spherical, Universal, GearConstraint, RollingWheelJoint,
157157
RollingWheel, FreeMotion, RevolutePlanarLoopConstraint
158158
include("joints.jl")
159159

src/forces.jl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,21 +90,22 @@ function BasicForce(; name, resolve_frame = :frame_b)
9090
r_0, f_b_0 = collect.((r_0, f_b_0))
9191

9292
eqs = [collect(r_0 .~ frame_b.r_0 - frame_a.r_0)
93+
collect(frame_b.tau) .~ 0
9394
0 .~ collect(frame_a.f) + resolve2(frame_a, f_b_0)
9495
0 .~ collect(frame_a.tau) + resolve2(frame_a, cross(r_0, f_b_0))]
9596

9697
if resolve_frame == :frame_a
9798
append!(eqs,
9899
[f_b_0 .~ -resolve1(frame_a, force.u)
99-
collect(frame_b.tau) .~ resolve2(frame_b, f_b_0)])
100+
collect(frame_b.f) .~ resolve2(frame_b, f_b_0)])
100101
elseif resolve_frame == :frame_b
101102
append!(eqs,
102103
[f_b_0 .~ -resolve1(frame_b, force.u)
103-
collect(frame_b.tau) .~ collect(-force.u)])
104+
collect(frame_b.f) .~ collect(-force.u)])
104105
elseif resolve_frame == :world
105106
append!(eqs,
106107
[f_b_0 .~ collect(-force.u)
107-
collect(frame_b.tau) .~ resolve2(frame_b, f_b_0)])
108+
collect(frame_b.f) .~ resolve2(frame_b, f_b_0)])
108109
else
109110
error("Unknown value of argument resolve_frame")
110111
end

src/joints.jl

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,13 @@ If `axisflange`, flange connectors for ModelicaStandardLibrary.Mechanics.Transla
100100
The function returns an ODESystem representing the prismatic joint.
101101
"""
102102
@component function Prismatic(; name, n = Float64[0, 0, 1], axisflange = false,
103-
isroot = true, iscut = false, s0 = 0, v0 = 0, radius = 0.05, color = [0,0.8,1,1])
104-
norm(n) 1 || error("Axis of motion must be a unit vector")
103+
s0 = 0, v0 = 0, radius = 0.05, color = [0,0.8,1,1], state_priority=10, iscut=false)
104+
if !(eltype(n) <: Num)
105+
norm(n) 1 || error("Prismatic axis of motion must be a unit vector, got norm(n) = $(norm(n))")
106+
end
105107
@named frame_a = Frame()
106108
@named frame_b = Frame()
107-
@parameters n[1:3]=n [description = "axis of motion"]
109+
@parameters n[1:3]=_normalize(n) [description = "axis of motion"]
108110
n = collect(n)
109111

110112
pars = @parameters begin
@@ -113,15 +115,15 @@ The function returns an ODESystem representing the prismatic joint.
113115
end
114116

115117
@variables s(t)=s0 [
116-
state_priority = 10,
118+
state_priority = state_priority,
117119
description = "Relative distance between frame_a and frame_b",
118120
]
119121
@variables v(t)=v0 [
120-
state_priority = 10,
122+
state_priority = state_priority,
121123
description = "Relative velocity between frame_a and frame_b",
122124
]
123125
@variables a(t)=0 [
124-
state_priority = 10,
126+
state_priority = state_priority,
125127
description = "Relative acceleration between frame_a and frame_b",
126128
]
127129
@variables f(t)=0 [
@@ -849,3 +851,81 @@ end
849851

850852
LinearAlgebra.normalize(a::Vector{Num}) = a / norm(a)
851853

854+
855+
"""
856+
Planar(; n = [0,0,1], n_x = [1,0,0], cylinderlength = 0.1, cylinderdiameter = 0.05, cylindercolor = [1, 0, 1, 1], boxwidth = 0.3*cylinderdiameter, boxheight = boxwidth, boxcolor = [0, 0, 1, 1])
857+
858+
Joint where frame_b can move in a plane and can rotate around an
859+
axis orthogonal to the plane. The plane is defined by
860+
vector `n` which is perpendicular to the plane and by vector `n_x`,
861+
which points in the direction of the x-axis of the plane.
862+
frame_a and frame_b coincide when s_x=prismatic_x.s=0,
863+
s_y=prismatic_y.s=0 and phi=revolute.phi=0.
864+
"""
865+
@mtkmodel Planar begin
866+
@structural_parameters begin
867+
state_priority = 1#, [description = "Priority used to choose whether the joint state variables are selected"]
868+
n
869+
n_x
870+
end
871+
begin
872+
cylindercolor = [1, 0, 1, 1]
873+
boxcolor = [0, 0, 1, 1]
874+
radius = 0.05
875+
end
876+
@parameters begin
877+
# (n[1:3]), [description = "Axis orthogonal to unconstrained plane, resolved in frame_a (= same as in frame_b)"]
878+
# (n_x[1:3]), [description = "Vector in direction of x-axis of plane, resolved in frame_a (n_x shall be orthogonal to n)"]
879+
cylinderlength = 0.1, [description = "Length of revolute cylinder"]
880+
cylinderdiameter = 0.05, [description = "Diameter of revolute cylinder"]
881+
# cylindercolor[1:4] = cylindercolordefault, [description = "Color of revolute cylinder"] # Endless bugs with array parameters
882+
boxwidth = 0.3*cylinderdiameter, [description = "Width of prismatic joint boxes"]
883+
boxheight = boxwidth, [description = "Height of prismatic joint boxes"]
884+
# boxcolor[1:4] = boxcolordefault, [description = "Color of prismatic joint boxes"]
885+
end
886+
begin
887+
n = collect(n)
888+
n_x = collect(n_x)
889+
end
890+
# @defaults begin
891+
# n .=> [0, 0, 1]
892+
# n_x .=> [1, 0, 0]
893+
# cylindercolor .=> [1, 0, 1, 1]
894+
# boxcolor .=> [0, 0, 1, 1]
895+
# end
896+
897+
@components begin
898+
frame_a = Frame()
899+
frame_b = Frame()
900+
prismatic_x = Prismatic(; state_priority=2.1, n=cross(cross(n, n_x), n), color=boxcolor)
901+
prismatic_y = Prismatic(; state_priority=2.1, n=cross(n, n_x), color=boxcolor)
902+
revolute = Revolute(; state_priority=2.1, n, isroot=false, color=cylindercolor, radius)
903+
end
904+
@variables begin
905+
(s_x(t) = 0), [state_priority = 3.0, description = "Relative distance along first prismatic joint starting at frame_a"]
906+
(s_y(t) = 0), [state_priority = 3.0, description = "Relative distance along second prismatic joint starting at first prismatic joint"]
907+
(phi(t) = 0), [state_priority = 3.0, description = "Relative rotation angle from frame_a to frame_b"]
908+
(v_x(t) = 0), [state_priority = 3.0, description = "Relative velocity along first prismatic joint"]
909+
(v_y(t) = 0), [state_priority = 3.0, description = "Relative velocity along second prismatic joint"]
910+
(w(t) = 0), [state_priority = 3.0, description = "Relative angular velocity around revolute joint"]
911+
(a_x(t) = 0), [description = "Relative acceleration along first prismatic joint"]
912+
(a_y(t) = 0), [description = "Relative acceleration along second prismatic joint"]
913+
(wd(t) = 0), [description = "Relative angular acceleration around revolute joint"]
914+
end
915+
@equations begin
916+
s_x ~ prismatic_x.s
917+
s_y ~ prismatic_y.s
918+
phi ~ revolute.phi
919+
v_x ~ D(s_x)
920+
v_y ~ D(s_y)
921+
w ~ D(phi)
922+
a_x ~ D(v_x)
923+
a_y ~ D(v_y)
924+
wd ~ D(w)
925+
connect(frame_a, prismatic_x.frame_a)
926+
connect(prismatic_x.frame_b, prismatic_y.frame_a)
927+
connect(prismatic_y.frame_b, revolute.frame_a)
928+
connect(revolute.frame_b, frame_b)
929+
end
930+
end
931+

test/runtests.jl

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,90 @@ doplot() && plot(sol, idxs = [body1.r_0...]) |> display
514514
# fixed_rotation_at_frame_a and b = true required
515515
end
516516

517+
@testset "FreeBody" begin
518+
t = Multibody.t
519+
D = Differential(t)
520+
world = Multibody.world
521+
522+
@named begin
523+
body = BodyShape(m = 1, I_11 = 1, I_22 = 1, I_33 = 1, r = [0.4, 0, 0],
524+
r_0 = [0.2, -0.5, 0.1], r_cm = [0.2, 0, 0], isroot = true)
525+
bar2 = FixedTranslation(r = [0.8, 0, 0])
526+
spring1 = Multibody.Spring(c = 20, s_unstretched = 0)
527+
spring2 = Multibody.Spring(c = 20, s_unstretched = 0)
528+
end
529+
530+
eqs = [connect(bar2.frame_a, world.frame_b)
531+
connect(spring1.frame_b, body.frame_a)
532+
connect(bar2.frame_b, spring2.frame_a)
533+
connect(spring1.frame_a, world.frame_b)
534+
connect(body.frame_b, spring2.frame_b)]
535+
536+
@named model = ODESystem(eqs, t,
537+
systems = [
538+
world,
539+
body,
540+
bar2,
541+
spring1,
542+
spring2,
543+
])
544+
ssys = structural_simplify(IRSystem(model))#, alias_eliminate = true)
545+
# ssys = structural_simplify(model, allow_parameters = false)
546+
prob = ODEProblem(ssys,
547+
[collect(body.body.w_a .=> 0);
548+
collect(body.body.v_0 .=> 0);
549+
collect(D.(body.body.phi)) .=> 1;
550+
# collect((body.body.r_0)) .=> collect((body.r_0));
551+
collect(D.(D.(body.body.phi))) .=> 1], (0, 10))
552+
553+
# @test_skip begin # The modelica example uses angles_fixed = true, which causes the body component to run special code for variable initialization. This is not yet supported by MTK
554+
# Without proper initialization, the example fails most of the time. Random perturbation of u0 can make it work sometimes.
555+
sol = solve(prob, Rodas4())
556+
@test SciMLBase.successful_retcode(sol)
557+
558+
@info "Initialization broken, initial value for body.r_0 not respected, add tests when MTK has a working initialization"
559+
doplot() && plot(sol, idxs = [body.r_0...]) |> display
560+
# end
561+
562+
end
563+
564+
# ==============================================================================
565+
## Planar joint ================================================================
566+
# ==============================================================================
567+
using LinearAlgebra
568+
@mtkmodel PlanarTest begin
569+
@components begin
570+
world = W()
571+
planar = Planar(n=[0,0,1], n_x=[1,0,0])
572+
force = Force()
573+
body = Body(m=1)
574+
end
575+
@equations begin
576+
connect(world.frame_b, planar.frame_a, force.frame_a)
577+
connect(planar.frame_b, body.frame_a, force.frame_b)
578+
force.force.u[1] ~ sin(t)
579+
force.force.u[2] ~ t
580+
force.force.u[3] ~ t^2/2
581+
# force.force.u .~ [sin(t), t, t^2]
582+
end
583+
end
584+
@named sys = PlanarTest()
585+
sys = complete(sys)
586+
ssys = structural_simplify(IRSystem(sys))
587+
prob = ODEProblem(ssys, [
588+
sys.world.g => 9.80665; # Modelica default
589+
# collect(sys.body.w_a) .=> 0;
590+
# collect(sys.body.v_0) .=> 0;
591+
], (0, 2))
592+
593+
sol = solve(prob, Rodas4())
594+
@test SciMLBase.successful_retcode(sol)
595+
596+
# plot(sol, idxs=sys.force.frame_a.f)
597+
@test sol(2, idxs=sys.body.r_0) [1.0907, -18.28, 0] atol=1e-3
598+
599+
# plot(sol)
600+
517601
# ==============================================================================
518602
## Sperical-joint pendulum ===================================================
519603
# ==============================================================================

0 commit comments

Comments
 (0)