Parameters

In this tutorial we look at the parameters of the soft arm and of a controller, choose which ones the compiled controller keeps live, and move a goal while the arm runs.

Read and change a Param

Every number of a robot or of a controller is a Param: a value with a unit, bounds and a scope. Here is the rest length of the arm’s first segment:

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

arm = helyx.add_dynamics(helyx.arm("145-145-145"))
length = arm.params["seg1.L0"]  # arm.params maps names to Params
length, length.bounds
(Param('seg1.L0', 0.145, unit='m', scope='design'), (0.0, inf))

vmc.Kinematics evaluates at the current values, so lengthening the first segment moves the tip of the straight arm by the same amount:

kin = vmc.Kinematics(arm)
before = kin.position(np.zeros(9), 1.0)  # the tip [m]
length.value = 0.150  # [m]
after = kin.position(np.zeros(9), 1.0)
length.value = 0.145  # back to the real arm
1000 * (after - before)  # [mm]
array([0., 0., 5.])

Scopes

The scope says how often a value may change: fixed never, design with the hardware (lengths, masses, transmissions), episode between runs (attachment points, directions, joint ranges) and stage at any control step (stiffnesses, dampings, goals). Each component gives its Params their scopes:

goal = vmc.Ref("goal", value=[0.0, 0.0, 0.40])  # a named goal [m]
tip = arm.point(s=1.0)  # s: the arc parameter, 0 at the base, 1 at the tip

ctrl = vmc.Mechanism("ctrl")
ctrl.add("reach", vmc.LinearSpring(tip - goal, 600.0))  # [N/m]
ctrl.add("damp", vmc.LinearDamper(tip, 5.0))  # [N·s/m]
ctrl.add("gravity", vmc.GravityCompensation(arm))
system = vmc.VirtualMechanismSystem(arm, ctrl)
{name: p.scope for name, p in system.params.items() if "ctrl" in name}
{'ctrl.reach.stiffness': 'stage',
 'ctrl.reach.s': 'episode',
 'ctrl.reach.goal': 'stage',
 'ctrl.damp.damping': 'stage'}

In a system, names read <mechanism>.<component>.<name>. The tip’s arc parameter belongs to the first component that uses the point, reach. Gravity compensation shares the robot’s own gravity Param, which keeps the robot’s name, arm.gravity.

What compile keeps live

compile keeps the stage Params as live inputs of the compiled function and folds every other Param in as a constant, at its current value. Constants make the function faster. To change one, change its Param and compile again.

law = vmc.compile(system)
law.live
['ctrl.reach.stiffness', 'ctrl.reach.goal', 'ctrl.damp.damping']

runtime= keeps more Params live, by name or by glob pattern. Here, every attachment point of the controller:

vmc.compile(system, runtime=["ctrl.*.s"]).live
['ctrl.reach.stiffness',
 'ctrl.reach.s',
 'ctrl.reach.goal',
 'ctrl.damp.damping']

Change a value while running

controller.set changes live values, all at once, and returns the jump of the controller’s energy, at the last measured state, so we give the controller one step first. It changes the controller’s own copy: the Param objects keep their values, and a new controller starts from those.

controller = vmc.VMCController(law)
plant = vmc.sim.ModelPlant(arm)
controller.step(plant.t, plant.read())  # the arm at rest, straight
stored = controller.energy()  # [J]
jump = controller.set({"ctrl.reach.stiffness": 1200.0})
stored, jump
(0.36749999323333343, 0.36749999323333343)

Doubling the stiffness doubles the energy of the stretched spring, so the jump equals what it stored. Goals are live Params too. A plain list on either side of - becomes a Param named ref:

other = vmc.Mechanism("ctrl")
other.add("reach", vmc.LinearSpring(tip - [0.0, 0.0, 0.40], 600.0))
list(other.params)
['reach.stiffness', 'reach.s', 'reach.ref']

In a system that is ctrl.reach.ref. A goal that changes while running reads better with a name of its own, given by vmc.Ref("goal", ...) as above: ctrl.reach.goal.

Move the goal

The goal swings from side to side while the arm runs. The loop is the one vmc.sim.run runs (read, step, write, advance; the guard of Your first controller is left out) with one set before each step. The log also records the goal, to draw it below.

controller = vmc.VMCController(law)  # a new controller: 600 N/m again
plant = vmc.sim.ModelPlant(arm)
log = vmc.sim.RunLog()
dt = 1 / 330  # [s]
controller.reset(plant.t, plant.read())  # start from a measured state
for _ in range(round(4.0 / dt)):
    target = [0.12 * np.sin(np.pi * plant.t), 0.0, 0.40]  # [m]
    controller.set({"ctrl.reach.goal": target})
    meas = plant.read()
    plant.write(controller.step(plant.t, meas))
    log.step(t=plant.t, q=meas["q"], goal=target)
    plant.advance(dt)

We follow the tip and the goal from side to side:

rows = log.arrays()
tip_x = [kin.position(q, 1.0)[0] for q in rows["q"]]

fig, ax = plt.subplots()
ax.plot(rows["t"], 100 * rows["goal"][:, 0], label="goal")
ax.plot(rows["t"], 100 * np.array(tip_x), "--", label="tip")
ax.set_xlabel("time [s]")
ax.set_ylabel("$x$ [cm]")
ax.legend(loc="lower left", fontsize=18);
../_images/da3456d34b5c295102765e298ed21ec6a67ffe6398f069fcd5f1bad220aeb26b.svg

The tip follows the goal about 21 ms behind and stays within 2.9 cm of it. The gap is mostly along \(z\): the tip stays short of the goal, where the spring balances the arm’s own stiffness.

Animate

viz.animate calls draw at every frame with the logged values of that step, so the goal and its spring move with the run.

def draw(ax, row):
    viz.draw_spring(ax, kin.position(row["q"], 1.0), row["goal"])
    viz.draw_goal(ax, row["goal"])

viz.animate(arm, log, "parameters.mp4", draw=draw, trace=1.0)
The goal (red cross), moved by controller.set at every step, and the tip following it on its spring.