Build Paths and Constraints

Path Options

Use Path.from_waypoints when your path is a sequence of waypoints. The default Python matrix layout is sample_major:

waypoints.shape == (n_points, dim)

Use Path.from_evaluator_2nd or Path.from_evaluator_3rd when you already have analytic derivatives. The evaluator receives a one-dimensional float64 array of path parameters and returns contiguous arrays in the selected layout.

Use Path.from_jax, Path.from_autograd, Path.from_casadi, or Path.from_sympy when you want Python to differentiate a scalar-parametric function or symbolic expression. These optional backends are imported lazily only when the corresponding constructor is called.

Path Evaluation

Path evaluation methods return a consistent PathDerivatives object rather than switching between arrays and tuples. For position-only evaluation, evaluate_q fills only the q field:

out = path.evaluate_q(s)
q = out.q
assert out.dq is None
assert out.ddq is None
assert out.dddq is None

Higher-order calls fill the corresponding derivative fields on the same result type:

out = path.evaluate_up_to_2nd(s)
q = out.q
dq = out.dq
ddq = out.ddq

Robot vs Constraints

Robot owns station samples, sampled path derivatives, optional inverse dynamics, and a raw Constraints buffer. It is the recommended entry point for most applications:

robot = copp.Robot(dim, capacity=n)
robot.append_s(s)
robot.set_q_from_path_3rd(path, 0, n)
robot.add_velocity_limits(upper, lower, start_idx_s=0, length=n)
robot.add_acceleration_limits(upper, lower, start_idx_s=0, length=n)
robot.add_jerk_limits(upper, lower, start_idx_s=0, length=n)

Constraints can be constructed directly when you already have path-domain rows and do not need robot derivative storage or torque objectives.

Problem Descriptors Reference Live Buffers

Solver-specific Problem classes keep references to the data they are built from. A TOPP problem built from robot.constraints continues to see that same constraint buffer; a COPP problem built from robot continues to see that same robot, including later constraint and path-sample changes.

This is useful for workflows that update limits or station windows, but it also means a problem is not a frozen snapshot:

problem = copp.solver.topp2_ra.Problem(
    robot.constraints,
    idx_s_interval=(0, len(robot) - 1),
    a_boundary=(0.0, 0.0),
)

robot.add_velocity_limits(new_upper, new_lower, start_idx_s=0, length=len(robot))
problem.validate()  # validates against the updated constraints

The problem’s own configuration is immutable. Build a new problem when changing the interval, boundary values, objective list, or third-order a_linearization.

Third-order problem construction and validation also refresh cached affine jerk rows in the referenced constraint buffer. This mirrors Rust build_with_linearization(): nonlinear jerk constraints are left intact, and the linearized rows are regenerated from the problem’s copied a_linearization. To refine a third-order solution, construct a new problem with the new profile.a.

Limit Shapes

One-dimensional limits have shape (dim,) and are broadcast across the station interval. Two-dimensional limits must match the station interval and selected layout. With the default sample_major layout:

limits.shape == (length, dim)

The lower limit arrays should be negative for symmetric bounds such as [-1, 1]. The upper arrays should be positive.

Inverse Dynamics

If no inverse-dynamics callback is installed, torque methods use point-mass dynamics:

\[\tau = \ddot{q}.\]

For robot-specific torque limits or torque objectives, construct Robot with a callable:

def inverse_dynamics(q, dq, ddq):
    return tau

robot = copp.Robot(dim, capacity=n, inverse_dynamics=inverse_dynamics)

The callback receives contiguous one-dimensional float64 arrays of shape (dim,) and must return a value convertible to the same shape.