"""Optimal trajectory and kinematic filtering."""
import functools
from typing import NamedTuple
import numpy as np
from being.constants import INF
from being.math import sign, clip
[docs]class State(NamedTuple):
"""Kinematic state."""
position: float = 0.
"""Scalar position value."""
velocity: float = 0.
"""Scalar speed value."""
acceleration: float = 0.
"""Scalar acceleration value."""
[docs]def optimal_trajectory(
initial: State,
target: State,
maxSpeed: float = 1.0,
maxAcc: float = 1.0,
) -> list:
"""Calculate acceleration bang profiles for optimal trajectory from initial
state `start` to `target` state. Respecting the kinematic limits. Bang
profiles are given by their duration and the acceleration values.
Both target and start velocities have to be below the `maxSpeed` limit.
Error otherwise.
Args:
initial: Initial state.
target: Target state.
maxSpeed (optional): Maximum speed value. 1.0 by default.
maxAcc (optional): Maximum acceleration (and deceleration) value. 1.0 by default.
Returns:
List of bang speed profiles. A bang segment is a (duration,
acceleration) tuple. 1-3 bang profiles depending on resulting speed
profile (critical, triangular or trapezoidal).
Example:
>>> optimal_trajectory(initial=(0, 0, 0), target=(1, 0, 0), maxSpeed=0.5)
[(0.5, 1.0), (1.5, 0.0), (0.5, -1.0)]
Resources:
- Francisco Ramos, Mohanarajah Gajamohan, Nico Huebel and Raffaello
D’Andrea: Time-Optimal Online Trajectory Generator for Robotic
Manipulators.
http://webarchiv.ethz.ch/roboearth/wp-content/uploads/2013/02/OMG.pdf
"""
x0 = initial[0]
v0 = initial[1]
xEnd = target[0]
vEnd = target[1]
dx = xEnd - x0
dv = vEnd - v0
if dx == dv == 0:
return []
# Determine critical profile
sv = sign(dv)
tCritical = sv * dv / maxAcc
dxCritical = .5 * (vEnd + v0) * tCritical
if dxCritical == dx:
return [(tCritical, sv * maxAcc)]
# Reachable peek speed, in relation with maximum speed, determines shape of
# speed profile. Either triangular or trapezoidal.
s = sign(dx - dxCritical) # Direction
peekSpeed = (.5 * (vEnd**2 + v0**2) + s * dx * maxAcc)**.5
if peekSpeed <= maxSpeed:
# Triangular speed profile
accDuration = (s * peekSpeed - v0) / (s * maxAcc)
decDuration = (vEnd - s * peekSpeed) / (-s * maxAcc)
return [
(accDuration, s * maxAcc),
(decDuration, -s * maxAcc),
]
# Trapezoidal speed profile
accDuration = (s * maxSpeed - v0) / (s * maxAcc)
t2 = ((vEnd**2 + v0**2 - 2 * s * maxSpeed * v0) / (2 * maxAcc) + s * dx) / maxSpeed
cruiseDuration = t2 - accDuration
decDuration = (vEnd - s * maxSpeed) / (-s * maxAcc)
return [
(accDuration, s * maxAcc),
(cruiseDuration, 0.),
(decDuration, -s * maxAcc),
]
[docs]def sequencable(func):
"""Filter function for an sequence of input values. Carry on state between
single filter calls.
"""
@functools.wraps(func)
def wrapped_func(targets, dt, state=State(), **kwargs):
if isinstance(targets, float):
return func(targets, dt, state=state, **kwargs)
traj = []
for target in targets:
state = func(target, dt, state=state, **kwargs)
traj.append(state)
return np.array(traj)
return wrapped_func
[docs]def step(state: State, dt: float) -> State:
"""Evolve state for some time interval."""
x0, v0, a0 = state
return State(
x0 + v0 * dt + 0.5 * a0 * dt**2,
v0 + a0 * dt,
a0,
)
[docs]def kinematic_filter(
targetPosition: float,
dt: float,
initial: State = State(),
targetVelocity: float = 0.0,
maxSpeed: float = 1.0,
maxAcc: float = 1.0,
lower: float = -INF,
upper: float = INF,
) -> State:
"""Filter target position with respect to the kinematic limits (maximum
speed and maximum acceleration / deceleration). Online optimal trajectory.
Args:
targetPosition: Target position value.
dt: Time interval.
state: Initial / current state.
targetVelocity: Target velocity value. Use with care! Steady sate with
non-zero targetVelocity leads to oscillation.
maxSpeed: Maximum speed value.
maxAcc: Maximum acceleration (and deceleration) value.
lower: Lower clipping value for target value.
upper: Upper clipping value for target value.
Returns:
The next state.
"""
target = (clip(targetPosition, lower, upper), targetVelocity)
bangProfiles = optimal_trajectory(initial, target, maxSpeed, maxAcc)
# Effectively spline evaluation. Go through all segments and see where we
# are at `dt`. Update state for intermediate steps.
for duration, acc in bangProfiles:
if dt <= duration:
return step((initial.position, initial.velocity, acc), dt)
initial = step((initial.position, initial.velocity, acc), duration)
dt -= duration
return initial._replace(acceleration=0.)
[docs]def kinematic_filter_vec(targets, dt, initial=State(), **kwargs):
traj = []
for target in targets:
initial = kinematic_filter(target, dt, initial=initial, **kwargs)
traj.append(initial)
return np.array(traj)