Skip to content

Commit 2135549

Browse files
authored
add BodyBox component (#87)
* mention some additional joints * add BodyBox component * add "see also" in docstring * some progress something weird left in either rendering or dynamics * update rendering * tweak tol
1 parent c700221 commit 2135549

File tree

9 files changed

+350
-19
lines changed

9 files changed

+350
-19
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

docs/src/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ Pages = ["frames.jl"]
179179

180180
A joint restricts the number of degrees of freedom (DOF) of a body. For example, a free floating body has 6 DOF, but if it is attached to a [`Revolute`](@ref) joint, the joint restricts all but one rotational degree of freedom (a revolute joint acts like a hinge). Similarily, a [`Prismatic`](@ref) joint restricts all but one translational degree of freedom (a prismatic joint acts like a slider).
181181

182-
A [`Spherical`](@ref) joints restricts all translational degrees of freedom, but allows all rotational degrees of freedom. It thus transmits no torque.
182+
A [`Spherical`](@ref) joints restricts all translational degrees of freedom, but allows all rotational degrees of freedom. It thus transmits no torque. A [`Planar`](@ref) joint moves in a plane, i.e., it restricts one translational DOF and two rotational DOF. A [`Universal`](@ref) joint has two rotational DOF.
183183

184184
Some joints offer the option to add 1-dimensional components to them by providing the keyword `axisflange = true`. This allows us to add, e.g., springs, dampers, sensors, and actuators to the joint.
185185

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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Create a 3D animation of a multibody system
2626
- `loop`: The animation will be looped this many times. Please note: looping the animation using this argument is only recommended when `display = true` for camera manipulation purposes. When the camera is not manipulated, looping the animation by other means is recommended to avoid an increase in the file size.
2727
- `filename` controls the name and the file type of the resulting animation
2828
- `traces`: An optional array of frames to show the trace of.
29+
- `show_axis = false`: Whether or not to show the plot axes, including background grid.
2930
3031
# Camera control
3132
The following keyword arguments are available to control the camera pose:
@@ -150,7 +151,7 @@ include("frames.jl")
150151
export PartialTwoFrames
151152
include("interfaces.jl")
152153

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

156157
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)

0 commit comments

Comments
 (0)