Experiments in files

In this tutorial we write an experiment on the soft arm as a YAML file, run it, let its goal and its controller change on a schedule, and save it back with a new stiffness.

The file

A configuration file describes an experiment in a few sections. robot names a robot template and its arguments. coordinates names coordinates that the controllers share. controller lists the virtual elements: each names its registered type, the coordinate it acts on and its gains. This one is like the controller of your first controller, with another goal:

# The soft arm on its side: a spring walks its tip through two goals,
# then a gentler spring, never pulling more than 0.5 N, takes over.
robot:
  template: helyx.arm
  geometry: "145-145-145"
coordinates:
  tip: {point: {s: 1.0}}
controller:
  elements:
    reach:
      type: linear_spring
      coordinate:
        difference: [tip, {ref: {name: goal, value: [0.0, 0.0, 0.435]}}]
      stiffness: 300.0
    damp: {type: linear_damper, coordinate: tip, damping: 5.0}
    gravity: {type: gravity_compensation}

A coordinate is a name from coordinates, or a mapping of one kind to its arguments, written as in Python, for instance point ({s: 1.0}, a site’s name, or {at, s, offset}), joint (an index, a list, or {start, stop}), ref (a live reference with its name and value), difference, projection ({of, direction}), norm, slice ({of, index}), stack, plane_distance, sphere_distance and state (the full list is at the end of this page). A plain list in a difference is a live reference named ref, as tip - [0.1, 0.0, 0.40] is in Python.

Run it

The file is reach.yaml: download it into the folder where you run the code. In the file the sections come in this order: robot, coordinates, controller, swaps and experiment. This page shows experiment before swaps, so keep the file’s order if you type it. vmc.config.load reads the file and builds the robot from its template, the controllers and the plant. run runs the experiment from its start and returns the log. With run settings in the file, it also saves the log, as Run logs shows:

import numpy as np
import matplotlib.pyplot as plt
import virtualmodelcontrol as vmc

experiment = vmc.config.load("reach.yaml")
rows = experiment.run().arrays()
sorted(rows)
['law_torque',
 'motor_position',
 'motor_torque',
 'motor_velocity',
 'q',
 't',
 'v']

experiment.robot, experiment.mechanism and experiment.controller are the objects the file describes, the same ones your first controller builds in Python. So the other tutorials apply to them unchanged.

Change values on a schedule

The experiment section says how to run it: the plant (here a simulation with the arm’s own stiffness, damping and gravity from helyx.add_dynamics), the control rate [Hz], the duration [s] and a schedule. An output list adds output stages to the controller, such as the real arm’s pretension {type: helyx.output_stage}. This file has none:

experiment:
  plant:
    type: simulation
    dynamics: helyx.add_dynamics
  rate: 330
  duration: 6.0
  schedule:
    - param: ctrl.reach.goal
      points:
        - [0.5, [0.0, 0.0, 0.435]]
        - [1.5, [0.10, 0.0, 0.40]]
        - [3.0, [-0.10, 0.0, 0.40]]
    - {swap: gentle, at: 4.0, duration: 1.0}

A schedule entry moves one live Param through its points: pairs of a time [s] from the start and a value. The Param is named as in controller.set (Parameters): the mechanism, the element and the Param, so ctrl.reach.goal is the goal of the element reach of the controller section (a swap’s mechanism is named by its key, as gentle). Between points the value moves in a straight line, or jumps with interpolation: step. Before the first point the Param keeps its own value, and after the last it holds the last one. Here the goal waits half a second, then walks to one side and to the other.

A swap entry blends to another controller over its duration, with the quintic blend of vmc.control.SwapController. swaps lists the controllers to swap to, each named by its key and with the same elements as controller:

swaps:
  gentle:
    elements:
      reach:
        type: tanh_spring
        coordinate: {difference: [tip, [0.0, 0.0, 0.40]]}
        stiffness: 300.0
        max_force: 0.5
      damp: {type: linear_damper, coordinate: tip, damping: 5.0}
      gravity: {type: gravity_compensation}

The gentle controller’s spring pulls the tip towards the middle with a force of at most 0.5 N on each axis. We follow the tip and the goal along \(x\) and \(z\):

from virtualmodelcontrol.control import Schedule

kin = vmc.Kinematics(experiment.robot)
tip = np.array([kin.position(q, 1.0) for q in rows["q"]])
t = rows["t"].ravel()
walk, swap = experiment.spec["experiment"]["schedule"]
path = Schedule("goal", walk["points"])
start = np.array([0.0, 0.0, 0.435])  # [m], the goal before the walk
middle = np.array([0.0, 0.0, 0.40])  # [m], the gentle spring's goal


def goal_at(time):  # [m], the goal pulling the tip at a time [s]
    if time >= swap["at"]:
        return middle
    return path.value(time) if time >= walk["points"][0][0] else start


goal = np.array([goal_at(x) for x in t])
walking = (t >= walk["points"][0][0]) & (t < swap["at"])

fig, axes = plt.subplots(2, 1, figsize=(6.4, 6.4), sharex=True)
for ax, k, axis in zip(axes, (0, 2), "xz"):
    ax.axvspan(swap["at"], swap["at"] + swap["duration"], color="0.9")
    ax.plot(t, 100 * goal[:, k], label="goal")
    ax.plot(t, 100 * tip[:, k], "--", label="tip")
    ax.set_ylabel(f"${axis}$ [cm]")
axes[0].legend(loc="upper right", fontsize=18)
axes[1].set_xlabel("time [s]");
../_images/c20afb78eb6fa2fc3677b12b0bd2a1c8b457e972e6adec14b2536489735ab318.svg

The tip follows the walking goal within 0.7 cm along \(x\). Along \(z\) it stays above the goal, held by the arm’s own stiffness, as in your first controller. From 4 s the gentle controller takes over within a second. Its spring brings the tip back to the middle along \(x\), but with 0.5 N it cannot shorten the arm: the tip ends 3.5 cm above its goal.

from virtualmodelcontrol import viz

def draw(ax, row):
    viz.draw_goal(ax, goal_at(row["t"]))

viz.animate(experiment.robot, rows, "configurations.mp4", draw=draw,
            trace=1.0)
The goal (red cross) walks from side to side and the tip follows it; from 4 s the gentle controller pulls the tip towards the middle.

Save it back

save writes the configuration with the current values of the controllers’ Params, so a value changed in Python reaches the file. Here the spring gets twice the stiffness:

experiment.mechanism.components["reach"].stiffness.value = 600.0  # [N/m]
experiment.save("reach-stiffer.yaml")
stiffer = vmc.config.load("reach-stiffer.yaml")
stiffer.spec["controller"]["elements"]["reach"]
{'type': 'linear_spring',
 'coordinate': {'difference': ['tip',
   {'ref': {'name': 'goal', 'value': [0.0, 0.0, 0.435]}}]},
 'stiffness': 600.0}
tip = np.array([kin.position(q, 1.0) for q in stiffer.run().arrays()["q"]])
gap = 100 * np.abs(tip[walking, 0] - goal[walking, 0]).max()  # [cm], x
float(gap)
0.5633642819303175

The stiffer spring follows the goal more closely. save writes the file anew, without the comments of the original. It saves the robot as its template call. controller.set changes only the running controller’s own copy of a value (Parameters), so only the values of the Params themselves reach the file.

What a file can name

Every name in a file comes from the library’s registry, so a project adds its own robots, elements or coordinate kinds by registering them (Extend the library). These are the library’s own:

  • A robot’s template: adapt.finger, adapt.hand, bimanual.arms, helyx.arm, planar.arm, planar.continuum, turtle.robot, ur5.arm, ur5.with_hand.

  • A simulation’s dynamics: adapt.add_dynamics, bimanual.add_dynamics, helyx.add_dynamics, planar.add_continuum_dynamics, planar.add_dynamics.

  • An output stage: adapt.hand_output_stage, adapt.output_stage, bimanual.output_stage, friction_compensation, helyx.output_stage, helyx.tension_stage, pretension, static_friction_compensation, tension_floor, torque_limit, torque_offset.

  • A controller’s template: turtle.controller.

  • z0, the virtual state from the first reading: turtle.initial_state.

  • An element’s type: adapt.hand_joint_limit_spring, adapt.joint_limit_spring, constrained_gaussian_spring, constrained_linear_damper, constrained_linear_spring, constrained_tanh_damper, constrained_tanh_spring, contact_damper, contact_friction, contact_spring, diode_damper, force_source, gaussian_spring, gravity, gravity_compensation, helyx.unwind_limit, inertance, limit_spring, linear_damper, linear_spring, phase_spring, point_mass, polynomial_spring, rotational_inertia, sigmoid_spring, speed_regulator, tanh_damper, tanh_spring.

  • A coordinate’s kind: box_distance, capsule_distance, cylinder_distance, difference, from_frame, in_frame, joint, norm, orientation_error, plane_distance, point, projection, ref, rotation, slice, sphere_distance, stack, state, sum.