Turtle: two cranks and a virtual flywheel

In this example we keep the two cranks of a crawling turtle in step with a virtual flywheel, and simulate the cranks following it.

The robot

The turtle crawls on two cranks, left and right, each turned by its own motor. A third motor sets the angle of a variable-stiffness actuator (VSA). It is position-controlled and not part of this controller. turtle.robot() holds only the two crank angles \(q\) = (left, right) [rad]: it has no geometry and no masses.

The controller adds a virtual flywheel, a degree of freedom of the controller with its own inertia. A spring and a damper tie each crank to it: the left crank to the flywheel’s angle \(\varphi\), the right crank to \(\varphi - \delta\). A speed regulator drives the flywheel towards a commanded speed \(\bar\omega\), and a constant bias torque acts on both cranks.

../_images/66e20db3ced3256541537212c8b3c0082852e8735310067a082d6f1f7f8daf01.svg

The crank angles follow the gait convention, in which both cranks turn forward at positive angles. The right motor is mounted mirrored, so its sign is −1: multiply the raw readings and the commands by turtle.MOTOR_SIGNS (see conventions).

turtle.controller takes the controller’s parameters as keyword arguments. Their defaults are starting values, not tuned:

Keyword

Default

Unit

Meaning

stiffness

1

N·m/rad

\(K\), the spring from each crank to the flywheel

damping

0.0001

N·m·s/rad

\(C\), the damper beside each spring

inertia

0.1

kg·m²

\(J_v\), inertia of the flywheel

flywheel_damping

0.1

N·m·s/rad

\(b_v\), gain of the speed regulator

speed

0.1

rad/s

\(\bar\omega\), the speed it drives the flywheel to

phase

0

rad

\(\delta\), how far the right crank runs behind

ramp_time

3

s

time to ramp \(\bar\omega\) up from zero

torque_bias

0

N·m

\(\tau_b\), a constant torque on both cranks

Simulate the cranks

The template has no dynamics, so we give the cranks an inertia and a viscous friction with two library components. Their values are illustrative. They belong to the robot mechanism, which ModelPlant simulates, while compile uses the controller’s components only. We ask for a faster speed than the default, with the cranks half a turn apart.

import numpy as np
import matplotlib.pyplot as plt
import virtualmodelcontrol as vmc
from virtualmodelcontrol import viz
from virtualmodelcontrol.robots import turtle

robot = turtle.robot()
cranks = robot.joint([0, 1])
robot.add("inertia", vmc.Inertance(cranks, 1e-3))  # [kg·m²]
robot.add("friction", vmc.LinearDamper(cranks, 1e-2))  # [N·m·s/rad]

ctrl = turtle.controller(robot, speed=2.0, phase=np.pi, ramp_time=1.0)
system = vmc.VirtualMechanismSystem(robot, ctrl)
controller = vmc.VMCController(vmc.compile(system))
plant = vmc.sim.ModelPlant(robot)
z0 = turtle.initial_state(plant.read())
clock = vmc.sim.SimClock(dt=1 / turtle.CONTROL_RATE)
log = vmc.sim.run(plant, controller, clock, T=8.0, z0=z0)

initial_state starts the flywheel at the left crank’s angle, at rest. The log holds the flywheel’s angle and speed as z, next to the crank angles q:

rows = log.arrays()
t, q, z = rows["t"].ravel(), rows["q"], rows["z"]

fig, (top, bottom) = plt.subplots(2, 1, figsize=(6.4, 6.4), sharex=True)
top.plot(t, q[:, 0], label="left crank")
top.plot(t, q[:, 1], label="right crank")
top.plot(t, z[:, 0], "--", label="flywheel")
top.set_ylabel("angle [rad]")
top.legend()
bottom.plot(t, q[:, 0] - q[:, 1], label="left minus right")
bottom.axhline(np.pi, color=viz.PALETTE[9], ls="--", label="commanded")
bottom.set_xlabel("time [s]")
bottom.set_ylabel("phase [rad]")
bottom.legend();
../_images/3941b956ff1172f3d75dd652f13a68120cfc0d5ae31a32d35efd92a0034e8fbf.svg

The cranks start together. The right crank’s spring pulls it back by half a turn. It overshoots, swings and settles after about 0.8 s, half a turn behind the left one from then on. The flywheel spins up over the ramp, and the left crank follows it so closely that their lines overlap. The flywheel settles at 1.67 rad/s, not 2: the cranks’ friction \(c\) loads it through the springs, and \(b_v(\bar\omega - \omega) = 2c\,\omega\) gives 1.67 rad/s.

Animate

The robot has no geometry, so animate cannot draw it alone. The draw callback draws the cranks as bars and the flywheel as a disc with a mark, from each row of the log. limits sets the view.

def bar(ax, x, angle, **style):
    ax.plot([x, x + 0.6 * np.cos(angle)], [0, 0.6 * np.sin(angle)],
            solid_capstyle="round", **style)

def draw(ax, row):
    ax.set_axis_off()
    for x, angle, name in zip((-1.6, 1.6), row["q"], turtle.CRANKS):
        ax.add_patch(plt.Circle((x, 0), 0.6, fill=False, ec="0.7", ls="--"))
        bar(ax, x, angle, color=viz.PALETTE[3], lw=6)
        ax.text(x, -0.8, name, ha="center", va="top")
    ax.add_patch(plt.Circle((0, 0), 0.6, color=viz.PALETTE[5], alpha=0.35))
    bar(ax, 0.0, row["z"][0], color=viz.PALETTE[3], lw=3)
    ax.text(0, -0.8, "flywheel", ha="center", va="top")

viz.animate(robot, log, "turtle.mp4", draw=draw,
            limits=((-2.3, 2.3), (-1.1, 0.7)), figsize=(6.4, 3.2))
The two cranks follow the virtual flywheel (middle), the right one half a turn behind.

To see the turtle’s cranks crawl in simulation, with a body on the ground, see Crawl with a flywheel.