Skip to content

Commit d47a727

Browse files
committed
Merge branch 'main' of github.com:JuliaComputing/Multibody.jl
2 parents defe6d0 + 2135549 commit d47a727

File tree

8 files changed

+348
-18
lines changed

8 files changed

+348
-18
lines changed

docs/src/examples/gyroscopic_effects.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ In this example, we demonstrate how a rotating body creates a reaction torque wh
77
The system consists of a pendulum suspended in a spherical joint, a joint without any rotational constraints. The tip of the pendulum is a cylinder that is rotating around a revolute joint in its center. When the pendulum swings, the rotation axis of the rotating tip is changed, this causes the entire pendulum to rotate around the axis through the pendulum rod.
88

99

10-
```@example spring_mass_system
10+
```@example GYRO
1111
using Multibody
1212
using ModelingToolkit
1313
using Plots
@@ -42,7 +42,7 @@ ssys = structural_simplify(IRSystem(model))
4242
4343
prob = ODEProblem(ssys, [model.world.g => 9.80665, model.revolute.w => 10], (0, 5))
4444
45-
sol = solve(prob, Rodas5P(), abstol=1e-7, reltol=1e-7);
45+
sol = solve(prob, FBDF(), abstol=1e-8, reltol=1e-8);
4646
@assert SciMLBase.successful_retcode(sol)
4747
using Test # hide
4848
@test sol(5, idxs=collect(model.body2.r_0[1:3])) ≈ [-0.0357364, -0.188245, 0.02076935] atol=1e-3 # hide

docs/src/examples/spherical_pendulum.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
![animation](spherical.gif)
44

5-
This example models a spherical pendulum. The pivot point is modeled using a [`Spherical`](@ref) joint, the pendulum rod is modeled using a [`FixedTranslation`](@ref) and the mass is modeled using a [`Body`](@ref). In this example, we choose the joint to be the root (joints are often better root objects than bodies).
5+
This example models a spherical pendulum. The pivot point is modeled using a [`Spherical`](@ref) joint, this lets the pendulum rotate in three directions. The pendulum rod is modeled using a [`FixedTranslation`](@ref), a component without inertial properties, and the mass of the tip is modeled using a [`Body`](@ref). To model a rod with inertial properties, see, e.g., [`BodyShape`](@ref) or [`BodyCylinder`](@ref), In this example, we choose the joint to be the root (joints are often better root objects than bodies).
66

77

88
```@example spring_mass_system

ext/Render.jl

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,44 @@ function render!(scene, ::typeof(BodyCylinder), sys, sol, t)
435435
true
436436
end
437437

438+
function render!(scene, ::typeof(BodyBox), sys, sol, t)
439+
440+
# NOTE: This draws a solid box without the hole in the middle. Cannot figure out how to render a hollow box
441+
color = get_color(sys, sol, [1, 0.2, 1, 0.9])
442+
width = Float32(sol(sol.t[1], idxs=sys.width))
443+
height = Float32(sol(sol.t[1], idxs=sys.height))
444+
length = Float32(sol(sol.t[1], idxs=sys.render_length))
445+
446+
length_dir = sol(sol.t[1], idxs=collect(sys.render_length_dir))
447+
width_dir = sol(sol.t[1], idxs=collect(sys.render_width_dir))
448+
height_dir = normalize(cross(normalize(length_dir), normalize(width_dir)))
449+
width_dir = normalize(cross(height_dir, length_dir))
450+
451+
Rfun = get_rot_fun(sol, sys.frame_a)
452+
r_0a = get_fun(sol, collect(sys.frame_a.r_0)) # Origin is translated by r_shape
453+
r_shape = sol(sol.t[1], idxs=collect(sys.render_r_shape))
454+
r = sol(sol.t[1], idxs=collect(sys.render_r))
455+
456+
R0 = [length_dir width_dir height_dir]
457+
# R0 = Multibody.from_nxy(r, width_dir).R'
458+
@assert isapprox(det(R0), 1.0, atol=1e-6)
459+
# NOTE: The rotation by this R and the translation with r_shape needs to be double checked
460+
461+
origin = Vec3f(0, -width/2, -height/2) + r_shape
462+
extent = Vec3f(length, width, height)
463+
thing = Makie.Rect3f(origin, extent)
464+
m = mesh!(scene, thing; color, specular = Vec3f(1.5))
465+
on(t) do t
466+
r1 = Point3f(r_0a(t))
467+
R = Rfun(t)
468+
q = Rotations.QuatRotation(R*R0).q
469+
Q = Makie.Quaternionf(q.v1, q.v2, q.v3, q.s)
470+
Makie.transform!(m, translation=r1, rotation=Q)
471+
end
472+
473+
true
474+
end
475+
438476
function render!(scene, ::typeof(Damper), sys, sol, t)
439477
r_0a = get_fun(sol, collect(sys.frame_a.r_0))
440478
r_0b = get_fun(sol, collect(sys.frame_b.r_0))

src/Multibody.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ include("frames.jl")
151151
export PartialTwoFrames
152152
include("interfaces.jl")
153153

154-
export World, world, Mounting1D, Fixed, FixedTranslation, FixedRotation, Body, BodyShape, BodyCylinder, Rope
154+
export World, world, Mounting1D, Fixed, FixedTranslation, FixedRotation, Body, BodyShape, BodyCylinder, BodyBox, Rope
155155
include("components.jl")
156156

157157
export Revolute, Prismatic, Planar, Spherical, Universal, GearConstraint, RollingWheelJoint,

src/components.jl

Lines changed: 239 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ The `BodyShape` component is similar to a [`Body`](@ref), but it has two frames
357357
- `r`: Vector from `frame_a` to `frame_b` resolved in `frame_a`
358358
- All `kwargs` are passed to the internal `Body` component.
359359
- `shapefile`: A path::String to a CAD model that can be imported by MeshIO for 3D rendering. If none is provided, a cylinder shape is rendered.
360+
361+
See also [`BodyCylinder`](@ref) and [`BodyBox`](@ref) for body components with predefined shapes and automatically computed inertial properties based on geometry and density.
360362
"""
361363
@component function BodyShape(; name, m = 1, r = [0, 0, 0], r_cm = 0.5*r, r_0 = 0, radius = 0.08, color=purple, shapefile="", kwargs...)
362364
systems = @named begin
@@ -643,8 +645,8 @@ end
643645
@components begin
644646
frame_a = Frame()
645647
frame_b = Frame()
646-
frameTranslation = FixedTranslation(r = r)
647-
body = Body(; m, r_cm, I_11 = I[1,1], I_22 = I[2,2], I_33 = I[3,3], I_21 = I[2,1], I_31 = I[3,1], I_32 = I[3,2], isroot, quat)
648+
translation = FixedTranslation(r = r)
649+
body = Body(; m, r_cm, I_11 = I[1,1], I_22 = I[2,2], I_33 = I[3,3], I_21 = I[2,1], I_31 = I[3,1], I_32 = I[3,2])
648650
end
649651

650652
@equations begin
@@ -657,8 +659,240 @@ end
657659
a_0[1] ~ D(v_0[1])
658660
a_0[2] ~ D(v_0[2])
659661
a_0[3] ~ D(v_0[3])
660-
connect(frame_a, frameTranslation.frame_a)
661-
connect(frame_b, frameTranslation.frame_b)
662+
connect(frame_a, translation.frame_a)
663+
connect(frame_b, translation.frame_b)
662664
connect(frame_a, body.frame_a)
663665
end
664-
end
666+
end
667+
668+
669+
@mtkmodel BodyBox begin
670+
@structural_parameters begin
671+
r = [1, 0, 0]
672+
r_shape = [0, 0, 0]
673+
width_dir = [0,1,0] # https://github.com/SciML/ModelingToolkit.jl/issues/2810
674+
length_dir = _normalize(r - r_shape)
675+
length = _norm(r - r_shape)
676+
end
677+
begin
678+
iszero(r_shape) || error("non-zero r_shape not supported")
679+
width_dir = collect(width_dir)
680+
length_dir = collect(length_dir)
681+
end
682+
683+
@parameters begin
684+
# r[1:3]=r, [ # MTKs symbolic language is too weak to handle this as a symbolic parameter in from_nxy
685+
# description = "Vector from frame_a to frame_b resolved in frame_a",
686+
# ]
687+
# r_shape[1:3]=zeros(3), [
688+
# description = "Vector from frame_a to box origin, resolved in frame_a",
689+
# ]
690+
# length = _norm(r - r_shape), [
691+
# description = "Length of box",
692+
# ]
693+
# length_dir[1:3] = _norm(r - r_shape), [
694+
# description = "Vector in length direction of box, resolved in frame_a",
695+
# ]
696+
697+
# width_dir[1:3] = width_dir0, [
698+
# description = "Vector in width direction of box, resolved in frame_a",
699+
# ]
700+
701+
# NOTE: these are workarounds to allow rendering of this component. Unfortunately, MTK/JSCompiler cannot handle parameter arrays well enough to let these be actual parameters
702+
render_r[1:3]=r, [description="For internal use only"]
703+
render_r_shape[1:3]=r_shape, [description="For internal use only"]
704+
render_length = length, [description="For internal use only"]
705+
render_length_dir[1:3] = length_dir, [description="For internal use only"]
706+
render_width_dir[1:3] = width_dir, [description="For internal use only"]
707+
708+
709+
width = 0.3*length, [
710+
description = "Width of box",
711+
]
712+
height = width, [
713+
description = "Height of box",
714+
]
715+
716+
inner_width = 0, [
717+
description = "Width of inner box surface (0 <= inner_width <= width)",
718+
]
719+
inner_height = inner_width, [
720+
description = "Height of inner box surface (0 <= inner_height <= height)",
721+
]
722+
density = 7700, [
723+
description = "Density of cylinder (e.g., steel: 7700 .. 7900, wood : 400 .. 800)",
724+
]
725+
color[1:4] = purple, [description = "Color of box in animations"]
726+
end
727+
begin
728+
mo = density*length*width*height
729+
mi = density*length*inner_width*inner_height
730+
m = mo - mi
731+
R = from_nxy(r, width_dir)
732+
r_cm = r_shape + _normalize(length_dir)*length/2
733+
r_cm = collect(r_cm)
734+
735+
I11 = mo*(width^2 + height^2) - mi*(inner_width^2 + inner_height^2)
736+
I22 = mo*(length^2 + height^2) - mi*(length^2 + inner_height^2)
737+
I33 = mo*(length^2 + width^2) - mi*(length^2 + inner_width^2)
738+
I = resolve_dyade1(R, Diagonal([I11, I22, I33] ./ 12))
739+
end
740+
741+
@variables begin
742+
r_0(t)[1:3]=zeros(3), [
743+
state_priority = 2,
744+
description = "Position vector from origin of world frame to origin of frame_a",
745+
]
746+
v_0(t)[1:3]=zeros(3), [
747+
state_priority = 2,
748+
description = "Absolute velocity of frame_a, resolved in world frame (= D(r_0))",
749+
]
750+
a_0(t)[1:3]=zeros(3), [
751+
description = "Absolute acceleration of frame_a resolved in world frame (= D(v_0))",
752+
]
753+
end
754+
755+
@components begin
756+
frame_a = Frame()
757+
frame_b = Frame()
758+
translation = FixedTranslation(r = r)
759+
body = Body(; m, r_cm, I_11 = I[1,1], I_22 = I[2,2], I_33 = I[3,3], I_21 = I[2,1], I_31 = I[3,1], I_32 = I[3,2])
760+
end
761+
762+
@equations begin
763+
r_0[1] ~ ((frame_a.r_0)[1])
764+
r_0[2] ~ ((frame_a.r_0)[2])
765+
r_0[3] ~ ((frame_a.r_0)[3])
766+
v_0[1] ~ D(r_0[1])
767+
v_0[2] ~ D(r_0[2])
768+
v_0[3] ~ D(r_0[3])
769+
a_0[1] ~ D(v_0[1])
770+
a_0[2] ~ D(v_0[2])
771+
a_0[3] ~ D(v_0[3])
772+
connect(frame_a, translation.frame_a)
773+
connect(frame_b, translation.frame_b)
774+
connect(frame_a, body.frame_a)
775+
end
776+
end
777+
778+
# function BodyBox2(;
779+
# r = [1, 0, 0],
780+
# # r_shape = [0, 0, 0],
781+
# width_dir = [0,1,0],
782+
# # length_dir = _normalize(r - r_shape),
783+
# # length = _norm(r - r_shape),
784+
785+
# # r,
786+
# r_shape = nothing,
787+
# length = nothing,
788+
# length_dir = nothing,
789+
# # width_dir = nothing,
790+
# width = nothing,
791+
# height = nothing,
792+
# inner_width = nothing,
793+
# inner_height = nothing,
794+
# density = nothing,
795+
# color = nothing,
796+
# name,
797+
# )
798+
799+
800+
# # @parameters r[1:3]=something(r, [1,0,0]), [
801+
# # description = "Vector from frame_a to frame_b resolved in frame_a",
802+
# # ]
803+
# r = collect(r)
804+
# @parameters r_shape[1:3]=something(r_shape, zeros(3)), [
805+
# description = "Vector from frame_a to box origin, resolved in frame_a",
806+
# ]
807+
# r_shape = collect(r_shape)
808+
# @parameters length_dir[1:3] = something(length_dir, _norm(r - r_shape)), [
809+
# description = "Vector in length direction of box, resolved in frame_a",
810+
# ]
811+
# length_dir = collect(length_dir)
812+
# # @parameters width_dir[1:3] = something(width_dir, [0,1,0]), [
813+
# # description = "Vector in width direction of box, resolved in frame_a",
814+
# # ]
815+
# width_dir = collect(width_dir)
816+
# @parameters color[1:4] = something(color, purple), [
817+
# description = "Color of box in animations"
818+
# ]
819+
# color = collect(color)
820+
# pars = @parameters begin
821+
# length = something(length, _norm(r - r_shape)), [
822+
# description = "Length of box",
823+
# ]
824+
# width = something(width, 0.3*length), [
825+
# description = "Width of box",
826+
# ]
827+
# height = something(height, width), [
828+
# description = "Height of box",
829+
# ]
830+
# inner_width = something(inner_width, 0), [
831+
# description = "Width of inner box surface (0 <= inner_width <= width)",
832+
# ]
833+
# inner_height = something(inner_height, inner_width), [
834+
# description = "Height of inner box surface (0 <= inner_height <= height)",
835+
# ]
836+
# density = something(density, 7700), [
837+
# description = "Density of box (e.g., steel: 7700 .. 7900, wood : 400 .. 800)",
838+
# ]
839+
# end
840+
# pars = [
841+
# pars;
842+
# # collect(r);
843+
# collect(r_shape);
844+
# collect(length_dir);
845+
# # collect(width_dir);
846+
# collect(color);
847+
# ]
848+
# mo = density*length*width*height
849+
# mi = density*length*inner_width*inner_height
850+
# m = mo - mi
851+
# R = from_nxy(r, width_dir)
852+
# r_cm = r_shape + _normalize(length_dir)*length/2
853+
# r_cm = collect(r_cm)
854+
855+
# I11 = mo*(width^2 + height^2) - mi*(inner_width^2 + inner_height^2)
856+
# I22 = mo*(length^2 + height^2) - mi*(length^2 + inner_height^2)
857+
# I33 = mo*(length^2 + width^2) - mi*(length^2 + inner_width^2)
858+
# I = resolve_dyade1(R, Diagonal([I11, I22, I33] ./ 12))
859+
860+
# @variables begin
861+
# r_0(t)[1:3]=zeros(3), [
862+
# state_priority = 2,
863+
# description = "Position vector from origin of world frame to origin of frame_a",
864+
# ]
865+
# v_0(t)[1:3]=zeros(3), [
866+
# state_priority = 2,
867+
# description = "Absolute velocity of frame_a, resolved in world frame (= D(r_0))",
868+
# ]
869+
# a_0(t)[1:3]=zeros(3), [
870+
# description = "Absolute acceleration of frame_a resolved in world frame (= D(v_0))",
871+
# ]
872+
# end
873+
# r_0, v_0, a_0 = collect.((r_0, v_0, a_0))
874+
# vars = [r_0; v_0; a_0]
875+
876+
# systems = @named begin
877+
# frame_a = Frame()
878+
# frame_b = Frame()
879+
# translation = FixedTranslation(r = r)
880+
# body = Body(; m, r_cm, I_11 = I[1,1], I_22 = I[2,2], I_33 = I[3,3], I_21 = I[2,1], I_31 = I[3,1], I_32 = I[3,2])
881+
# end
882+
883+
# equations = [
884+
# r_0[1] ~ ((frame_a.r_0)[1])
885+
# r_0[2] ~ ((frame_a.r_0)[2])
886+
# r_0[3] ~ ((frame_a.r_0)[3])
887+
# v_0[1] ~ D(r_0[1])
888+
# v_0[2] ~ D(r_0[2])
889+
# v_0[3] ~ D(r_0[3])
890+
# a_0[1] ~ D(v_0[1])
891+
# a_0[2] ~ D(v_0[2])
892+
# a_0[3] ~ D(v_0[3])
893+
# connect(frame_a, translation.frame_a)
894+
# connect(frame_b, translation.frame_b)
895+
# connect(frame_a, body.frame_a)
896+
# ]
897+
# ODESystem(equations, t, vars, pars; name, systems)
898+
# end

src/joints.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ Joint with 3 constraints that define that the origin of `frame_a` and the origin
241241
[connect_orientation(ori(frame_b), absolute_rotation(frame_a, Rrel); iscut)
242242
zeros(3) .~ collect(frame_a.f) + resolve1(Rrel, frame_b.f)])
243243
else
244+
# NOTE: this branch should never happen
244245
append!(eqs,
245246
[connect_orientation(Rrel_inv, inverse_rotation(Rrel); iscut)
246247
ori(frame_a) ~ absolute_rotation(frame_b, Rrel_inv)

src/orientation.jl

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -317,16 +317,24 @@ end
317317

318318

319319
function from_nxy(n_x, n_y)
320-
e_x = norm(n_x) < 1e-10 ? [1.0, 0, 0] : _normalize(n_x)
321-
e_y = norm(n_y) < 1e-10 ? [0, 1.0, 0] : _normalize(n_y)
320+
321+
nn_x = collect(n_x) ./ _norm(n_x)
322+
nn_y = collect(n_y) ./ _norm(n_y)
323+
324+
e_x = [ifelse(_norm(n_x) < 1e-10, [1.0, 0, 0][i], nn_x[i]) for i in 1:3]
325+
e_y = [ifelse(_norm(n_y) < 1e-10, [0, 1.0, 0][i], nn_y[i]) for i in 1:3]
322326
n_z_aux = cross(e_x, e_y)
323-
n_y_aux = #if n_z_aux' * n_z_aux > 1.0e-6 # TODO: MTK too buggy with ifelse to handle this logic
324-
e_y
325-
# elseif abs(e_x[1]) > 1.0e-6
326-
# [0, 1.0, 0]
327-
# else
328-
# [1.0, 0, 0]
329-
# end
327+
328+
cond = n_z_aux' * n_z_aux > 1.0e-6
329+
330+
n_y_aux = [ifelse(
331+
cond,
332+
e_y[i],
333+
ifelse(
334+
abs(e_x[1]) > 1.0e-6,
335+
[0, 1.0, 0][i],
336+
[1.0, 0, 0][i])
337+
) for i = 1:3]
330338
e_z_aux = cross(e_x, n_y_aux)
331339
e_z = _normalize(e_z_aux)
332340
RotationMatrix([e_x cross(e_z, e_x) e_z]', zeros(3))

0 commit comments

Comments
 (0)