Motion Planning¶
Motion Planner¶
MotionPlanner(robot_name: str, config: PlannerConfig | None = None, pointcloud: np.ndarray | None = None, base_config: np.ndarray | None = None, constraints: list | None = None, costs: list | None = None)
¶
Motion planner using OMPL + VAMP C++ backend.
All internals are private. The public API accepts and returns only numpy arrays.
For subgroup planners the helper methods :meth:extract_config,
:meth:embed_config, and :meth:embed_path convert between the
reduced DOF space used by the planner and the full 24-DOF body
configuration.
Source code in autolife_planning/planning/motion_planner.py
bounds: tuple[np.ndarray, np.ndarray]
property
¶
(lower, upper) joint limits of the active DOF.
joint_limits: dict[str, tuple[float, float]]
property
¶
{joint name: (lower, upper)} the planner samples in, for every
joint of the full body. :attr:bounds gives the active joints'
ranges as arrays.
joint_names: list[str]
property
¶
Joint names controlled by this planner, in DOF order.
is_subgroup: bool
property
¶
True if this planner controls a subset of the full body.
subgroup_indices: np.ndarray | None
property
¶
Indices of this planner's joints in the full 24-DOF config.
base_config: np.ndarray
property
¶
The 24-DOF stance frozen for joints outside this subgroup.
has_pointcloud: bool
property
¶
True if a pointcloud is currently registered.
set_joint_limits(limits: Mapping[str, tuple[float, float]] | None = None) -> None
¶
Set the joint ranges the planner samples in, by joint name.
Joints left out of limits take the range compiled into the
robot model, so None restores every compiled range. The
ranges cover the full body and are kept across
:meth:set_subgroup. Collision checking does not read them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limits
|
Mapping[str, tuple[float, float]] | None
|
|
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown joint name, or bounds that are not
finite with |
Source code in autolife_planning/planning/motion_planner.py
clear_constraints() -> None
¶
set_constraints(constraints: list) -> None
¶
Replace all constraints: clear existing, then push new ones.
clear_costs() -> None
¶
set_costs(costs: list) -> None
¶
add_pointcloud(pointcloud: np.ndarray) -> None
¶
Set the scene pointcloud, replacing any previously-registered cloud.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pointcloud
|
ndarray
|
|
required |
Source code in autolife_planning/planning/motion_planner.py
remove_pointcloud() -> bool
¶
Drop the currently-registered pointcloud.
Returns False if there was no cloud to remove.
filter_pointcloud(pointcloud: np.ndarray, min_dist: float, max_range: float, origin: np.ndarray | list[float], workspace_min: np.ndarray | list[float], workspace_max: np.ndarray | list[float], cull: bool = True) -> np.ndarray
¶
Spatially downsample a point cloud via Morton-curve sorting.
Keeps one representative point per min_dist neighbourhood and
discards points farther than max_range from origin or
outside the [workspace_min, workspace_max] bounding box.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pointcloud
|
ndarray
|
|
required |
min_dist
|
float
|
Minimum distance between two retained points. |
required |
max_range
|
float
|
Maximum distance from |
required |
origin
|
ndarray | list[float]
|
|
required |
workspace_min
|
ndarray | list[float]
|
|
required |
workspace_max
|
ndarray | list[float]
|
|
required |
cull
|
bool
|
If |
True
|
Returns:
| Type | Description |
|---|---|
ndarray
|
|
Source code in autolife_planning/planning/motion_planner.py
filter_self_from_pointcloud(pointcloud: np.ndarray, point_radius: float, config: np.ndarray) -> np.ndarray
¶
Remove points that collide with the robot body or environment.
Computes forward kinematics at config, then drops every
point whose inflated sphere (radius point_radius) overlaps
any robot collision sphere or any registered obstacle.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pointcloud
|
ndarray
|
|
required |
point_radius
|
float
|
Inflation radius for each point. |
required |
config
|
ndarray
|
Active-DOF configuration (same space as |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
Source code in autolife_planning/planning/motion_planner.py
set_subgroup(robot_name: str, base_config: np.ndarray | None = None) -> None
¶
Switch active joints without rebuilding the collision environment.
Clears all constraints. The pointcloud is preserved.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
robot_name
|
str
|
Subgroup name from |
required |
base_config
|
ndarray | None
|
24-DOF frozen config for inactive joints. Defaults to the previously stored base config. |
None
|
Source code in autolife_planning/planning/motion_planner.py
extract_config(full_config: np.ndarray) -> np.ndarray
¶
Extract this planner's joints from a full 24-DOF configuration.
Source code in autolife_planning/planning/motion_planner.py
embed_config(config: np.ndarray, base_config: np.ndarray | None = None) -> np.ndarray
¶
Embed a subgroup config into a full 24-DOF configuration.
base_config defaults to the planner's stored base — the same
24-DOF values the C++ collision checker injects for inactive
joints — so the embedded config matches what was validated.
Source code in autolife_planning/planning/motion_planner.py
embed_path(path: np.ndarray, base_config: np.ndarray | None = None) -> np.ndarray
¶
Convert a subgroup path (N, sub_dof) to (N, 24).
base_config defaults to the planner's stored base — the same
24-DOF values the C++ collision checker injects for inactive
joints — so the embedded path matches what was validated.
Source code in autolife_planning/planning/motion_planner.py
plan(start: np.ndarray, goal: np.ndarray, time_limit: float | None = None) -> PlanningResult
¶
Plan a collision-free path from start to goal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
ndarray
|
Start configuration (active DOF). |
required |
goal
|
ndarray
|
Goal configuration (active DOF). |
required |
time_limit
|
float | None
|
Optional per-call override for the solver time
limit. Defaults to |
None
|
Source code in autolife_planning/planning/motion_planner.py
simplify_path(path: np.ndarray, time_limit: float = 1.0) -> np.ndarray
¶
Run OMPL's shortcut-based path simplifier on path.
Same pipeline plan(..., simplify=True) uses internally
(reduceVertices + collapseCloseVertices + shortcutPath
+ B-spline smoothing), but detached so you can apply it to any
path you already have — e.g. replay an old plan with a
different collision environment.
Shortcuts only consult the motion validator. Custom soft
costs (:class:Cost) are ignored; for cost-driven plans, run
:meth:plan with simplify=False and leave the path
untouched unless you've explicitly decided shortcut shaping
is acceptable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
ndarray
|
|
required |
time_limit
|
float
|
Wall-clock budget for the simplifier, seconds. |
1.0
|
Returns:
| Type | Description |
|---|---|
ndarray
|
|
Source code in autolife_planning/planning/motion_planner.py
interpolate_path(path: np.ndarray, count: int = 0, resolution: float = 64.0) -> np.ndarray
¶
Densify path with uniform waypoints along every edge.
Three modes (pick one; the other must be zero):
* ``count > 0`` — exactly that many total waypoints
distributed proportionally to edge length.
* ``resolution > 0.0`` — ``ceil(edge_length * resolution)``
waypoints per edge (uniform density in state-space
distance — the default).
* both ``0`` — OMPL's default longest-valid-segment
fraction.
Uses StateSpace::interpolate internally, so the inserted
states stay on the constraint manifold for projected state
spaces as well as flat ones. No collision check is performed
— the densification only lifts points along the existing
piecewise-linear edges.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
ndarray
|
|
required |
count
|
int
|
Exact total waypoint count if |
0
|
resolution
|
float
|
Waypoints per unit state-space distance if
|
64.0
|
Returns:
| Type | Description |
|---|---|
ndarray
|
|
Source code in autolife_planning/planning/motion_planner.py
validate(configuration: np.ndarray) -> bool
¶
validate_batch(configurations: np.ndarray) -> np.ndarray
¶
Batched collision check — one SIMD block per rake configs.
Packs rake distinct configurations into a single VAMP
ConfigurationBlock<rake> and runs one fkcc<rake> call
per block, so N queries cost ceil(N / rake) SIMD
sweeps in the common case. When a packed block fails we fall
back to per-lane single-state checks for that block only, so
the returned array is always exactly one bool per input.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
configurations
|
ndarray
|
|
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
ndarray
|
|
Source code in autolife_planning/planning/motion_planner.py
sample_valid(max_samples: int = 100000) -> np.ndarray
¶
Sample a random collision-free configuration.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If none of |
Source code in autolife_planning/planning/motion_planner.py
Factory Function¶
create_planner(robot_name: str = 'autolife', config: PlannerConfig | None = None, pointcloud: np.ndarray | None = None, base_config: np.ndarray | None = None, constraints: list | None = None, costs: list | None = None) -> MotionPlanner
¶
Create a motion planner for any robot or subgroup.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
robot_name
|
str
|
Robot or subgroup name. Use :func: |
'autolife'
|
config
|
PlannerConfig | None
|
Planner configuration (uses defaults if None). |
None
|
pointcloud
|
ndarray | None
|
|
None
|
base_config
|
ndarray | None
|
24-DOF values to inject for joints not controlled
by this planner (i.e. the frozen joints of a subgroup).
Defaults to |
None
|
constraints
|
list | None
|
Optional list of
:class: |
None
|
costs
|
list | None
|
Optional list of
:class: |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
MotionPlanner
|
class: |
Source code in autolife_planning/planning/motion_planner.py
Available Robots¶
available_robots() -> list[str]
¶
Return all available robot names for planning.
Constraint¶
Constraint(residual: ca.SX, q_sym: ca.SX, name: str = 'constraint')
dataclass
¶
A user-defined holonomic constraint, JIT-compiled via CasADi.
Constructing this class takes the symbolic Jacobian via
ca.jacobian(residual, q_sym), generates C and compiles it to a
cached shared library (see :func:compile_function).
SymbolicContext(subgroup: str, base_config: np.ndarray | None = None)
¶
CasADi-friendly view of the planner's active subgroup.
Holds the CasADi symbolic joint vector q matching the subgroup's
active dimension. Joints outside the subgroup are frozen at
base_config, the same 24-DOF body vector
([x, y, theta, j0..j20]) the planner uses.
Source code in autolife_planning/planning/symbolic.py
link_pose(link_name: str, q_active: ca.SX | None = None) -> ca.SX
¶
Symbolic 4x4 world pose of a URDF link.
Uses self.q unless another active-dimension symbol is given.
Source code in autolife_planning/planning/symbolic.py
link_translation(link_name: str, q_active: ca.SX | None = None) -> ca.SX
¶
Symbolic 3-vector: link position in world frame.
link_rotation(link_name: str, q_active: ca.SX | None = None) -> ca.SX
¶
evaluate_link_pose(link_name: str, q_active_numeric: np.ndarray) -> np.ndarray
¶
Compute a NUMERIC 4x4 link pose (handy for building targets).
Source code in autolife_planning/planning/symbolic.py
project(q_init: np.ndarray, residual: ca.SX, tol: float = 1e-08, max_iters: int = 100) -> np.ndarray
¶
Project a joint configuration onto the manifold residual(q) = 0.
Runs damped Gauss-Newton on the CasADi Jacobian — the same
iteration OMPL's ProjectedStateSpace runs internally — so
the returned config will pass the planner's tolerance check
and can be used directly as a start or goal state.
Source code in autolife_planning/planning/symbolic.py
Cost¶
Cost(expression: ca.SX, q_sym: ca.SX, name: str = 'cost', weight: float = 1.0)
dataclass
¶
A user-defined soft path cost, JIT-compiled via CasADi.
Pass a scalar CasADi expression in the planner's active joint vector. Construction triggers (on cold cache):
1. symbolic gradient via ``ca.gradient(expression, q_sym)``
2. C code generation and compilation to a cached shared library
(see :func:`~autolife_planning.planning.symbolic.compile_function`)
The C++ planner wraps the loaded function in an OMPL
StateCostIntegralObjective — so per-state values are
trapezoidally integrated along every motion, which is what
RRT*-family optimal planners expect.
The expression must be non-negative (OMPL objectives accumulate
with operator+ and the optimal-planner tooling assumes the
zero cost is the minimum). We don't enforce this at runtime
because that would require evaluating the symbolic expression,
but violating it produces nonsensical RRT* solutions.
Low-level C++ binding¶
OmplVampPlanner
¶
OMPL planner with VAMP SIMD-accelerated collision checking.
Two construction modes:
OmplVampPlanner()— full body, 24 DOF (3 base + 21 joints).OmplVampPlanner(active_indices, frozen_config)— subgroup planner over the joints listed inactive_indices; the C++ collision checker injectsfrozen_configfor every other slot in the 24-DOF body on every state and motion validity query.
add_pointcloud(points: NDArray[np.float32], point_radius: float) -> None
¶
Set the scene pointcloud.
The planner holds at most one cloud; calling this replaces any previously-registered cloud.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points
|
NDArray[float32]
|
|
required |
point_radius
|
float
|
Inflation radius applied to every cloud point. |
required |
Source code in autolife_planning/_ompl_vamp.pyi
remove_pointcloud() -> bool
¶
Drop the currently-registered pointcloud.
Returns False if there was no cloud to remove.
has_pointcloud() -> bool
¶
add_sphere(center: Sequence[float], radius: float) -> None
¶
clear_environment() -> None
¶
add_compiled_constraint(so_path: str, symbol_name: str, ambient_dim: int, co_dim: int) -> None
¶
Load a CasADi-generated shared library as an OMPL constraint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
so_path
|
str
|
Path to the compiled |
required |
symbol_name
|
str
|
CasADi function symbol name inside the library. |
required |
ambient_dim
|
int
|
Dimension of the joint space (must match
:meth: |
required |
co_dim
|
int
|
Number of constraint equations (rows of the residual). |
required |
Source code in autolife_planning/_ompl_vamp.pyi
clear_constraints() -> None
¶
num_constraints() -> int
¶
add_compiled_cost(so_path: str, symbol_name: str, ambient_dim: int, weight: float = 1.0) -> None
¶
Load a CasADi-generated shared library as a soft path cost.
The cost is wrapped as an ompl::StateCostIntegralObjective
— trapezoidally integrated along every motion — and drives
the search of asymptotically-optimal planners (RRT*, BIT*,
AIT*, …). Multiple costs are summed with their weight.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
so_path
|
str
|
Path to the compiled |
required |
symbol_name
|
str
|
CasADi function symbol name inside the library. |
required |
ambient_dim
|
int
|
Dimension of the joint space (must match
:meth: |
required |
weight
|
float
|
Positive scalar multiplier applied to the cost. |
1.0
|
Source code in autolife_planning/_ompl_vamp.pyi
clear_costs() -> None
¶
num_costs() -> int
¶
plan(start: NDArray[np.float64], goal: NDArray[np.float64], planner_name: str = 'rrtc', time_limit: float = 10.0, simplify: bool = True, interpolate: bool = True, resolution: float = 64.0) -> PlanResult
¶
Plan a collision-free path from start to goal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
NDArray[float64]
|
Active-DOF start configuration (length :meth: |
required |
goal
|
NDArray[float64]
|
Active-DOF goal configuration (length :meth: |
required |
planner_name
|
str
|
OMPL planner identifier (e.g. |
'rrtc'
|
time_limit
|
float
|
Solver time limit in seconds. |
10.0
|
simplify
|
bool
|
If true, run |
True
|
interpolate
|
bool
|
If true, densify the simplified path. |
True
|
resolution
|
float
|
Waypoints per unit of state-space distance. Each
edge of length |
64.0
|
Source code in autolife_planning/_ompl_vamp.pyi
simplify_path(path: NDArray[np.float64], time_limit: float = 1.0) -> NDArray[np.float64]
¶
Run OMPL's shortcut simplifier on a waypoint list.
Reuses the current collision environment and constraints. Shortcuts only consult the motion validator, so custom soft costs are ignored. Returns the simplified path as waypoints.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
NDArray[float64]
|
|
required |
time_limit
|
float
|
Simplifier wall-clock budget, seconds. |
1.0
|
Source code in autolife_planning/_ompl_vamp.pyi
interpolate_path(path: NDArray[np.float64], count: int = 0, resolution: float = 64.0) -> NDArray[np.float64]
¶
Densify a waypoint list along its existing edges.
Pass at most one of count (exact total waypoints,
distributed by edge length) or resolution (waypoints per
unit of state-space distance). Both zero falls back to OMPL's
default longest-valid-segment fraction. No collision check is
performed — densification only calls StateSpace::interpolate
on the existing edges.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
NDArray[float64]
|
|
required |
count
|
int
|
Total waypoint count if > 0. |
0
|
resolution
|
float
|
Waypoints per unit distance if > 0.0. |
64.0
|
Source code in autolife_planning/_ompl_vamp.pyi
validate(config: NDArray[np.float64]) -> bool
¶
Return True if config is collision-free.
config must have length :meth:dimension. Subgroup
planners expand it to a full 24-DOF state with the stored
frozen_config before checking.
Source code in autolife_planning/_ompl_vamp.pyi
validate_batch(configs: NDArray[np.float64]) -> NDArray[np.bool_]
¶
Batched collision check — deep SIMD, per-config result.
Packs up to rake distinct configurations into a single
VAMP ConfigurationBlock<rake> so one Robot::fkcc<rake>
call sphere-FKs and collision-checks all of them
simultaneously — the same SIMD primitive the motion-edge
validator uses for interpolated samples, fed independent
configs per lane instead.
Returns one bool per input config in input order. In the
common case (most configs valid) costs ceil(N / rake)
SIMD calls; when a packed block fails, only that block falls
back to per-lane single-state checks.
configs has shape (N, dimension()).
Subgroup planners expand each to a full 24-DOF state with
the stored frozen_config before packing.
Source code in autolife_planning/_ompl_vamp.pyi
dimension() -> int
¶
lower_bounds() -> NDArray[np.float64]
¶
upper_bounds() -> NDArray[np.float64]
¶
default_joint_limits() -> tuple[NDArray[np.float64], NDArray[np.float64]]
staticmethod
¶
(lower, upper) limits compiled into the robot model, for all
24 joints in full-body order.
joint_limits() -> tuple[NDArray[np.float64], NDArray[np.float64]]
¶
(lower, upper) limits in effect for all 24 joints, in
full-body order. :meth:lower_bounds and :meth:upper_bounds
give the same limits for the active joints only.
Source code in autolife_planning/_ompl_vamp.pyi
set_joint_limits(lower: NDArray[np.float64], upper: NDArray[np.float64]) -> None
¶
Replace the limits of all 24 joints (full-body order).
The active state space takes its bounds from these limits, now
and after every :meth:set_subgroup or :meth:set_full_body.
Collision checking does not read them.
Raises:
| Type | Description |
|---|---|
ValueError
|
Unless both arrays have 24 finite entries with
|
Source code in autolife_planning/_ompl_vamp.pyi
filter_pointcloud(points: NDArray[np.float32], min_dist: float, max_range: float, origin: Sequence[float], workspace_min: Sequence[float], workspace_max: Sequence[float], cull: bool = True) -> NDArray[np.float32]
¶
Morton-curve downsampling of an (N, 3) cloud, optionally
culled to max_range from origin and to the workspace box.
Source code in autolife_planning/_ompl_vamp.pyi
filter_self_from_pointcloud(points: NDArray[np.float32], point_radius: float, config: NDArray[np.float64]) -> NDArray[np.float32]
¶
Drop the points of an (N, 3) cloud that overlap the robot's
spheres at config (active DOF) or the current environment.
Source code in autolife_planning/_ompl_vamp.pyi
set_subgroup(active_indices: Sequence[int], frozen_config: Sequence[float]) -> None
¶
Switch to a different subgroup without rebuilding the environment.
Clears all constraints. The pointcloud and collision geometry are preserved.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
active_indices
|
Sequence[int]
|
Positions in the full 24-DOF body that this planner will plan over, in DOF order. |
required |
frozen_config
|
Sequence[float]
|
24-DOF stance to inject for every joint not
in |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in autolife_planning/_ompl_vamp.pyi
set_full_body() -> None
¶
Switch back to full-body planning (24 DOF).
Clears all constraints. The pointcloud and collision geometry are preserved.