Low-Level SQP API¶
Real-time trajectory optimization via Sequential Quadratic Programming. Use this when you need sub-10ms optimizer steps for online replanning — lower-level than TaskComposer, higher-control than plan_cartesian.
When to reach for this¶
| Situation | Use |
|---|---|
| Offline, one-shot plan | plan_freespace / plan_cartesian |
| Multi-stage pipeline (sampling → optimize → time-param) | TaskComposer |
| Online replanning with moving obstacle, ~100 Hz | Low-level SQP (this page) |
Measured step rates for a reference 8-DOF gantry problem are printed at runtime by
online_planning_sqp_example.py — run it locally to see numbers on your machine.
Modules¶
| Module | Purpose |
|---|---|
tesseract_robotics.trajopt_ifopt |
Variables (Var, Node, NodesVariables), constraints (joint, Cartesian, collision), factory helpers |
tesseract_robotics.trajopt_sqp |
IfoptProblem (NLP), IfoptQPProblem (QP wrapper), TrustRegionSQPSolver, OSQPEigenSolver |
The standalone tesseract_robotics.ifopt module was removed in 0.34.
All types merged into tesseract_robotics.trajopt_ifopt. See the
0.33 → 0.34 migration guide for details.
Variable Hierarchy (0.34+)¶
The JointPosition class is gone. Variables now use a three-level hierarchy:
Var— a single variable (e.g., joint values at one waypoint)Node— groups multipleVars (e.g., joints + velocities at one waypoint)NodesVariables— container ofNodes passed to the problem constructor
Build the full hierarchy with createNodesVariables:
from tesseract_robotics.trajopt_ifopt import Bounds, createNodesVariables
bounds = Bounds(-3.14, 3.14)
nodes_variables = createNodesVariables(
"trajectory", joint_names, initial_states, bounds,
)
Get Var references for passing to constraints:
Problem Setup¶
import sys
import time
import numpy as np
# Low-level SQP imports
from tesseract_robotics import trajopt_ifopt as ti
from tesseract_robotics import trajopt_sqp as tsqp
from tesseract_robotics.planning import Robot
from tesseract_robotics.tesseract_common import Isometry3d
TesseractViewer = None
if "pytest" not in sys.modules:
from tesseract_robotics.viewer import TesseractViewer
Constraints and Costs¶
The build_optimization_problem function shows the full 0.34 pattern — NodesVariables
factory, IfoptProblem (NLP) for joint/Cartesian constraints, then IfoptQPProblem(nlp)
wrapping for collision constraints, followed by problem.setup():
def build_optimization_problem(
robot, joint_names, start_pos, target_pos, steps=10, use_continuous_collision=True
):
"""Build the IFOPT optimization problem using 0.34 Var/Node API.
Args:
robot: Robot instance with environment
joint_names: List of joint names
start_pos: Starting joint positions
target_pos: Target joint positions
steps: Number of trajectory waypoints
use_continuous_collision: If True, use LVS continuous collision.
If False, use discrete single-timestep collision.
Returns:
dict with problem and all objects that must stay alive
"""
# Get kinematic group and joint limits
manip = robot.env.getKinematicGroup("manipulator")
joint_limits = manip.getLimits().joint_limits
# Interpolate initial trajectory
initial_states = ti.interpolate(start_pos, target_pos, steps)
bounds = ti.toBounds(joint_limits)
# Create NodesVariables — factory handles Node/Var creation on C++ side
nodes_variables = ti.createNodesVariables(
"trajectory", list(joint_names), list(initial_states), bounds
)
nlp = tsqp.IfoptProblem(nodes_variables)
# Get Var references for constraints
vars_list = []
for node in nodes_variables.getNodes():
vars_list.append(node.getVar("joints"))
# Add start position constraint (first waypoint = current position)
home_coeffs = np.ones(len(joint_names)) * 5.0
home_constraint = ti.JointPosConstraint(start_pos, vars_list[0], home_coeffs, "Home_Position")
nlp.addConstraintSet(home_constraint)
# Add target pose constraint (last waypoint = target in Cartesian space)
target_tf = manip.calcFwdKin(target_pos)["tool0"]
target_constraint = ti.CartPosConstraint(
vars_list[-1],
manip,
"tool0", # source_frame
"world", # target_frame
Isometry3d.Identity(), # source_frame_offset
target_tf, # target_frame_offset
"Target_Pose",
)
nlp.addConstraintSet(target_constraint)
# Add velocity cost (smooth motion)
vel_target = np.zeros(len(joint_names))
vel_constraint = ti.JointVelConstraint(vel_target, vars_list, np.ones(1), "JointVelocity")
nlp.addCostSet(vel_constraint)
# Create QP problem from NLP
problem = tsqp.IfoptQPProblem(nlp)
# Add collision constraints
margin = 0.1 # 10cm safety margin
margin_coeff = 10.0
collision_config = ti.TrajOptCollisionConfig(margin, margin_coeff)
collision_config.collision_margin_buffer = 0.10
collision_evaluators = []
collision_constraints = []
if use_continuous_collision:
for i in range(1, steps):
collision_evaluator = ti.LVSDiscreteCollisionEvaluator(
manip,
robot.env,
collision_config,
True,
)
collision_constraint = ti.ContinuousCollisionConstraint(
collision_evaluator,
vars_list[i - 1],
vars_list[i],
False,
False,
1,
False,
f"LVSCollision_{i - 1}_{i}",
)
problem.addConstraintSet(collision_constraint)
collision_evaluators.append(collision_evaluator)
collision_constraints.append(collision_constraint)
else:
for i in range(1, steps):
collision_evaluator = ti.SingleTimestepCollisionEvaluator(
manip,
robot.env,
collision_config,
True,
)
collision_constraint = ti.DiscreteCollisionConstraint(
collision_evaluator, vars_list[i], 1, False, f"Collision_{i}"
)
problem.addConstraintSet(collision_constraint)
collision_evaluators.append(collision_evaluator)
collision_constraints.append(collision_constraint)
# Setup the problem (must be called after adding all sets)
problem.setup()
return {
"problem": problem,
"nlp": nlp,
"nodes_variables": nodes_variables,
"vars_list": vars_list,
"target_constraint": target_constraint,
"home_constraint": home_constraint,
"vel_constraint": vel_constraint,
"collision_config": collision_config,
"collision_evaluators": collision_evaluators,
"collision_constraints": collision_constraints,
"manip": manip,
}
Available constraint types in tesseract_robotics.trajopt_ifopt:
| Constraint | Purpose |
|---|---|
JointPosConstraint |
Target joint values at specific waypoints |
JointVelConstraint |
Velocity limits across waypoints |
JointAccelConstraint |
Acceleration limits |
JointJerkConstraint |
Jerk (3rd derivative) limits — smoother motion |
CartPosConstraint |
TCP pose at a waypoint |
CartLineConstraint |
TCP on a line segment |
DiscreteCollisionConstraint |
Collision at single timesteps |
DiscreteCollisionNumericalConstraint |
Alt. collision jacobian |
ContinuousCollisionConstraint |
Collision across segments (LVS) |
InverseKinematicsConstraint |
IK-based optimization |
Available collision evaluators:
| Evaluator | Pairs with |
|---|---|
SingleTimestepCollisionEvaluator |
DiscreteCollisionConstraint |
LVSDiscreteCollisionEvaluator |
ContinuousCollisionConstraint (LVS discrete) |
LVSContinuousCollisionEvaluator |
ContinuousCollisionConstraint (LVS continuous) |
SQP Solver Loop¶
The solver loop: initial global solve, then per-tick setVariables + stepSQPSolver
for warm-started incremental optimization. setBoxSize resets the trust region each
tick (trust-region shrinking can drive the box to zero):
# Create SQP solver with OSQP
# Match C++ defaults: box_size=0.01
box_size = 0.01
qp_solver = tsqp.OSQPEigenSolver()
solver = tsqp.TrustRegionSQPSolver(qp_solver)
solver.verbose = verbose
solver.params.initial_trust_box_size = box_size
# Initial global solve
print("Running initial global solve...")
t0 = time.perf_counter()
solver.solve(problem)
solve_time = time.perf_counter() - t0
x = solver.getResults().best_var_vals
cost = problem.getTotalExactCost()
print(f"Initial solve: cost={cost:.4f}, time={solve_time * 1000:.1f}ms")
trajectories = [x.copy()]
timings = [solve_time]
# Online replanning loop
num_replan = 10
print(f"\nOnline replanning ({num_replan} iterations)...")
n_joints = len(joint_names)
for iteration in range(num_replan):
# Move obstacle
human_x = 0.5 + 0.3 * np.sin(iteration * 0.3)
update_human_position(robot, human_x, 0.0)
# Extract joint values from solution
joint_vals = x[: steps * n_joints]
traj = joint_vals.reshape(steps, n_joints)
# Rebuild problem with warm-start
t0 = time.perf_counter()
problem_data = build_optimization_problem(
robot,
joint_names,
traj[0],
target_pos,
steps,
use_continuous_collision,
)
problem = problem_data["problem"]
# Set warm-start values — flatten trajectory into decision vector
problem_data["nodes_variables"].setVariables(traj.flatten())
# Single SQP step (incremental optimization)
solver.init(problem)
solver.stepSQPSolver()
# Reset box size (trust region loop can shrink it to zero)
solver.setBoxSize(box_size)
x = solver.getResults().best_var_vals
dt = time.perf_counter() - t0
timings.append(dt)
trajectories.append(x.copy())
if verbose:
cost = problem.getTotalExactCost()
print(f" Iter {iteration + 1}: cost={cost:.4f}, time={dt * 1000:.1f}ms")
Warm-Starting¶
Between solver steps, update NodesVariables with the previous solution:
nodes_variables.setVariables(trajectory.flatten())
solver.init(problem)
solver.stepSQPSolver()
The example above rebuilds the problem each tick because the obstacle pose is baked
into collision constraints. If your scene is static, you can reuse the same problem
and only call setVariables + stepSQPSolver.
Python Subclassing¶
You can subclass ConstraintSet from Python to define custom constraints — the
trampoline landed in 0.34.1.1 (see the
CHANGELOG).
scipy is required at runtime because nanobind's sparse-matrix conversion
goes through scipy.sparse.
Migrating from 0.33¶
If you have 0.33 SQP code, see the full migration guide — critical items:
from tesseract_robotics import ifopt→from tesseract_robotics import trajopt_ifoptJointPosition(...)→createNodesVariables(...)+VarrefsIfoptQPProblem()→IfoptProblem(nodes_variables)+IfoptQPProblem(nlp)CartPosInfostruct removed —CartPosConstrainttakes args directlyCollisionCacheremoved — caching is internalgetTotalExactCost()/getExactCosts()take no argumentsgetStaticKey()→getKey()(instance method on profiles)- Call
problem.setup()after adding all constraint/cost sets
See also¶
online_planning_sqp_example.py— full working example- Online Planning examples
- 0.33 → 0.34 Migration Guide