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
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.
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
¶
Check if a configuration is collision-free.
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() -> np.ndarray
¶
Sample a random collision-free configuration.
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 triggers (on cold cache):
1. symbolic Jacobian via ca.jacobian(residual, q_sym)
2. C code generation via CasADi
3. compilation to a .so with c++ -O3 -shared -fPIC
4. caching under ~/.cache/autolife_planning/constraints/<sha>/
On a cache hit the whole thing is a single stat + string compare.
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, plus a pinocchio.casadi model for symbolic FK.
The context hides the planar-root encoding (the 24-DOF body vector
uses [x, y, theta, j0..j20]; pinocchio uses
[x, y, cos(theta), sin(theta), j0..j20]) and the mapping from
the active subspace back to the full 24-DOF body via base_config.
Source code in autolife_planning/planning/symbolic.py
link_pose(link_name: str, q_active: ca.SX | None = None)
¶
Return the symbolic pinocchio SE3 of a URDF link.
Pass q_active=self.q (or omit) to get an expression that
depends on the active joints symbolically.
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).
Uses the numeric pinocchio model, not the symbolic one, so it is fast and has no dependence on CasADi expressions.
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 via CasADi
3. compilation to a ``.so`` with ``c++ -O3 -shared -fPIC``
4. caching under ``~/.cache/autolife_planning/costs/<sha>/``
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: Sequence[Sequence[float]], r_min: float, r_max: float, 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
|
Sequence[Sequence[float]]
|
|
required |
r_min
|
float
|
Minimum robot collision-sphere radius (from
:meth: |
required |
r_max
|
float
|
Maximum robot collision-sphere radius (from
:meth: |
required |
point_radius
|
float
|
Inflation radius applied to every cloud point. |
required |
Source code in autolife_planning/_ompl_vamp.pyi
remove_pointcloud() -> bool
¶
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: Sequence[float], goal: Sequence[float], planner_name: str = 'rrtc', time_limit: float = 10.0, simplify: bool = True, interpolate: bool = True, interpolate_count: int = 0, resolution: float = 64.0) -> PlanResult
¶
Plan a collision-free path from start to goal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
Sequence[float]
|
Active-DOF start configuration (length :meth: |
required |
goal
|
Sequence[float]
|
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. Density
is picked by |
True
|
interpolate_count
|
int
|
Target total waypoint count for the whole
path. OMPL distributes states across edges proportionally
to their length. |
0
|
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: Sequence[Sequence[float]], time_limit: float = 1.0) -> list[list[float]]
¶
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
|
Sequence[Sequence[float]]
|
|
required |
time_limit
|
float
|
Simplifier wall-clock budget, seconds. |
1.0
|
Source code in autolife_planning/_ompl_vamp.pyi
interpolate_path(path: Sequence[Sequence[float]], count: int = 0, resolution: float = 64.0) -> list[list[float]]
¶
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
|
Sequence[Sequence[float]]
|
|
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: Sequence[float]) -> 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: Sequence[Sequence[float]]) -> list[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.
Each configs[i] must have length :meth: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() -> list[float]
¶
upper_bounds() -> list[float]
¶
min_max_radii() -> tuple[float, float]
¶
(min_radius, max_radius) of the robot's collision spheres.
Pass these to :meth:add_pointcloud so VAMP can index its
broadphase correctly.
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 |
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.