Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Python (maturin)

The caliper-py face builds the engine into a native Python extension with maturin / PyO3, so you can import caliper and script the engine like NumPy/MATLAB. This is also the surface the oracle runs through — validating FK, Jacobians, RNEA, CRBA, forward dynamics, and singularity metrics against Pinocchio/NumPy exercises the shipped Python bindings, not a private test path.

Build

python -m venv .venv && source .venv/bin/activate
pip install maturin
maturin develop -m crates/caliper-py/Cargo.toml

(In this repo the convention is env -u CONDA_PREFIX .venv/bin/maturin develop -m crates/caliper-py/Cargo.toml, building into the repo .venv.)

Use

import caliper

robot = caliper.Robot.from_urdf("robot.urdf")
res   = robot.ik(target, seed)   # target: a 4x4 column-major pose; seed: joints
pose  = robot.fk(res["q"])       # ik returns {success, q, residual, iters, …}

Beyond Robot, the bindings expose Planner, ControlLoop (with step_with_target and last_warn), Recorder, DatasetReader, and a run_graph entry point for the dataflow graph — this is the surface the learning sidecar builds on. The full surface is typed in crates/caliper-py/python/caliper/__init__.pyi; the capability matrix maps every function to its engine capability.

Doctors & lint

The three diagnostic engines (see Doctors & trajectory lint) are plain functions returning plain data — findings never raise; only an uninspectable input does:

import caliper

# Asset doctor: A001–A016 over a URDF/xacro. Findings are dicts with
# {code, severity ("error"|"warning"|"info"), message, fix_hint, auto_fixable}.
rep = caliper.doctor("robot.urdf")
assert rep["clean"] or rep["errors"] == 0

# repair=True writes a repaired COPY (default <input>.repaired.urdf; the
# input is never modified) and reports {out, applied, skipped, mesh_copies}.
rep = caliper.doctor("robot.urdf", repair=True, density=2700.0)
fixed = rep["repair"]["out"]
assert caliper.doctor(fixed)["clean"]          # findings describe the ORIGINAL

# Dataset doctor: D001–D016 over a LeRobotDataset v3.0 root. Also returns the
# recomputed per-feature stats {dim, mean, std, min, max, bin_occupancy}.
dr = caliper.data_doctor("~/datasets/pick_place")
for f in dr["findings"]:
    print(f["severity"], f["code"], f["message"])

# Trajectory lint: T001–T007 over sampled rows (exactly what
# Trajectory.sample_uniform returns); [] means the trajectory lints clean.
robot = caliper.Robot.from_urdf("robot.urdf")
goal = [0.5] * robot.ndof
traj = robot.move_j([0.0] * robot.ndof, goal)
times, q, qd, qdd = traj.sample_uniform(0.01)
findings = caliper.lint_path(robot, times, q, qd, qdd)

(The collision half of the lint, T008/T009, is CLI-only — caliper report.)

Pose convention: unified

Every pose-accepting entry point (Robot.ik / analytic_ik / move_l / move_c, Planner.plan_to_pose, ReachChecker.status / reachable, calibrate_joint_offsets) takes the same input: a 4×4 column-major nested list (or an equivalent flat 16-element column-major list), and frame arguments accept a name everywhere (an integer index is still accepted where it historically was, now bounds-checked).

One legacy form is grandfathered for back-compat: Planner.plan_to_pose also accepts its original flat 12-element row-major pose (9 rotation entries then tx, ty, tz). New code should use the 4×4 form.

fk output is NOT ik input. Robot.fk (and exp6) return 4×4 ROW-major nested lists, while every pose input above is COLUMN-major — so robot.ik(robot.fk(q), seed) does not round-trip: the bytes silently parse as the transpose (rotation inverted, translation read as [0, 0, 0]) and IK "solves" a wrong target with no error raised. Transpose at the boundary: robot.ik(np.array(robot.fk(q)).T.tolist(), seed). Full table + details: Pose forms.