Build Paths and Constraints¶
Path Options¶
Use Path.from_waypoints_interpolating when your path is a sequence of
waypoints that it must pass through exactly. Path.from_waypoints is an
equivalent alias with the same arguments. The default Python matrix layout is
sample_major:
waypoints.shape == (n_points, dim)
Use Path.from_waypoints_fitting when the waypoints are noisy samples or a
dense programmed polyline that only needs to be followed within a tolerance.
It fits an adaptive nonuniform C4 quintic B-spline and does not pass through
interior waypoints: each selected axis may deviate from the piecewise-linear
reference polyline through the waypoints by at most its tolerance, audited at
the same path parameter over every complete reference interval. Endpoints are
retained, and unselected axes keep interpolating every waypoint. Tolerances
are in each axis’s input units, and a per-axis tolerance array follows the
order of axes:
path = copp.Path.from_waypoints_fitting(
waypoints,
tolerance=[1e-3, 1e-2],
axes=[0, 3], # 0-based path dimensions
)
report = path.smoothing_report
assert (report.max_errors <= [1e-3, 1e-2]).all()
path.smoothing_report is None for paths that were not fitted. Fitting
failures, such as a tolerance that cannot be met within max_segments,
raise PathError.
Note
Both waypoint constructor families are unstable: their names, signatures,
and configuration types (SplineConfig and SmoothingConfig) may
change as more waypoint algorithms are added.
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.
When that profile comes from a third-order solve, also pass
b_linearization=profile.b. Each jerk row is then anchored where it is
nearly active at the feasible state (profile.a, profile.b) instead of
directly at profile.a. This is recommended when a spans a wide range
and especially when it can approach zero: anchoring both rows of one axis at
the same tiny a_linearization[k] cancels b and c and leaves
a[k] <= 3 * a_linearization[k]. b_linearization must have the same
length as a_linearization and must come from the same third-order profile;
a TOPP2 profile’s b is discretized differently and must not be used. On
profiles well away from zero, the default direct linearization is cheaper and
nearly identical.
problem = copp.solver.topp3_lp.Problem(
robot.constraints,
profile.a,
a_boundary=(0.0, 0.0),
b_boundary=(0.0, 0.0),
b_linearization=profile.b,
)
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:
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.