|
| 1 | +# Pendulum---The "Hello World of multi-body dynamics" |
| 2 | +This beginners tutorial will model a pendulum pivoted around the origin in the world frame. The world frame is a constant that lives inside the Multibody module, all multibody models are "grounded" in the same world. To start, we load the required packages |
| 3 | +```@example pendulum |
| 4 | +using ModelingToolkit |
| 5 | +using Multibody |
| 6 | +using OrdinaryDiffEq # Contains the ODE solver we will use |
| 7 | +using Plots |
| 8 | +``` |
| 9 | +We then access the world frame and time variable from the Multibody module |
| 10 | +```@example pendulum |
| 11 | +t = Multibody.t |
| 12 | +world = Multibody.world |
| 13 | +``` |
| 14 | + |
| 15 | +## Modeling the pendulum |
| 16 | +Our simple pendulum will initially consist of a [`Body`](@ref) and a [`Revolute`](@ref) joint (the pivot joint). We construct these elements by calling their constructors |
| 17 | +```@example pendulum |
| 18 | +@named joint = Revolute(n = [0, 0, 1], isroot = true) |
| 19 | +@named body = Body(; m = 1, isroot = false, r_cm = [0.5, 0, 0]) |
| 20 | +nothing # hide |
| 21 | +``` |
| 22 | +The `n` argument to [`Revolute`](@ref) denotes the rotational axis of the joint, this vector must have `norm(n) == 1`. We also indicate that the revolute joint is the root of the kinematic tree, i.e., the potential states of the joint will serve as the state variables for the system. |
| 23 | + |
| 24 | +The [`Body`](@ref) is constructed by providing its mass, `m`, and the vector `r_cm` from its first frame, `body.frame_a`, to the center of mass. Since the world by default has the gravity field pointing along the negative ``y`` axis, we place the center of mass along the ``x``-axis to make the pendulum swing back and forth. The body is not selected as the root of the kinematic tree, since we have a joint in this system, but if we had attached the body directly to, e.g., a spring, we could set the body to be the root and avoid having to introduce an "artificial joint". |
| 25 | + |
| 26 | +To connect the components together, we create a vector of connections using the `connect` function. A joint typically has two frames, `frame_a` and `frame_b`. The first frame of the joint is attached to the world frame, and the body is attached to the second joint frame. The order of the connections is not important for ModelingToolkit, but it's good practice to follow some convention, here, we start at the world and progress outwards in the kinematic tree. |
| 27 | +```@example pendulum |
| 28 | +connections = [ |
| 29 | + connect(world.frame_b, joint.frame_a) |
| 30 | + connect(joint.frame_b, body.frame_a) |
| 31 | +] |
| 32 | +nothing # hide |
| 33 | +``` |
| 34 | + |
| 35 | +With all components and connections defined, we can create an `ODESystem` like so: |
| 36 | +```@example pendulum |
| 37 | +@named model = ODESystem(connections, t, systems=[world, joint, body]) |
| 38 | +nothing # hide |
| 39 | +``` |
| 40 | +The `ODESystem` is the fundamental model type in ModelingToolkit used for multibody-type models. |
| 41 | + |
| 42 | +Before we can simulate the system, we must perform model compilation using [`structural_simplify`](@ref) |
| 43 | +```@example pendulum |
| 44 | +ssys = structural_simplify(model, allow_parameter = false) |
| 45 | +``` |
| 46 | +This results in a simplified model with the minimum required variables and equations to be able to simulate the system efficiently. This step rewrites all `connect` statements into the appropriate equations, and removes any redundant variables and equations. |
| 47 | + |
| 48 | +We are now ready to create an `ODEProblem` and simulate it. We use the `Rodas4` solver from OrdinaryDiffEq.jl, and pass a dictionary for the initial conditions. We specify only initial condition for some variables, for those variables where no initial condition is specified, the default initial condition defined the model will be used. |
| 49 | +```@example pendulum |
| 50 | +D = Differential(t) |
| 51 | +defs = Dict(D(joint.phi) => 0, D(D(joint.phi)) => 0) |
| 52 | +prob = ODEProblem(ssys, defs, (0, 10)) |
| 53 | +
|
| 54 | +sol = solve(prob, Rodas4()) |
| 55 | +plot(sol, idxs = joint.phi) |
| 56 | +``` |
| 57 | +The solution `sol` can be plotted directly if the Plots package is loaded. The figure indicates that the pendulum swings back and forth without any damping. To add damping as well, we could add a `Damper` from the `ModelingToolkitStandardLibrary.Mechanical.Rotational` module to the revolute joint. We do this below |
| 58 | + |
| 59 | +## Adding damping |
| 60 | +To add damping to the pendulum such that the pendulum will eventually come to rest, we add a [`Damper`](@ref) to the revolute joint. The damping coefficient is given by `d`, and the damping force is proportional to the angular velocity of the joint. To add the damper to the revolute joint, we must create the joint with the keyword argument `useAxisFlange = true`, this adds two internal flanges to the joint to which you can attach components from the `ModelingToolkitStandardLibrary.Mechanical.Rotational` module. We then connect one of the flanges of the damper to the axis flange of the joint, and the other damper flange to the support flange which is rigidly attached to the world. |
| 61 | +```@example pendulum |
| 62 | +using ModelingToolkitStandardLibrary.Mechanical.Rotational |
| 63 | +
|
| 64 | +@named damper = Damper(d = 0.1) |
| 65 | +@named joint = Revolute(n = [0, 0, 1], isroot = true, useAxisFlange = true) |
| 66 | +
|
| 67 | +connections = [connect(world.frame_b, joint.frame_a) |
| 68 | + connect(damper.flange_b, joint.axis) |
| 69 | + connect(joint.support, damper.flange_a) |
| 70 | + connect(body.frame_a, joint.frame_b)] |
| 71 | +
|
| 72 | +@named model = ODESystem(connections, t, systems = [world, joint, body, damper]) |
| 73 | +ssys = structural_simplify(model, allow_parameter = false) |
| 74 | +
|
| 75 | +
|
| 76 | +prob = ODEProblem(ssys, [damper.phi_rel => 1, D(joint.phi) => 0, D(D(joint.phi)) => 0], |
| 77 | + (0, 30)) |
| 78 | +
|
| 79 | +
|
| 80 | +sol = solve(prob, Rodas4()) |
| 81 | +plot(sol, idxs = collect(joint.phi)) |
| 82 | +``` |
| 83 | +This time we see that the pendulum loses energy and eventually comes to rest at the stable equilibrium point ``\pi / 2``. |
0 commit comments