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形过渡,加速度曲线连续无突变。
双S曲线规划器验证结果(示例1) 时间 (s) 速度 (m/s) 加加速 匀加速 减加速 匀速段 减速段 vmax 时间 (s) 加速度 (m/s²) +amax_acc -amax_dec 速度 加速度

判定标准

  • 示例1输出峰值速度≈0.5m/s,总时间约2.5~3.5s(取决于参数),终点位置误差<1e-3m;
  • 示例2输出峰值速度显著低于0.5m/s(约0.1~0.2m/s),证明短距离降级逻辑生效;
  • 速度曲线无突变,加速度曲线在阶段切换点连续,满足双S曲线平滑性要求。