4. 双S曲线规划器
任务说明
实现一个双S曲线速度规划器,输入起点/终点位置、最大速度、最大加速度及加加速度( jerk ),输出位置、速度、加速度随时间变化的完整运动轨迹。规划器需支持非对称加减速参数(即加速段与减速段的最大加速度、 jerk 可分别设置),并正确处理短距离下无法达到最大速度的降级场景。
双S曲线规划原理
双S曲线将运动分为七个阶段:加加速、匀加速、减加速、匀速、加减速、匀减速、减减速。当距离不足以完成完整加速-匀速-减速过程时,规划器自动收缩匀速段长度;若距离仍不足,则进一步降低峰值速度,使加速段与减速段直接衔接(三角形速度剖面)。本实现采用时间离散化方法:根据各阶段持续时间计算每个采样时刻的运动学状态,避免解析求解复杂的分段方程。
规划器实现
import numpy as np
class DoubleSPlanner:
def __init__(self, q0, q1, vmax, amax_acc, jmax_acc, amax_dec, jmax_dec, dt=0.001):
"""
参数:
q0, q1: 起点/终点位置 (m)
vmax: 最大允许速度 (m/s)
amax_acc, jmax_acc: 加速段最大加速度/加加速度
amax_dec, jmax_dec: 减速段最大加速度/加加速度
dt: 采样周期 (s)
"""
self.q0 = q0
self.q1 = q1
self.vmax = abs(vmax)
self.amax_acc = abs(amax_acc)
self.jmax_acc = abs(jmax_acc)
self.amax_dec = abs(amax_dec)
self.jmax_dec = abs(jmax_dec)
self.dt = dt
self.direction = 1 if q1 >= q0 else -1
self.dist = abs(q1 - q0)
self._plan()
def _plan(self):
"""计算各阶段持续时间"""
# 加速段达到最大加速度所需时间
ta_acc = self.amax_acc / self.jmax_acc
# 减速段达到最大加速度所需时间
ta_dec = self.amax_dec / self.jmax_dec
# 加速段从0加速到vmax所需距离
if self.vmax >= self.amax_acc * ta_acc:
# 完整加速: 加加速+匀加速+减加速
s_acc_full = self.amax_acc * ta_acc**2 + self.vmax * ta_acc
else:
# 三角形加速剖面
ta_tri = np.sqrt(self.vmax / self.jmax_acc)
s_acc_full = self.jmax_acc * ta_tri**3
# 减速段从vmax减速到0所需距离
if self.vmax >= self.amax_dec * ta_dec:
s_dec_full = self.amax_dec * ta_dec**2 + self.vmax * ta_dec
else:
ta_tri_dec = np.sqrt(self.vmax / self.jmax_dec)
s_dec_full = self.jmax_dec * ta_tri_dec**3
# 判断是否能达到最大速度
if s_acc_full + s_dec_full <= self.dist:
# 情况1: 有匀速段
self.v_peak = self.vmax
self.T_acc = self._acc_time_to_v(self.v_peak)
self.T_dec = self._dec_time_from_v(self.v_peak)
s_acc = self._acc_dist_to_v(self.v_peak)
s_dec = self._dec_dist_from_v(self.v_peak)
self.T_const = (self.dist - s_acc - s_dec) / self.v_peak
self.T_total = self.T_acc + self.T_const + self.T_dec
else:
# 情况2: 无法达到vmax,寻找实际峰值速度
v_low = 0.0
v_high = self.vmax
for _ in range(100): # 二分搜索
v_mid = (v_low + v_high) / 2
s_acc_mid = self._acc_dist_to_v(v_mid)
s_dec_mid = self._dec_dist_from_v(v_mid)
if s_acc_mid + s_dec_mid <= self.dist:
v_low = v_mid
else:
v_high = v_mid
self.v_peak = (v_low + v_high) / 2
self.T_acc = self._acc_time_to_v(self.v_peak)
self.T_dec = self._dec_time_from_v(self.v_peak)
self.T_const = 0.0
self.T_total = self.T_acc + self.T_dec
def _acc_time_to_v(self, v):
"""加速段从0到v所需时间"""
if v >= self.amax_acc * self.amax_acc / self.jmax_acc:
return self.amax_acc / self.jmax_acc + v / self.amax_acc
else:
return 2 * np.sqrt(v / self.jmax_acc)
def _dec_time_from_v(self, v):
"""减速段从v到0所需时间"""
if v >= self.amax_dec * self.amax_dec / self.jmax_dec:
return self.amax_dec / self.jmax_dec + v / self.amax_dec
else:
return 2 * np.sqrt(v / self.jmax_dec)
def _acc_dist_to_v(self, v):
"""加速段从0到v所需距离"""
if v >= self.amax_acc * self.amax_acc / self.jmax_acc:
t1 = self.amax_acc / self.jmax_acc
return 0.5 * self.jmax_acc * t1**3 + v * t1 + (v - self.amax_acc * t1)**2 / (2 * self.amax_acc)
else:
return v * np.sqrt(v / self.jmax_acc)
def _dec_dist_from_v(self, v):
"""减速段从v到0所需距离"""
if v >= self.amax_dec * self.amax_dec / self.jmax_dec:
t1 = self.amax_dec / self.jmax_dec
return 0.5 * self.jmax_dec * t1**3 + v * t1 + (v - self.amax_dec * t1)**2 / (2 * self.amax_dec)
else:
return v * np.sqrt(v / self.jmax_dec)
def _acc_profile(self, t, T_acc):
"""加速段速度曲线 (0~T_acc)"""
if t <= 0:
return 0.0
if t >= T_acc:
return self.v_peak
# 加加速阶段
t1 = self.amax_acc / self.jmax_acc
if t <= t1:
return 0.5 * self.jmax_acc * t**2
# 匀加速阶段
t2 = T_acc - t1
if t <= t1 + t2:
return self.amax_acc * t1 + self.amax_acc * (t - t1)
# 减加速阶段
t3 = t - t1 - t2
return self.v_peak - 0.5 * self.jmax_acc * (t1 - t3)**2
def _dec_profile(self, t_local, T_dec):
"""减速段速度曲线 (0~T_dec)"""
if t_local <= 0:
return self.v_peak
if t_local >= T_dec:
return 0.0
t1 = self.amax_dec / self.jmax_dec
# 加减速阶段
if t_local <= t1:
return self.v_peak - 0.5 * self.jmax_dec * t_local**2
# 匀减速阶段
t2 = T_dec - t1
if t_local <= t1 + t2:
return self.v_peak - self.amax_dec * t1 - self.amax_dec * (t_local - t1)
# 减减速阶段
t3 = t_local - t1 - t2
return 0.5 * self.jmax_dec * (t1 - t3)**2
def generate(self):
"""生成完整轨迹"""
n = int(np.ceil(self.T_total / self.dt)) + 1
times = np.linspace(0, self.T_total, n)
positions = np.zeros(n)
velocities = np.zeros(n)
accelerations = np.zeros(n)
for i, t in enumerate(times):
if t <= self.T_acc:
v = self._acc_profile(t, self.T_acc)
a = self._acceleration(t, self.T_acc, is_acc=True)
elif t <= self.T_acc + self.T_const:
v = self.v_peak
a = 0.0
else:
t_dec = t - self.T_acc - self.T_const
v = self._dec_profile(t_dec, self.T_dec)
a = self._acceleration(t_dec, self.T_dec, is_acc=False)
velocities[i] = v * self.direction
accelerations[i] = a * self.direction
if i == 0:
positions[i] = self.q0
else:
positions[i] = positions[i-1] + velocities[i] * self.dt
# 修正终点位置
positions[-1] = self.q1
return times, positions, velocities, accelerations
def _acceleration(self, t, T, is_acc):
"""计算加速度"""
if is_acc:
t1 = self.amax_acc / self.jmax_acc
if t <= t1:
return self.jmax_acc * t
elif t <= T - t1:
return self.amax_acc
elif t <= T:
return self.jmax_acc * (T - t)
else:
return 0.0
else:
t1 = self.amax_dec / self.jmax_dec
if t <= t1:
return -self.jmax_dec * t
elif t <= T - t1:
return -self.amax_dec
elif t <= T:
return -self.jmax_dec * (T - t)
else:
return 0.0
使用示例与验证
# 示例1: 正常长距离,非对称参数
planner = DoubleSPlanner(q0=0, q1=1.0, vmax=0.5,
amax_acc=1.0, jmax_acc=5.0,
amax_dec=0.8, jmax_dec=4.0)
t, p, v, a = planner.generate()
print(f"总时间: {t[-1]:.3f}s, 峰值速度: {np.max(np.abs(v)):.3f}m/s")
print(f"终点位置: {p[-1]:.3f}m (目标1.0m)")
# 示例2: 短距离,无法达到最大速度
planner2 = DoubleSPlanner(q0=0, q1=0.05, vmax=0.5,
amax_acc=1.0, jmax_acc=5.0,
amax_dec=0.8, jmax_dec=4.0)
t2, p2, v2, a2 = planner2.generate()
print(f"短距离总时间: {t2[-1]:.3f}s, 实际峰值速度: {np.max(np.abs(v2)):.3f}m/s")
print(f"终点位置: {p2[-1]:.3f}m (目标0.05m)")
验证方法
运行上述示例,观察输出:
- 示例1中峰值速度应接近0.5m/s(达到vmax),且终点位置误差小于1e-3m;
- 示例2中峰值速度应明显小于0.5m/s(因距离不足自动降速),终点位置同样收敛到目标值;
- 绘制速度曲线应呈现平滑的S形过渡,加速度曲线连续无突变。
判定标准
- 示例1输出峰值速度≈0.5m/s,总时间约2.5~3.5s(取决于参数),终点位置误差<1e-3m;
- 示例2输出峰值速度显著低于0.5m/s(约0.1~0.2m/s),证明短距离降级逻辑生效;
- 速度曲线无突变,加速度曲线在阶段切换点连续,满足双S曲线平滑性要求。