Skip to content

Python API Quick Start

Use skelarm as a library. Everything shown here is importable from the top-level skelarm package; the per-module details live in the API Reference (start at Architecture).

Build a robot and compute kinematics

From a config file, or directly in code:

import numpy as np

from skelarm import LinkProp, Skeleton, compute_forward_kinematics

skeleton = Skeleton.from_toml("examples/four_dof_robot.toml")  # from TOML

link = LinkProp(length=1.0, m=1.0, i=0.1, rgx=0.5, rgy=0.0, qmin=-np.pi, qmax=np.pi)
skeleton = Skeleton([link])  # or programmatically

skeleton.q = np.array([0.5])  # setters clamp into the joint limits (and warn)
compute_forward_kinematics(skeleton)
tip = skeleton.links[-1]
print(tip.xe, tip.ye)

Run a scenario headlessly

The highest-level entry point mirrors the GUI tools — one config, one call:

from skelarm import load_scenario, rerun_log, run_scenario

scenario = load_scenario("examples/reach.toml")
log = run_scenario(scenario)  # duration from [task], dt from [simulator]
log.save("reach.sklog.npz")

again = rerun_log(log)  # deterministic re-simulation from the embedded config

The returned StateLog gives channel arrays for analysis (log.channel("q"), log.times) and replays in tools/player.py — see Record, Replay, and Re-simulate.

Simulate uncontrolled dynamics

simulate_robot integrates the free dynamics with adaptive solve_ivp, taking a torque callback f(t, skeleton) -> tau:

import numpy as np

from skelarm import LinkProp, Skeleton, simulate_robot

link = LinkProp(length=1.0, m=1.0, i=0.1, rgx=0.5, rgy=0.0, qmin=-np.pi, qmax=np.pi)
skeleton = Skeleton([link])
skeleton.q = np.array([0.0])
skeleton.dq = np.array([0.0])


def control_torques(t, skel):
    return np.array([0.0])  # zero torque


times, q_traj, dq_traj = simulate_robot(skeleton, (0.0, 1.0), control_torques)

For a stateful controller (anything with an update hook, MPC in particular), use the fixed-step simulate_controlled loop instead — adaptive solve_ivp may call the torque callback several times per output interval, which breaks controller state.

simulate_controlled can also script a disturbance at the tip: external_force is called as f(t, skeleton) -> (fx, fy) once per step, and its force acts on top of the controller's torque (mapped to the joints as J^T F, like the mouse force of the dynamics simulator). The force is recorded as the ext_force channel, so tools/player.py replays it as an arrow at the tip:

import numpy as np

from skelarm import ComputedTorque, LinkProp, Skeleton, Trajectory, compute_jacobian, simulate_controlled

upper_arm = LinkProp(length=1.0, m=1.0, i=0.1, rgx=0.5, rgy=0.0, qmin=-np.pi, qmax=np.pi)
forearm = LinkProp(length=0.8, m=0.8, i=0.05, rgx=0.4, rgy=0.0, qmin=-np.pi, qmax=np.pi)
skeleton = Skeleton([upper_arm, forearm])
skeleton.q = np.array([0.3, 1.5])
reference = Trajectory(skeleton.q, skeleton.q + np.array([0.4, -0.3]), duration=1.0)
controller = ComputedTorque(reference, kp=100.0, kd=20.0)


def push(t, skel):  # a 20 N push along +x from 0.4 s to 0.5 s
    return np.array([20.0, 0.0]) if 0.4 <= t < 0.5 else np.zeros(2)


held = np.array([skeleton.links[-1].xe, skeleton.links[-1].ye])  # where the tip starts


def grip(t, skel):  # a stiff hand holding the tip where it started
    tip = np.array([skel.links[-1].xe, skel.links[-1].ye])
    return -5000.0 * (tip - held) - 100.0 * (compute_jacobian(skel) @ skel.dq)


pushed = simulate_controlled(skeleton, controller, duration=2.0, dt=0.002, external_force=push)
gripped = simulate_controlled(skeleton, controller, duration=2.0, dt=0.002, external_force=grip)

Lower-level building blocks

from skelarm import (
    compute_forward_dynamics,  # tau -> ddq (mass matrix solve)
    compute_inverse_dynamics,  # motion -> tau (Recursive Newton-Euler)
    compute_inverse_kinematics,  # endpoint target -> q (check result.success)
    compute_jacobian,  # endpoint Jacobian
    ik_joint_reference,  # task path -> joint reference (warns on deviation)
    simulate_controlled,  # fixed-step control loop
)

Conventions to keep in mind:

  • No gravity — the arm is horizontal; the grav_vec parameter on the dynamics functions is an advanced/testing hook outside the supported model.
  • Joint limits — kinematic setters clamp and warn; the fixed-step integrators apply hard stops; simulate_robot is unconstrained (Joint Limits).
  • Radians internally — TOML configs use degrees, the API uses radians.
  • Arrays are numpy.typing.NDArray[np.float64] throughout.