Skip to content

Control Configuration

A scenario file describes a complete controlled run in one TOML document: the robot, its start state, the reaching task, and the controller that drives it. It adds [task] and [controller] sections on top of the Robot Configuration ([skeleton] / [initial]), and is loaded by skelarm.load_scenario.

uv run python tools/reaching_simulator.py examples/reach.toml             # interactive reach GUI (drag to perturb)
uv run python tools/reaching_simulator.py examples/reach.toml --save reach.sklog.npz   # headless batch run + log
uv run python tools/player.py reach.sklog.npz                # replay and analyze a saved run

A scenario combines four sections:

Section Purpose Loader
[skeleton] Robot geometry (links, base length, limits) Skeleton.from_toml
[initial] Start pose / velocity (degrees) applied by Skeleton.from_toml
[task] The goal and how the motion is shaped Task.from_toml
[simulator] How the dynamics are integrated (dt, enforce_limits) Simulator.from_dict
[controller] The control law and its gains build_controller

See Trajectory Tracking Control and Reaching Control for the underlying theory.

The [task] section

Key Type Default Meaning
type str required Task kind — the discriminator that decides what else the task needs. Only reaching is built in; more are planned (and you can add your own).
target [x, y] or table required for reaching Endpoint goal in meters (see below). Other task types may omit it.
duration float 2.0 Total simulated time / planned-motion horizon, in seconds.
schedule str "minimum_jerk" Time scaling for planned trajectories: linear, cubic, quintic, or minimum_jerk.

type is the one always-required key: it names the kind of task, and the kind determines the rest (reaching requires a target). duration, schedule, and target define the planned task-space trajectory used by the trajectory-tracking controllers; the reaching controllers use only target and generate motion online. The numerical-integration parameters (dt, enforce_limits) live in their own [simulator] table, not here.

Target

target is either a plain [x, y] array (position only) or a table that carries the position plus optional attributes:

Target key Type Default Meaning
pos [x, y] required Endpoint position in meters.
label str none Name for the target (used to pick one among several; multi-target tasks come later).
color str "purple" Marker color (any Qt/SVG name or #rrggbb).
tolerance float none Admittable tip-to-target distance (m). The reach is "reached" within it, and the marker's hollow ring is drawn at this radius.
[task]
type = "reaching"
target = { pos = [0.55, 1.21], label = "goal", color = "purple", tolerance = 0.02 }
# array shorthand (position only) also works:
# target = [0.55, 1.21]

Task types

type selects the task kind. Beyond reaching, two trajectory-tracking types track a reference loaded from a .sklog.npz file (e.g. one recorded with tools/trajectory_recorder.py):

type Reference Tracked by
reaching A planned point-to-point reach to target. any controller
multi_target_reaching Several candidate targets; the active one is reached (switchable live in the GUI). any controller
periodic_curve A closed task-space curve traced repeatedly, converted to joint angles by IK. trajectory-tracking controllers
trajectory_tracking The recorded tip (x, y) path, converted to joint angles by IK. trajectory-tracking controllers
joint_trajectory_tracking The recorded per-joint q(t) series directly (no IK). joint-space controllers

Each task type has a dedicated interactive simulator — tools/reaching_simulator.py, tools/multi_target_simulator.py, tools/periodic_curve_simulator.py, and tools/trajectory_tracking_simulator.py — sharing the same controls (drag to apply disturbance forces, Record / Export…, the --initial / --pose / --task / --controller overrides, and --save for a headless batch). Their runs replay in tools/player.py, which draws the task overlay (target, curve, or reference).

A periodic_curve task names a curve kind plus its parameters and a period (one loop); duration sets how many loops are traced. Built-in curves:

curve Parameters Shape
circle center, radius, phase a circle
ellipse center, a, b, phase an axis-aligned ellipse
lemniscate center, a the Bernoulli ∞ (horizontal)
vertical_lemniscate center, a, b an upright figure-eight
rose center, a, k a rhodonea (k petals if odd, 2k if even)
[task]
type = "periodic_curve"
curve = "rose"
center = [0.8, 0.0]
a = 0.4
k = 3
period = 4.0     # seconds per loop
duration = 12.0  # three loops

A multi_target_reaching task lists several candidate targets (each a [x, y] or a { pos, label, color, tolerance } table) and an active index (default 0). The active target flows through the ordinary reaching pipeline; tools/multi_target_simulator.py draws all candidates and switches the active one live when you press a number key (1..N), retargeting a reaching controller (e.g. virtual_spring_damper) on the fly.

[task]
type = "multi_target_reaching"
active = 0
targets = [
  { pos = [1.2, 0.4], label = "A", tolerance = 0.03 },
  { pos = [0.3, 1.2], label = "B", color = "teal" },
]

The trajectory-tracking types take these extra [task] keys:

Key Type Default Meaning
file str required Path to the reference .sklog.npz.
filter table none Pre-smoothing (see below): { kind = …, cutoff_hz, order, window, polyorder }.
interpolator str "cubic_spline" Resampling scheme: cubic_spline, linear, or lagrange.

The filter.kind selects the smoother and which keys it needs (zero-phase in every case): none (off); lowpass / butterworth take a cutoff_hz (and Butterworth an order); moving_average / savgol take a window (odd, in samples), and savgol also a polyorder. See the theory chapter.

[task]
type = "joint_trajectory_tracking"
file = "teach.sklog.npz"
filter = { kind = "butterworth", cutoff_hz = 8.0, order = 4 }  # smooth a jaggy recording
interpolator = "cubic_spline"
# duration defaults to the reference's length when omitted

If duration is omitted, it defaults to the reference trajectory's length. The reference content is embedded in the run log, so rerun_log and exported configs reproduce the run without the original file. Curve and DOF rules: a joint_trajectory_tracking reference must have the same joint count as the robot.

Only these are built in. To add a goal that is not a single target point, or a new reference source, see Defining a Task.

The [simulator] section

How the dynamics are integrated, separate from the task (the desired motion). The whole table is optional; an absent [simulator] uses the defaults.

Key Type Default Meaning
dt float 0.002 The fixed control / integration step of the simulation loop (simulate_controlled). It is also the MPC prediction step.
enforce_limits bool true Apply the joint limits as hard stops in the dynamics. Set false to let the limits constrain only the kinematics.
[simulator]
dt = 0.002
enforce_limits = true

enforce_limits is a run condition: with the default true the fixed-step loop pins each joint at its [qmin, qmax] bound (a hard stop); with false the bounds are dropped from the dynamics and apply only to the kinematics (posing and inverse kinematics). Because it lives in the scenario config, the resolved value is embedded in the run's reproduction metadata, so a saved log re-runs with the same choice. The interactive simulators' --no-joint-limits flag overrides it off for a single run (and that resolved value is what gets recorded). See the Joint Limits guide for the underlying mechanics.

The [controller] section

type selects the control law; the remaining keys are its gains (any omitted key falls back to the default below). Diagonal PD gains (kp, kd) and task-space gains (k_task, d_task, c_joint) are isotropic scalars. To plug in a control law of your own, see Defining a Controller.

Trajectory tracking

These build a joint reference by converting the planned task trajectory with inverse kinematics, then track it. See Trajectory Tracking Control.

type Controller Keys (default)
computed_torque ComputedTorque kp (200), kd (30)
inverse_dynamics_pd InverseDynamicsFeedforwardPD kp (100), kd (20)
joint_pd JointPD kp (300), kd (40)

Reaching

These are endpoint-feedback controllers that drive the tip to target without a preplanned trajectory. See Reaching Control.

type Controller Keys (default)
virtual_spring_damper VirtualSpringDamper k_task (150), d_task (25), c_joint (0)
time_varying_stiffness TimeVaryingStiffness k0 (150), alpha (6), zeta1 (0.15), c_joint (0)
online_shaping OnlineReferenceShaping k_task (150), d_task (25), c_joint (0), r (0.5), t1 (0.2), t2 (0.2)
position_dependent_shaping PositionDependentShaping k_task (150), d_task (25), c_joint (0), a (0.01), t1 (0.2), t2 (0.2)
adaptive_shaping AdaptiveReferenceShaping k_task (150), d_task (25), c_joint (0), epsilon (0.01), t_adapt (5.0), t1 (0.2), t2 (0.2)

Model predictive control

type Controller Keys (default)
mpc JointSpaceMPC horizon (6), q_weight (10), dq_weight (1), tau_weight (0.001), terminal_weight (50), tau_max (none), limit_weight (0), max_iter (20)

MPC predicts with the simulation step, so its control interval is [simulator].dt. Re-optimizing every step is expensive at a small dt; use a larger [simulator].dt (for example 0.05) for MPC scenarios.

Reach time vs. settling

For trajectory-tracking controllers the planned motion spans [0, duration], so the endpoint arrives at the target at t = duration. The reaching controllers converge asymptotically, so give duration enough margin for the endpoint to settle.

Example

[skeleton]
base_length = 0.0
[[skeleton.link]]
length = 1.0
mass = 1.0
inertia = 0.1
com = [0.5, 0.0]
limits = [-180.0, 180.0]
[[skeleton.link]]
length = 0.8
mass = 0.8
inertia = 0.05
com = [0.4, 0.0]
limits = [-180.0, 180.0]

[initial]
q = [34.4, 57.3]        # degrees

[task]
type = "reaching"       # the task kind (required)
target = [0.55, 1.21]    # endpoint goal (x, y) in meters (required for reaching)
duration = 2.0
schedule = "minimum_jerk"

[simulator]
dt = 0.002              # control / integration step
enforce_limits = true   # joint-limit hard stop in the dynamics

[controller]
type = "computed_torque"
kp = 200.0
kd = 30.0

Swap the [controller] block to try a different law — for example a compliant, human-like reach:

[controller]
type = "adaptive_shaping"
k_task = 150.0
d_task = 25.0
t_adapt = 5.0

Overriding sections for comparison

tools/reaching_simulator.py can override the [initial], [task], and [controller] sections from separate files, so one base config can be reused across a comparison sweep without editing it. Each override file supplies the named table (e.g. a file with just a [controller] block). With --save PATH the run is headless (no GUI) and the log is written directly, which is convenient for a scripted sweep:

# Same robot and task, different controllers:
uv run python tools/reaching_simulator.py base.toml --controller computed_torque.toml --save ct.sklog.npz
uv run python tools/reaching_simulator.py base.toml --controller mpc.toml             --save mpc.sklog.npz

# Same controller, different tasks:
uv run python tools/reaching_simulator.py base.toml --task near.toml --save near.sklog.npz
uv run python tools/reaching_simulator.py base.toml --task far.toml  --save far.sklog.npz

Without --save, the same overrides configure the interactive GUI instead — e.g. tools/reaching_simulator.py base.toml --controller pd.toml opens the reach GUI driven by the PD controller. --initial FILE replaces the initial pose from a file's [initial] table, and --pose 20,45 then overrides just the joint angles (degrees) — matching the kinematics and dynamics tools. The override values are merged into the scenario, so each saved run's log still embeds its exact (overridden) config for reproduction.

Using it from Python

from skelarm import load_scenario, run_scenario

scenario = load_scenario("examples/reach.toml")
log = run_scenario(scenario)   # uses the task's duration / dt
log.save("reach.sklog.npz")    # replay/analyze with tools/player.py

run_scenario runs the fixed-step control loop (like simulate_controlled) but also embeds the scenario in the log for later reproduction. build_controller can also be called directly with a [controller] mapping, a Task, and a Skeleton, so controllers can be constructed without a file.

Reproducible runs

A log written by run_scenario (and by the interactive scenario simulators) is a self-contained, re-runnable record. It embeds — in the log's [extra] metadata — the original source config (the full [skeleton] / [initial] / [task] / [simulator] / [controller] tables, exactly as loaded), the actual run parameters (duration / dt / grav_vec / enforce_limits), and the skelarm / numpy / scipy versions. enforce_limits records the resolved joint-limit choice, so a --no-joint-limits override is reproduced on re-run even though the source config still reads true.

rerun_log reconstructs the scenario and re-simulates it:

from skelarm import rerun_log
from skelarm.recording import StateLog

log = StateLog.load("reach.sklog.npz")
again = rerun_log(log)   # rebuilds the scenario and re-runs the dynamics

Reconstruction reparses the embedded source config, so identical input gives identical state. The deterministic controllers (PD, computed torque, inverse-dynamics feedforward, and the reaching controllers) reproduce the recorded channels exactly on the same machine. MPC calls scipy.optimize.minimize, which is deterministic but only bit-identical for the same scipy / BLAS build, so an MPC re-run matches within a small numerical tolerance rather than exactly.

Export an editable config for comparison

To tweak parameters and compare, export the embedded config to an editable TOML and re-run it. Because the export is the original config verbatim, re-running it unedited reproduces the run exactly; editing a value gives a controlled variant:

from skelarm import export_scenario_toml, load_scenario, run_scenario
from skelarm.recording import StateLog

export_scenario_toml(StateLog.load("reach.sklog.npz"), "edited.toml")
# ... edit a gain / target / gravity in edited.toml ...
variant = run_scenario(load_scenario("edited.toml"))

From the command line, tools/export_config.py writes the config from a saved log:

uv run python tools/export_config.py reach.sklog.npz --output edited.toml
uv run python tools/reaching_simulator.py edited.toml                       # explore the edited scenario in the GUI
uv run python tools/reaching_simulator.py edited.toml --save edited.sklog.npz   # or re-run it headlessly

What is not captured

A controller built programmatically (not from a config) has no embedded config, so its run is recorded without reproduction metadata and rerun_log (and export_scenario_toml) reject it. Re-running is available for scenarios loaded from TOML.