《Python机器人基础原理》第二十一篇:机械臂仿真——点位搬运任务,采用三次多项式完成关节空间插值,生成关节角度随时间变化序列

 

1. 实际工业应用场景

 

周五上午,某3C电子装配车间的上下料工位。一台3-DOF垂直关节机械臂(基座偏航+肘部俯仰+末端旋转)正在执行PCB板从进料皮带到测试工位的搬运任务。机器人构型:大臂长600 mm,小臂长450 mm,末端旋转轴位于小臂末端。连杆质量:大臂5.2 kg(质心距基座0.28 m),小臂3.1 kg(质心距肘部0.20 m),末端夹具1.5 kg(质心距肘部0.12 m,质心到旋转轴线横向偏置0.02 m)。关节额定扭矩:基座偏航轴60 N·m,肘部俯仰轴180 N·m,关节最大角速度限制:基座偏航2.5 rad/s、肘部俯仰2.0 rad/s、末端旋转3.0 rad/s;最大角加速度限制:基座偏航5.0 rad/s²、肘部俯仰4.0 rad/s²、末端旋转6.0 rad/s²。

 

调试工程师小李把示教器递给旁边的新人小赵:"你试试让机器人从进料位走到测试位。起点:θ₁=0°、θ₂=-30°、θ₃=0°;终点:θ₁=60°、θ₂=-80°、θ₃=90°。走关节插值模式。"

 

小赵在示教器上设了起点和终点,选了"线性插值",点下执行。机器人动起来了——但走到一半突然抖了一下,末端夹具上的PCB板差点飞出去。

 

小李一把按住急停:"停。你用的线性插值,角度变化率是常数,意味着每个关节在起终点瞬间加速度无穷大——速度从零瞬间跳到某个值再瞬间归零,电机得承受理论上的冲击脉冲。那一下抖动就是冲击导致的。"

 

小赵愣了一下:"那应该用什么?"

 

"三次多项式。给定起点和终点的角度、角速度(通常设为零),多项式自动保证位置和速度连续——也就是C¹连续。但注意,三次多项式的加速度在起终点是不连续的:从静止瞬间跳到一个非零值,加加速度(jerk)在起点是无穷大。这比线性插值好得多——至少速度不再有阶跃——但如果你看加速度曲线,起点有个跳变。这恰好说明了三次的局限:它只能保证C¹,不能保证C²。下一篇我们上五次多项式,把起终点加速度也约束为零。"

 

小赵在草稿纸上列式子:"三次多项式θ(t) = a₀ + a₁t + a₂t² + a₃t³,四个约束解出四个系数。起终点速度为零的话,a₁=0,a₂=3Δq/T²,a₃=-2Δq/T³。速度峰值是1.5|Δq|/T,出现在t=T/2。"

 

"对。咱们取T=2.0 s。Joint2的Δq = -50° = -0.873 rad,峰值速度=1.5×0.873/2.0 ≈ 0.65 rad/s,峰值加速度=6×0.873/4.0 ≈ 1.31 rad/s²。都在限位以内——速度限2.0,加速度限4.0,余量很足。"

 

"好。那我写个离线仿真,T=2.0 s,用三次多项式生成θ₁、θ₂、θ₃各自的时间序列,画四张图验证。"

 

本篇解决的问题:给定一组关节空间点位(起点和终点的角度),采用三次多项式(Cubic Polynomial)完成关节空间插值,生成关节角度、角速度、角加速度随时间变化的连续序列。用于点位搬运任务的离线轨迹验证,确保位置和速度连续(C¹),校验是否超出物理限位。

 

⚠️ 声明:本篇全部代码为课程离线仿真,运行在普通PC上,用于点位搬运任务的关节空间轨迹生成与可视化。不可直接用于真实机器人控制器、伺服驱动器或装配产线设备。真实工业现场需结合运动控制卡、实时插补、软硬限位保护。代码作者不对直接使用本代码于真实设备造成的任何后果负责。

 

2. 痛点分析

 

课程模块 本篇对应的工程痛点

第6章 轨迹规划与控制 → 6.1 关节空间轨迹规划 点位搬运需要位置和速度连续,线性插值导致速度阶跃→理论加速度无穷大→机械冲击

第6章 轨迹规划与控制 → 6.2 多项式插值 三次多项式提供C¹连续(位置、速度),满足点位运动基本平滑性需求;加速度在起终点有阶跃(C¹但不C²)

第5章 机器人关节驱动与控制 → 5.1 伺服驱动基础 电机有最大转速和最大加速度限制,轨迹需离线校核是否超限

 

本篇代码解决 L2 离线轨迹规划层。输入:起点/终点关节角度、运动时间T=2.0 s、采样点数。输出:各关节的θ(t)、θ̇(t)、θ̈(t)时间序列。代码不是设备实时控制器,不做伺服闭环。它做的事情是:给定起点终点,用三次多项式生成C¹连续轨迹,校验连续性和物理可行性。

 

不覆盖:笛卡尔空间直线/圆弧插补、五次多项式/梯形速度规划、B样条、实时插补、多轴联动、6轴完整轨迹。五次多项式(C²连续)留待下一篇。

 

3. 核心逻辑讲解

 

物理直觉

 

你开车从A点停到B点。线性插值就像一脚油门踩到底然后急刹——速度瞬间从零跳到满速再瞬间归零,车会猛冲再猛停。三次多项式的做法是:缓缓加速到某个速度再缓缓减速到零,速度曲线是平滑的S形。但注意——加速度从起步瞬间就不是零(你一踩油门就有加速度),在停车瞬间也跳回零。所以三次多项式保证速度不断裂(C¹),但加速度在起终点有跳变(不是C²)。想要加速度也连续,得上五次多项式。

 

业务逻辑 → 代码映射

 

输入:起点角度q_start、终点角度q_end、运动时间T=2.0 s、采样点数N=100

  ↓

步骤1:对每个关节,建立三次多项式 θ(t) = a₀ + a₁·t + a₂·t² + a₃·t³

  ↓

步骤2:代入约束条件解出系数(线性方程组通解,支持非零起终点速度):

  - θ(0) = q_start → a₀ = q_start

  - θ(T) = q_end → a₀ + a₁·T + a₂·T² + a₃·T³ = q_end

  - θ'(0) = v_start → a₁ = v_start

  - θ'(T) = v_end → a₁ + 2·a₂·T + 3·a₃·T² = v_end

  ↓

步骤3(起终点速度为零的特例):

  a₀ = q_start, a₁ = 0, a₂ = 3Δq/T², a₃ = -2Δq/T³

  ↓

步骤4:在N个采样点上计算 θ(t), θ'(t) = 2a₂t + 3a₃t², θ''(t) = 2a₂ + 6a₃t

  ↓

步骤5:解析计算峰值(非采样近似):

  速度峰值 = 1.5|Δq|/T(t=T/2)

  加速度极值 = max(|2a₂|, |2a₂+6a₃T|)(边界值)

  ↓

步骤6:校验位置/速度/加速度三重限位,输出4张图表

 

关键公式文字翻译

 

三次多项式:θ(t) = a₀ + a₁·t + a₂·t² + a₃·t³。a₀是起点角度,a₁是起点速度,a₂和a₃由终点约束决定。

 

系数求解(通解):建立线性方程组求解a₂、a₃,支持任意起终点速度。本篇特例(起终点速度为零):a₀=q_start,a₁=0,a₂=3Δq/T²,a₃=-2Δq/T³。

 

速度:θ̇(t) = 2a₂t + 3a₃t²。起终点为零,中间在t=T/2达到峰值1.5|Δq|/T。

 

加速度:θ̈(t) = 2a₂ + 6a₃t。从2a₂线性变化到2a₂+6a₃T。在t=0处加速度跳变到2a₂(非零),在t=T处跳变回零——这就是C¹但不C²的数学表现。

 

单位:角度 rad,角速度 rad/s,角加速度 rad/s²,时间 s。

 

4. OOP代码实现

 

技术栈

 

严格限定:numpy, scipy, matplotlib。不引入pybullet、roboticstoolbox等任何第三方机器人库。

 

项目结构树

 

point_to_point_sim/

├── run_point_to_point_sim.py

├── requirements.txt

├── trajectory/

│ ├── __init__.py

│ ├── models.py

│ ├── cubic_interpolator.py

│ └── visualize.py

├── tests/

│ ├── __init__.py

│ └── test_point_to_point_sim.py

└── results/

 

核心源码

 

"trajectory/models.py" — dataclass数据模型

 

"""

3-DOF垂直关节机械臂数据模型

点位搬运仿真:三次多项式关节空间插值

 

构型说明:基座偏航(θ1, 竖直轴) + 肘部俯仰(θ2, 水平轴) + 末端旋转(θ3)

物理声明:

  - 本篇仅做轨迹规划,不涉及动力学计算

  - 关节限位用于校验插值结果是否越界

  - 速度/加速度限制用于校验轨迹可行性

"""

 

 

from dataclasses import dataclass, field

from typing import List, Tuple, Optional

import numpy as np

 

 

@dataclass

class JointWaypoint:

    """

    单个关节的单个路点

    angle: 关节角度 (rad)

    velocity: 关节角速度 (rad/s),设为零表示停靠

    """

    angle: float

    velocity: float = 0.0

    joint_name: str = ""

 

    def __post_init__(self):

        if not np.isfinite(self.angle):

            raise ValueError(f"JointWaypoint: 角度必须为有限数值")

        if not np.isfinite(self.velocity):

            raise ValueError(f"JointWaypoint: 速度必须为有限数值")

 

 

@dataclass

class JointLimit:

    """单关节硬件限位"""

    min_val: float

    max_val: float

    joint_name: str = ""

    max_velocity: float = 0.0 # 最大角速度 (rad/s)

    max_acceleration: float = 0.0 # 最大角加速度 (rad/s²)

 

    def __post_init__(self):

        if self.min_val >= self.max_val:

            raise ValueError(

                f"JointLimit: min_val({self.min_val})必须严格小于"

                f"max_val({self.max_val})")

        if not np.isfinite(self.min_val) or not np.isfinite(self.max_val):

            raise ValueError("JointLimit: 限位值必须为有限数值")

 

    def contains(self, value: float) -> bool:

        return (self.min_val - 1e-10) <= value <= (self.max_val + 1e-10)

 

    def velocity_ok(self, vel: float) -> bool:

        if self.max_velocity <= 0:

            return True

        return abs(vel) <= self.max_velocity + 1e-10

 

    def acceleration_ok(self, accel: float) -> bool:

        if self.max_acceleration <= 0:

            return True

        return abs(accel) <= self.max_acceleration + 1e-10

 

 

@dataclass

class JointLimitSet:

    """3-DOF机械臂关节限位集合"""

    limits: List[JointLimit]

 

    def __post_init__(self):

        if len(self.limits) != 3:

            raise ValueError(f"需要3个关节限位, 实际{len(self.limits)}个")

 

    @classmethod

    def default_3dof_arm(cls):

        """

        默认3-DOF机械臂限位

        θ1: ±170°, 最大速度2.5 rad/s, 最大加速度5.0 rad/s²

        θ2: ±140°, 最大速度2.0 rad/s, 最大加速度4.0 rad/s²

        θ3: ±180°, 最大速度3.0 rad/s, 最大加速度6.0 rad/s²

        """

        pi = 3.141592653589793

        return cls(limits=[

            JointLimit(-170 * pi / 180, 170 * pi / 180, 'joint1_base_yaw',

                       2.5, 5.0),

            JointLimit(-140 * pi / 180, 140 * pi / 180, 'joint2_elbow_pitch',

                       2.0, 4.0),

            JointLimit(-180 * pi / 180, 180 * pi / 180, 'joint3_end_rotate',

                       3.0, 6.0),

        ])

 

    def check_all_positions(self, joint_values: List[float]) -> Tuple[bool, List[int]]:

        if len(joint_values) != 3:

            raise ValueError(f"需要3个关节值, 实际{len(joint_values)}个")

        all_pass = True

        violation_indices = []

        for i, (val, limit) in enumerate(zip(joint_values, self.limits)):

            if not limit.contains(val):

                all_pass = False

                violation_indices.append(i)

        return all_pass, violation_indices

 

    def check_all_velocities(self, velocities: List[float]) -> Tuple[bool, List[int]]:

        if len(velocities) != 3:

            raise ValueError(f"需要3个速度值, 实际{len(velocities)}个")

        all_pass = True

        violation_indices = []

        for i, (vel, limit) in enumerate(zip(velocities, self.limits)):

            if not limit.velocity_ok(vel):

                all_pass = False

                violation_indices.append(i)

        return all_pass, violation_indices

 

    def check_all_accelerations(self, accelerations: List[float]) -> Tuple[bool, List[int]]:

        if len(accelerations) != 3:

            raise ValueError(f"需要3个加速度值, 实际{len(accelerations)}个")

        all_pass = True

        violation_indices = []

        for i, (accel, limit) in enumerate(zip(accelerations, self.limits)):

            if not limit.acceleration_ok(accel):

                all_pass = False

                violation_indices.append(i)

        return all_pass, violation_indices

 

 

@dataclass

class RobotParams:

    """

    3-DOF机械臂参数(本篇仅用于轨迹规划,不涉及动力学)

    """

    joint_names: List[str] = field(default_factory=lambda: [

        'joint1_base_yaw', 'joint2_elbow_pitch', 'joint3_end_rotate'

    ])

    link_lengths: List[float] = field(default_factory=lambda: [0.600, 0.450, 0.0])

 

    @classmethod

    def default_3dof_arm(cls):

        return cls()

 

"trajectory/cubic_interpolator.py" — 三次多项式插值器

 

"""

三次多项式关节空间轨迹插值器

θ(t) = a₀ + a₁·t + a₂·t² + a₃·t³

约束:θ(0)=q_start, θ(T)=q_end, θ'(0)=v_start, θ'(T)=v_end

 

连续性说明:三次多项式保证C¹连续(位置和速度连续),

但加速度在段边界有阶跃(不是C²)。起终点加速度非零跳变。

"""

 

 

import numpy as np

from trajectory.models import JointWaypoint, JointLimitSet

 

 

class CubicPolynomialSegment:

    """

    单个关节的单个三次多项式段

    系数: θ(t) = a₀ + a₁·t + a₂·t² + a₃·t³

    """

 

    def __init__(self, q_start: float, q_end: float, duration: float,

                 qd_start: float = 0.0, qd_end: float = 0.0):

        if duration <= 0:

            raise ValueError(f"运动时间必须大于零, 实际{duration}")

        if not np.isfinite(q_start) or not np.isfinite(q_end):

            raise ValueError("起点/终点角度必须为有限数值")

        if not np.isfinite(qd_start) or not np.isfinite(qd_end):

            raise ValueError("起点/终点速度必须为有限数值")

 

        self.q_start = q_start

        self.q_end = q_end

        self.duration = duration

        self.qd_start = qd_start

        self.qd_end = qd_end

 

        # 求解系数(线性方程组通解,支持非零起终点速度)

        # θ(0) = a₀ = q_start

        # θ'(0) = a₁ = qd_start

        # θ(T) = a₀ + a₁·T + a₂·T² + a₃·T³ = q_end

        # θ'(T) = a₁ + 2·a₂·T + 3·a₃·T² = qd_end

        self.a0 = q_start

        self.a1 = qd_start

 

        T = duration

        mat = np.array([[T**2, T**3],

                        [2*T, 3*T**2]], dtype=float)

        rhs = np.array([q_end - self.a0 - self.a1 * T,

                        qd_end - self.a1], dtype=float)

        try:

            coeffs = np.linalg.solve(mat, rhs)

        except np.linalg.LinAlgError:

            raise ValueError(f"三次多项式系数矩阵奇异, duration={duration}")

 

        self.a2 = coeffs[0]

        self.a3 = coeffs[1]

 

    def evaluate(self, t: float) -> Tuple[float, float, float]:

        """计算 t 时刻的角度、角速度、角加速度"""

        if t < -1e-12 or t > self.duration + 1e-12:

            raise ValueError(f"时间t={t}超出范围[0, {self.duration}]")

 

        t = max(0.0, min(t, self.duration))

 

        theta = self.a0 + self.a1 * t + self.a2 * t**2 + self.a3 * t**3

        theta_dot = self.a1 + 2 * self.a2 * t + 3 * self.a3 * t**2

        theta_ddot = 2 * self.a2 + 6 * self.a3 * t

 

        return theta, theta_dot, theta_ddot

 

    def max_velocity(self) -> float:

        """

        计算该段的最大绝对速度(解析解)

        速度函数是二次函数 θ'(t) = a₁ + 2a₂t + 3a₃t²

        极值点出现在 t = -a₂/(3a₃)(如果在[0,T]内)或边界

        特例(零起终点速度):峰值 = 1.5|Δq|/T,出现在 t=T/2

        """

        candidates = [0.0, self.duration]

        if abs(self.a3) > 1e-12:

            t_extreme = -self.a2 / (3 * self.a3)

            if 0 < t_extreme < self.duration:

                candidates.append(t_extreme)

 

        max_v = 0.0

        for t in candidates:

            _, v, _ = self.evaluate(t)

            max_v = max(max_v, abs(v))

        return max_v

 

    def max_acceleration(self) -> float:

        """

        计算该段的最大绝对加速度(解析解)

        加速度是线性的 θ''(t) = 2a₂ + 6a₃t,极值在边界

        """

        _, _, a_start = self.evaluate(0.0)

        _, _, a_end = self.evaluate(self.duration)

        return max(abs(a_start), abs(a_end))

 

    def peak_velocity_formula(self) -> float:

        """

        零起终点速度时的解析峰值速度:1.5|Δq|/T

        用于校验max_velocity()计算结果

        """

        if abs(self.qd_start) < 1e-12 and abs(self.qd_end) < 1e-12:

            delta_q = self.q_end - self.q_start

            return 1.5 * abs(delta_q) / self.duration

        return self.max_velocity()

 

 

class CubicJointTrajectory:

    """

    多关节三次多项式轨迹生成器

    对每个关节独立生成三次多项式插值序列

    构造时校验起终点路点是否在位置限位内(尽早失败)

    """

 

    def __init__(self, start_waypoints: List[JointWaypoint],

                 end_waypoints: List[JointWaypoint],

                 duration: float, n_points: int = 100,

                 limits: JointLimitSet = None):

        if len(start_waypoints) != 3 or len(end_waypoints) != 3:

            raise ValueError("需要3个关节的起点和终点路点")

 

        self.duration = duration

        self.n_points = n_points

        self.times = np.linspace(0, duration, n_points)

        self.limits = limits

 

        # 构造时尽早校验起终点位置限位

        if limits is not None:

            start_angles = [wp.angle for wp in start_waypoints]

            end_angles = [wp.angle for wp in end_waypoints]

            start_ok, start_violations = limits.check_all_positions(start_angles)

            if not start_ok:

                err_msgs = []

                for vi in start_violations:

                    err_msgs.append(

                        f"起点关节'{limits.limits[vi].joint_name}' "

                        f"角度={start_angles[vi]:.3f}超出限位"

                        f"[{limits.limits[vi].min_val:.3f}, "

                        f"{limits.limits[vi].max_val:.3f}]"

                    )

                raise ValueError(";".join(err_msgs))

            end_ok, end_violations = limits.check_all_positions(end_angles)

            if not end_ok:

                err_msgs = []

                for vi in end_violations:

                    err_msgs.append(

                        f"终点关节'{limits.limits[vi].joint_name}' "

                        f"角度={end_angles[vi]:.3f}超出限位"

                        f"[{limits.limits[vi].min_val:.3f}, "

                        f"{limits.limits[vi].max_val:.3f}]"

                    )

                raise ValueError(";".join(err_msgs))

 

        self.segments = []

        for i in range(3):

            seg = CubicPolynomialSegment(

                q_start=start_waypoints[i].angle,

                q_end=end_waypoints[i].angle,

                duration=duration,

                qd_start=start_waypoints[i].velocity,

                qd_end=end_waypoints[i].velocity

            )

            self.segments.append(seg)

 

        # 批量计算

        self.q_array = np.zeros((n_points, 3))

        self.q_dot_array = np.zeros((n_points, 3))

        self.q_ddot_array = np.zeros((n_points, 3))

 

        for i, t in enumerate(self.times):

            for j in range(3):

                q, qd, qdd = self.segments[j].evaluate(t)

                self.q_array[i, j] = q

                self.q_dot_array[i, j] = qd

                self.q_ddot_array[i, j] = qdd

 

    def validate_limits(self, limits: JointLimitSet) -> Tuple[bool, List[str]]:

        """

        校验整条轨迹是否满足限位、速度限、加速度限

        返回 (all_pass, messages)

        """

        all_pass = True

        messages = []

 

        for i in range(self.n_points):

            t = self.times[i]

            q_vals = self.q_array[i, :].tolist()

            qd_vals = self.q_dot_array[i, :].tolist()

            qdd_vals = self.q_ddot_array[i, :].tolist()

 

            # 位置校验

            pos_ok, pos_violations = limits.check_all_positions(q_vals)

            if not pos_ok:

                all_pass = False

                for vi in pos_violations:

                    messages.append(

                        f"t={t:.3f}s: 关节'{limits.limits[vi].joint_name}' "

                        f"角度={q_vals[vi]:.3f}超出限位"

                        f"[{limits.limits[vi].min_val:.3f}, "

                        f"{limits.limits[vi].max_val:.3f}]"

                    )

 

            # 速度校验

            vel_ok, vel_violations = limits.check_all_velocities(qd_vals)

            if not vel_ok:

                all_pass = False

                for vi in vel_violations:

                    messages.append(

                        f"t={t:.3f}s: 关节'{limits.limits[vi].joint_name}' "

                        f"角速度={qd_vals[vi]:.3f}超出限制"

                        f"{limits.limits[vi].max_velocity:.3f}"

                    )

 

            # 加速度校验

            acc_ok, acc_violations = limits.check_all_accelerations(qdd_vals)

            if not acc_ok:

                all_pass = False

                for vi in acc_violations:

                    messages.append(

                        f"t={t:.3f}s: 关节'{limits.limits[vi].joint_name}' "

                        f"角加速度={qdd_vals[vi]:.3f}超出限制"

                        f"{limits.limits[vi].max_acceleration:.3f}"

                    )

 

        return all_pass, messages

 

    def get_peak_values(self) -> dict:

        """返回各关节的峰值角度、速度、加速度"""

        return {

            'q_peak': np.max(np.abs(self.q_array), axis=0),

            'qd_peak': np.max(np.abs(self.q_dot_array), axis=0),

            'qdd_peak': np.max(np.abs(self.q_ddot_array), axis=0),

        }

 

    @classmethod

    def from_angle_pairs(cls, q_start: List[float], q_end: List[float],

                         duration: float, n_points: int = 100,

                         limits: JointLimitSet = None):

        """

        便捷工厂方法:直接从角度列表创建轨迹

        """

        if len(q_start) != 3 or len(q_end) != 3:

            raise ValueError("需要3个关节的起点和终点角度")

 

        start_waypoints = [

            JointWaypoint(q_start[0], 0.0, 'joint1_base_yaw'),

            JointWaypoint(q_start[1], 0.0, 'joint2_elbow_pitch'),

            JointWaypoint(q_start[2], 0.0, 'joint3_end_rotate'),

        ]

        end_waypoints = [

            JointWaypoint(q_end[0], 0.0, 'joint1_base_yaw'),

            JointWaypoint(q_end[1], 0.0, 'joint2_elbow_pitch'),

            JointWaypoint(q_end[2], 0.0, 'joint3_end_rotate'),

        ]

 

        return cls(start_waypoints, end_waypoints, duration, n_points, limits)

 

"trajectory/visualize.py" — 可视化模块

 

"""

点位搬运仿真结果可视化:4张核心图表

"""

 

 

import numpy as np

import matplotlib.pyplot as plt

 

plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']

plt.rcParams['axes.unicode_minus'] = False

 

 

class Visualizer:

    def __init__(self, results_dir="results"):

        self.results_dir = results_dir

        import os

        os.makedirs(results_dir, exist_ok=True)

 

    def plot_joint_angles(self, times, q_array):

        """

        ① 关节角度曲线:验证从起点平滑走到终点

        """

        fig, axes = plt.subplots(3, 1, figsize=(10, 9), sharex=True)

 

        joint_names = ['Joint1 (Base Yaw)', 'Joint2 (Elbow Pitch)', 'Joint3 (End Rotate)']

        colors = ['steelblue', 'red', 'green']

 

        for j_idx, (ax, name, color) in enumerate(zip(axes, joint_names, colors)):

            ax.plot(times, q_array[:, j_idx] * 180 / np.pi, '-',

                     color=color, linewidth=1.5)

            ax.set_ylabel(f'{name}\nAngle (deg)')

            ax.set_title(f'{name.split("(")[0].strip()} — Angle Profile')

            ax.grid(True, alpha=0.3)

 

        axes[2].set_xlabel('Time (s)')

        plt.tight_layout()

        path = f"{self.results_dir}/01_joint_angles.png"

        plt.savefig(path, dpi=150)

        plt.close()

        return path

 

    def plot_joint_velocities(self, times, q_dot_array):

        """

        ② 关节角速度曲线:验证起终点为零、中间平滑(C¹连续)

        """

        fig, axes = plt.subplots(3, 1, figsize=(10, 9), sharex=True)

 

        joint_names = ['Joint1 (Base Yaw)', 'Joint2 (Elbow Pitch)', 'Joint3 (End Rotate)']

        colors = ['steelblue', 'red', 'green']

 

        for j_idx, (ax, name, color) in enumerate(zip(axes, joint_names, colors)):

            ax.plot(times, q_dot_array[:, j_idx], '-',

                     color=color, linewidth=1.5)

            ax.axhline(y=0, color='black', linestyle='--', linewidth=0.8, alpha=0.5)

            ax.set_ylabel(f'{name}\nVelocity (rad/s)')

            ax.set_title(f'{name.split("(")[0].strip()} — Velocity Profile (C¹ Continuous)')

            ax.grid(True, alpha=0.3)

 

        axes[2].set_xlabel('Time (s)')

        plt.tight_layout()

        path = f"{self.results_dir}/02_joint_velocities.png"

        plt.savefig(path, dpi=150)

        plt.close()

        return path

 

    def plot_joint_accelerations(self, times, q_ddot_array):

        """

        ③ 关节角加速度曲线:验证线性变化,注意起终点有阶跃(C¹但不C²)

        """

        fig, axes = plt.subplots(3, 1, figsize=(10, 9), sharex=True)

 

        joint_names = ['Joint1 (Base Yaw)', 'Joint2 (Elbow Pitch)', 'Joint3 (End Rotate)']

        colors = ['steelblue', 'red', 'green']

 

        for j_idx, (ax, name, color) in enumerate(zip(axes, joint_names, colors)):

            ax.plot(times, q_ddot_array[:, j_idx], '-',

                     color=color, linewidth=1.5)

            ax.axhline(y=0, color='black', linestyle='--', linewidth=0.8, alpha=0.5)

            # 标注起点加速度跳变

            ax.axvline(x=0, color='orange', linestyle=':', linewidth=1.0, alpha=0.7)

            ax.axvline(x=times[-1], color='orange', linestyle=':', linewidth=1.0, alpha=0.7)

利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!

 

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐