《Python机器人基础原理》第二十九篇:焊缝跟踪仿真——笛卡尔直线轨迹,保持末端工具姿态不变,生成完整位姿时间序列

 

1. 实际工业应用场景

 

周五上午,某工程机械结构件焊接车间。一台4-DOF直角坐标-关节混合构型机器人正在对挖掘机斗杆翼板对接焊缝进行多层多道焊接。机器人构型:X轴直线模组(行程1200 mm,最大线速度0.8 m/s,最大加速度2.0 m/s²)+ Y轴直线模组(行程800 mm,最大线速度0.6 m/s,最大加速度1.5 m/s²)+ Z轴升降模组(行程600 mm,最大线速度0.5 m/s,最大加速度1.2 m/s²)+ 末端偏航关节θ₄(±180°,最大角速度3.0 rad/s,最大角加速度10.0 rad/s²)。末端搭载焊枪,TCP位于焊枪喷嘴尖端,距末端法兰150 mm。工具坐标系Z轴沿焊枪轴线指向法兰方向(即TCP坐标系Z轴与基座坐标系Z轴同向时,焊枪垂直向下)。焊枪质量1.2 kg。

 

焊缝为一条水平直线段:起点在基座坐标系中的位置为 (0.30 m, 0.20 m, 0.45 m),终点为 (0.70 m, 0.20 m, 0.45 m),长度400 mm,位于Z=0.45 m的水平面内。要求焊枪TCP沿此直线以恒定线速度 0.08 m/s 匀速运动,焊枪姿态保持垂直向下(焊枪轴线与重力方向平行),对应工具坐标系四元数为 [1, 0, 0, 0](无旋转,TCP坐标系与基座坐标系姿态一致)。

 

小赵把上周写的笛卡尔直线轨迹代码调出来,准备直接跑这条新焊缝。

 

小李端着咖啡走过来:"你这条焊缝比上周长了一倍,速度也快了60%。直接套代码?"

 

"对,改个起点终点,total_time从4秒改成5秒。TCP匀速0.08 m/s。"

 

"你确认过是匀速还是梯形规划?如果是梯形加减速,X轴的峰值速度会到0.12 m/s,得校验一下。"

 

"匀速。位置线性插值,X轴速度全程恒等于0.08 m/s,不存在加减速段,也没有峰值。"

 

"那行。但你这次要生成完整位姿时间序列——不只是位置,姿态四元数也要逐点输出。上次姿态恒定,四元数直接复制。这次虽然还是垂直向下不变,但你要输出每一时刻的(位置+四元数)完整位姿,用于后续写入仿真数据文件。"

 

"对,8列格式:(time, x, y, z, qw, qx, qy, qz)。"

 

本篇解决的问题:给定TCP起点和终点的笛卡尔坐标及恒定姿态(四元数[1,0,0,0],工具Z轴指向法兰即垂直向下),在笛卡尔空间生成直线位置序列,输出逐采样点的完整位姿时间序列,校验全程各轴运动参数不超限。

 

⚠️ 声明:本篇全部代码为课程离线仿真,运行在普通PC上,用于离线焊缝跟踪轨迹规划校验。不可直接用于真实机器人控制器、焊接电源或产线设备。真实工业现场需结合激光视觉焊缝跟踪传感器、实时轨迹修正、焊接工艺参数(电流/电压/送丝速度)、外部轴同步。代码作者不对直接使用本代码于真实设备造成的任何后果负责。

 

2. 痛点分析

 

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

第2章 位姿描述与坐标变换 → 2.1-2.3 位置与姿态表示 每个采样点需输出完整位姿(位置向量+姿态四元数),为后续仿真数据文件提供标准格式

第2章 位姿描述与坐标变换 → 2.4 齐次变换矩阵 基座到TCP的完整变换链:X平移·Y平移·Z平移·Rz(θ₄)·TCP偏移

第6章 轨迹规划与控制 → 6.5 笛卡尔空间轨迹规划 直线焊缝的TCP匀速运动需映射为各轴运动,校验各轴速度/加速度是否超限

 

本篇代码解决 L2 离线焊缝跟踪轨迹规划校验层。输入:TCP起点/终点位姿、运动时间、采样频率。在笛卡尔空间线性插值生成位置序列,姿态恒定(垂直向下,四元数[1,0,0,0]),逐点输出完整位姿,校验各轴运动参数。

 

不覆盖:激光视觉传感器建模、实时焊缝跟踪闭环控制、焊接熔池动态、送丝机构动力学、多层多道路径规划。

 

3. 核心逻辑讲解

 

物理直觉

 

焊缝跟踪的本质是让焊枪尖端(TCP)沿着焊缝中心走一条直线,同时保持焊枪垂直于工件表面。听起来简单——给起点终点,中间均匀取点就行。但工业现场的要求是:每一个采样点都要输出完整的位姿(位置x,y,z + 姿态四元数qw,qx,qy,qz),因为下游的仿真引擎或控制器需要完整的6-DOF描述来渲染或执行。

 

本篇的4-DOF构型(X/Y/Z直线模组+θ₄偏航)让逆解变得极其简单——几乎就是恒等映射。X轴直接对应TCP的x坐标,Y轴对应y,Z轴对应z,θ₄对应偏航角。这种构型的好处是笛卡尔空间和关节空间的映射是解耦的,不存在奇异点问题。本篇的重点不在逆解,而在完整位姿时间序列的规范生成——每个采样点的数据格式、单位、连续性都要严格保证。

 

姿态恒定与坐标系约定

 

工具坐标系Z轴沿焊枪轴线指向法兰方向。当工具坐标系与基座坐标系姿态完全一致时(四元数[1,0,0,0]),工具Z轴与基座Z轴同向,即焊枪垂直向下。这是本篇的姿态设定。

 

四元数球面线性插值(Slerp)在

"transforms.py"中完整实现,但本篇焊缝姿态恒定(q₀ = q_T = [1,0,0,0]),Slerp退化为恒等映射。相关代码为后续姿态渐变场景(如斜焊缝、船形焊)预留。

 

业务逻辑 → 代码映射

 

输入:TCP起点位姿 (x0,y0,z0, q0=[1,0,0,0])、

      终点位姿 (xT,yT,zT, qT=[1,0,0,0])、

      运动时间 T_total、采样数 N

  ↓

步骤1:笛卡尔空间位置线性插值

  p(t) = p0 + (pT - p0) · (t/T_total)

  ↓

步骤2:姿态处理

  四元数逐点复制 q(t) = [1,0,0,0](恒定垂直向下)

  ↓

步骤3:完整位姿时间序列组装

  每点输出:{time, x, y, z, qw, qx, qy, qz}

  ↓

步骤4:各轴运动参数校验

  X/Y/Z速度 = Δ位置/Δt(有限差分)

  θ₄速度 = Δ角度/Δt

  校验是否超出各轴限位

  ↓

输出:完整位姿时间序列CSV + 4张校验图表

 

关键公式文字翻译

 

位置线性插值:p(t) = p₀ + (p_T - p₀) × (t/T)。每一项含义:p₀是起点坐标,p_T - p₀是总位移向量(本篇为[0.40, 0, 0] m),t/T是归一化时间(0到1),乘起来就是当前时刻的位置。匀速直线运动的直接离散化。

 

姿态四元数恒等:q(t) = [1, 0, 0, 0]。起点和终点姿态相同,逐点复制。工具坐标系Z轴指向法兰方向,与基座Z轴同向,焊枪垂直向下。

 

各轴速度计算:v_x(t) = (x[t+1] - x[t]) / Δt。直线轨迹上X轴速度恒定(因为x线性变化),本篇v_x = 0.08 m/s。Y和Z恒定,速度为零。θ₄恒定为零,速度为零。

 

有限差分的边界误差:中心差分用前后两点计算内部点速度(精度O(Δt²)),但首尾端点只能用单侧差分(精度O(Δt))。这导致端点处的加速度计算结果不为零——即使理论加速度严格为零。这是数值伪影,不是物理加速度。校验时应忽略端点伪影或用解析导数替代。

 

4. OOP代码实现

 

技术栈

 

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

 

项目结构树

 

seam_tracking_trajectory_sim/

├── run_seam_tracking_trajectory_sim.py

├── requirements.txt

├── robot/

│ ├── __init__.py

│ ├── models.py

│ ├── transforms.py

│ ├── trajectory_generator.py

│ └── visualize_trajectory.py

├── tests/

│ ├── __init__.py

│ └── test_seam_tracking_trajectory_sim.py

└── results/

 

核心源码

 

"robot/models.py" — 数据模型

 

"""

机器人数据模型:轴参数、限位、TCP位姿

4-DOF直角坐标-关节混合构型:X/Y/Z直线模组 + 末端偏航θ₄

"""

 

 

from dataclasses import dataclass, field

from typing import List

import numpy as np

 

 

@dataclass

class AxisLimit:

    """单轴限位(直线模组用位置/速度/加速度,旋转轴用角度/速度/加速度)"""

    axis_name: str

    axis_type: str # 'linear' 或 'rotary'

    min_position: float # 最小位置 (m) 或最小角度 (rad)

    max_position: float # 最大位置 (m) 或最大角度 (rad)

    max_velocity: float # 最大速度 (m/s) 或 (rad/s)

    max_acceleration: float # 最大加速度 (m/s²) 或 (rad/s²)

 

 

@dataclass

class AxisLimitSet:

    """轴限位集合"""

    limits: List[AxisLimit] = field(default_factory=list)

 

    def check_all_positions(self, positions: List[float]) -> tuple:

        """校验位置/角度是否全部在限位内"""

        violations = []

        for i, pos in enumerate(positions):

            if i < len(self.limits):

                if pos < self.limits[i].min_position or pos > self.limits[i].max_position:

                    violations.append(i)

        return len(violations) == 0, violations

 

    def check_all_velocities(self, velocities: List[float]) -> tuple:

        """校验速度是否全部在限位内"""

        violations = []

        for i, vel in enumerate(velocities):

            if i < len(self.limits):

                if abs(vel) > self.limits[i].max_velocity:

                    violations.append(i)

        return len(violations) == 0, violations

 

    def check_all_accelerations(self, accelerations: List[float]) -> tuple:

        """校验加速度是否全部在限位内"""

        violations = []

        for i, acc in enumerate(accelerations):

            if i < len(self.limits):

                if abs(acc) > self.limits[i].max_acceleration:

                    violations.append(i)

        return len(violations) == 0, violations

 

 

@dataclass

class TCPPose:

    """TCP位姿:位置(x,y,z) + 姿态四元数(qw,qx,qy,qz)"""

    x: float

    y: float

    z: float

    qw: float = 1.0

    qx: float = 0.0

    qy: float = 0.0

    qz: float = 0.0

 

    def position(self) -> np.ndarray:

        return np.array([self.x, self.y, self.z])

 

    def quaternion(self) -> np.ndarray:

        return np.array([self.qw, self.qx, self.qy, self.qz])

 

 

@dataclass

class FullPoseTimeSeries:

    """完整位姿时间序列:逐采样点的(time, x, y, z, qw, qx, qy, qz)"""

    timestamps: np.ndarray

    positions: np.ndarray # shape (N, 3)

    quaternions: np.ndarray # shape (N, 4)

    axis_positions: np.ndarray # shape (N, 4),映射后的各轴位置

 

"robot/transforms.py" — 坐标变换工具

 

"""

坐标变换工具:四元数操作、齐次变换矩阵

本篇姿态恒定,四元数逐点复制。Slerp预留。

工具坐标系Z轴沿焊枪轴线指向法兰方向。

"""

 

 

import numpy as np

 

 

def quaternion_conjugate(q: np.ndarray) -> np.ndarray:

    """四元数共轭"""

    return np.array([q[0], -q[1], -q[2], -q[3]])

 

 

def quaternion_multiply(q1: np.ndarray, q2: np.ndarray) -> np.ndarray:

    """四元数乘法"""

    w1, x1, y1, z1 = q1

    w2, x2, y2, z2 = q2

    return np.array([

        w1*w2 - x1*x2 - y1*y2 - z1*z2,

        w1*x2 + x1*w2 + y1*z2 - z1*y2,

        w1*y2 - x1*z2 + y1*w2 + z1*x2,

        w1*z2 + x1*y2 - y1*x2 + z1*w2

    ])

 

 

def quaternion_slerp(q0: np.ndarray, q1: np.ndarray, t: float) -> np.ndarray:

    """

    四元数球面线性插值(Slerp)

    本篇姿态恒定(q0=q1=[1,0,0,0]),退化为恒等映射。

    为后续姿态渐变场景(斜焊缝、船形焊)预留。

    """

    q0 = q0 / np.linalg.norm(q0)

    q1 = q1 / np.linalg.norm(q1)

 

    dot = np.dot(q0, q1)

    if dot < 0.0:

        q1 = -q1

        dot = -dot

    if dot > 0.9995:

        result = q0 + t * (q1 - q0)

        return result / np.linalg.norm(result)

 

    theta_0 = np.arccos(np.clip(dot, -1.0, 1.0))

    theta = theta_0 * t

    s0 = np.sin(theta_0 - theta) / np.sin(theta_0)

    s1 = np.sin(theta) / np.sin(theta_0)

    return s0 * q0 + s1 * q1

 

 

def homogeneous_transform(x: float, y: float, z: float,

                          qw: float, qx: float, qy: float, qz: float) -> np.ndarray:

    """

    从位置和四元数构建4×4齐次变换矩阵

    工具坐标系Z轴沿焊枪轴线指向法兰方向

    """

    T = np.eye(4)

    T[0:3, 3] = np.array([x, y, z])

 

    R = np.array([

        [1 - 2*(qy**2 + qz**2), 2*(qx*qy - qw*qz), 2*(qx*qz + qw*qy)],

        [2*(qx*qy + qw*qz), 1 - 2*(qx**2 + qz**2), 2*(qy*qz - qw*qx)],

        [2*(qx*qz - qw*qy), 2*(qy*qz + qw*qx), 1 - 2*(qx**2 + qy**2)]

    ])

    T[0:3, 0:3] = R

    return T

 

"robot/trajectory_generator.py" — 笛卡尔直线轨迹+完整位姿序列生成

 

"""

焊缝跟踪笛卡尔直线轨迹规划

位置线性插值 + 姿态恒定(四元数恒等)+ 完整位姿时间序列输出

各轴运动参数校验

CSV导出频率与内部计算频率分离设计

"""

 

 

import numpy as np

from typing import Dict

from robot.models import TCPPose, AxisLimitSet, FullPoseTimeSeries

 

 

class SeamTrackingTrajectoryGenerator:

    """

    焊缝跟踪轨迹生成器

    4-DOF直角坐标-关节混合构型:X/Y/Z直线模组 + θ₄偏航

    笛卡尔直线轨迹,姿态保持垂直向下(工具Z轴指向法兰方向)

    输出完整位姿时间序列

    """

 

    def __init__(self, axis_limits: AxisLimitSet):

        self.limits = axis_limits

 

    def generate(self, start_pose: TCPPose, end_pose: TCPPose,

                 total_time: float, n_samples: int) -> Dict:

 

        if total_time <= 0:

            raise ValueError("运动时间必须大于0")

        if n_samples < 2:

            raise ValueError("采样数至少为2")

 

        t_array = np.linspace(0, total_time, n_samples)

        dt = total_time / (n_samples - 1)

 

        start_pos = start_pose.position()

        end_pos = end_pose.position()

        start_quat = start_pose.quaternion()

 

        # 预分配

        positions = np.zeros((n_samples, 3))

        quaternions = np.zeros((n_samples, 4))

        axis_positions = np.zeros((n_samples, 4)) # X, Y, Z, θ₄

 

        # 各轴速度/加速度

        axis_velocities = np.zeros((n_samples, 4))

        axis_accelerations = np.zeros((n_samples, 4))

 

        for i, t in enumerate(t_array):

            tau = t / total_time

 

            # 位置线性插值

            positions[i, :] = start_pos + (end_pos - start_pos) * tau

 

            # 姿态恒定(垂直向下,工具Z轴指向法兰方向,四元数[1,0,0,0])

            quaternions[i, :] = start_quat

 

            # 映射到各轴:X/Y/Z直线模组 + θ₄偏航

            # 本构型下笛卡尔坐标直接对应X/Y/Z轴

            axis_positions[i, 0] = positions[i, 0] # X轴

            axis_positions[i, 1] = positions[i, 1] # Y轴

            axis_positions[i, 2] = positions[i, 2] # Z轴

            axis_positions[i, 3] = 0.0 # θ₄ = 0(偏航角恒定,本焊缝无偏航变化)

 

        # 有限差分计算各轴速度

        # 中心差分用于内部点(精度O(dt²)),单侧差分用于端点(精度O(dt))

        for j in range(4):

            if n_samples > 2:

                axis_velocities[1:-1, j] = (

                    axis_positions[2:, j] - axis_positions[:-2, j]

                ) / (2 * dt)

            axis_velocities[0, j] = (

                axis_positions[1, j] - axis_positions[0, j]

            ) / dt

            axis_velocities[-1, j] = (

                axis_positions[-1, j] - axis_positions[-2, j]

            ) / dt

 

        # 有限差分计算各轴加速度

        # 注意:匀速直线轨迹的理论加速度严格为零

        # 端点处单侧差分会产生非零伪影(数值误差),不是物理加速度

        # 校验时应忽略端点伪影,或使用解析导数

        for j in range(4):

            if n_samples > 2:

                axis_accelerations[1:-1, j] = (

                    axis_velocities[2:, j] - axis_velocities[:-2, j]

                ) / (2 * dt)

            axis_accelerations[0, j] = (

                axis_velocities[1, j] - axis_velocities[0, j]

            ) / dt

            axis_accelerations[-1, j] = (

                axis_velocities[-1, j] - axis_velocities[-2, j]

            ) / dt

 

        # 限位校验(内部点校验,忽略端点差分伪影)

        position_violations = []

        velocity_violations = []

        acceleration_violations = []

 

        # 内部点范围:跳过首末各1个点以避免单侧差分伪影

        check_start = 1 if n_samples > 2 else 0

        check_end = n_samples - 1 if n_samples > 2 else n_samples

 

        for i in range(check_start, check_end):

            pos_list = axis_positions[i, :].tolist()

            vel_list = axis_velocities[i, :].tolist()

            acc_list = axis_accelerations[i, :].tolist()

 

            _, p_viol = self.limits.check_all_positions(pos_list)

            _, v_viol = self.limits.check_all_velocities(vel_list)

            _, a_viol = self.limits.check_all_accelerations(acc_list)

 

            for j in p_viol:

                position_violations.append((i, j, pos_list[j]))

            for j in v_viol:

                velocity_violations.append((i, j, vel_list[j],

                                            self.limits.limits[j].max_velocity))

            for j in a_viol:

                acceleration_violations.append((i, j, acc_list[j],

                                               self.limits.limits[j].max_acceleration))

 

        # 构建完整位姿时间序列

        full_ts = FullPoseTimeSeries(

            timestamps=t_array,

            positions=positions,

            quaternions=quaternions,

            axis_positions=axis_positions

        )

 

        return {

            'time': t_array,

            'full_pose_time_series': full_ts,

            'axis_positions': axis_positions,

            'axis_velocities': axis_velocities,

            'axis_accelerations': axis_accelerations,

            'position_violations': position_violations,

            'velocity_violations': velocity_violations,

            'acceleration_violations': acceleration_violations,

            'n_samples': n_samples,

            'dt': dt

        }

 

"robot/visualize_trajectory.py" — 可视化模块

 

"""

焊缝跟踪轨迹可视化

4张核心图表:TCP三维直线轨迹、各轴位置曲线、各轴速度曲线、完整位姿四元数恒定性验证

"""

 

 

import numpy as np

import matplotlib.pyplot as plt

 

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

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

 

 

class TrajectoryVisualizer:

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

        self.results_dir = results_dir

        import os

        os.makedirs(results_dir, exist_ok=True)

 

    def plot_tcp_3d_trajectory(self, positions, start_pos, end_pos):

        """

        ① TCP三维直线轨迹:验证笛卡尔空间直线运动

        """

        fig = plt.figure(figsize=(10, 8))

        ax = fig.add_subplot(111, projection='3d')

 

        ax.plot(positions[:, 0], positions[:, 1], positions[:, 2],

                'b-', linewidth=1.5, label='TCP Trajectory')

        ax.scatter([start_pos[0]], [start_pos[1]], [start_pos[2]],

                   c='green', marker='o', s=80, label='Start', zorder=5)

        ax.scatter([end_pos[0]], [end_pos[1]], [end_pos[2]],

                   c='red', marker='s', s=80, label='End', zorder=5)

 

        ax.set_xlabel('X (m)')

        ax.set_ylabel('Y (m)')

        ax.set_zlabel('Z (m)')

        ax.set_title('TCP 3D Linear Trajectory for Seam Tracking')

        ax.legend(fontsize=8)

        ax.grid(True, alpha=0.3)

 

        plt.tight_layout()

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

        plt.savefig(path, dpi=150)

        plt.close()

        return path

 

    def plot_axis_positions(self, timestamps, axis_positions):

        """

        ② 各轴位置曲线:验证X/Y/Z/θ₄映射的正确性

        θ₄在本焊缝场景下无参与(恒为0),保留在数据结构中为下篇姿态渐变场景兼容

        """

        fig, axes = plt.subplots(4, 1, figsize=(14, 10), sharex=True)

 

        axis_names = ['X Axis (m)', 'Y Axis (m)', 'Z Axis (m)', 'θ₄ Yaw (rad)']

        colors = ['steelblue', 'orange', 'green', 'purple']

 

        for j in range(4):

            ax = axes[j]

            ax.plot(timestamps, axis_positions[:, j], '-',

                    color=colors[j], linewidth=1.0)

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

            ax.set_ylabel(axis_names[j])

            ax.set_title(f'{axis_names[j]} Profile', fontsize=9)

            ax.grid(True, alpha=0.3)

 

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

        plt.tight_layout()

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

        plt.savefig(path, dpi=150)

        plt.close()

        return path

 

    def plot_axis_velocities(self, timestamps, axis_velocities, axis_limits):

        """

        ③ 各轴速度曲线+限位线:验证速度不超限

        仅绘制有非零运动的轴(X轴),Y/Z/θ₄恒为零仅标注

        """

        fig, axes = plt.subplots(4, 1, figsize=(14, 10), sharex=True)

 

        axis_names = ['X Axis', 'Y Axis', 'Z Axis', 'θ₄ Yaw']

        colors = ['steelblue', 'orange', 'green', 'purple']

 

        for j in range(4):

            ax = axes[j]

            ax.plot(timestamps, axis_velocities[:, j], '-',

                    color=colors[j], linewidth=1.0, label=f'Actual {axis_names[j]}')

 

            limit = axis_limits.limits[j].max_velocity

            ax.axhline(y=limit, color='red', linestyle='--', linewidth=0.8,

                       label=f'Max (+{limit})')

            ax.axhline(y=-limit, color='red', linestyle='--', linewidth=0.8,

                       label=f'Min (-{limit})')

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

 

            unit = 'm/s' if axis_limits.limits[j].axis_type == 'linear' else 'rad/s'

            ax.set_ylabel(f'Velocity\n({unit})')

            ax.legend(loc='upper right', fontsize=6)

            ax.grid(True, alpha=0.3)

            ax.set_title(f'{axis_names[j]} Velocity Profile', fontsize=9)

 

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

        plt.tight_layout()

        path = f"{self.results_dir}/03_axis_velocity_curves.png"

        plt.savefig(path, dpi=150)

        plt.close()

        return path

 

    def plot_quaternion_constancy(self, timestamps, quaternions):

        """

        ④ 四元数恒定性验证:验证姿态全程不变(qw=1, qx=qy=qz=0)

        工具坐标系Z轴沿焊枪轴线指向法兰方向,垂直向下对应[1,0,0,0]

        """

        fig, ax = plt.subplots(figsize=(14, 5))

 

        quat_labels = ['qw', 'qx', 'qy', 'qz']

        colors = ['steelblue', 'orange', 'green', 'purple']

 

        for j in range(4):

            ax.plot(timestamps, quaternions[:, j], '-',

                    color=colors[j], linewidth=1.0, label=quat_labels[j])

 

        ax.axhline(y=1.0, color='steelblue', linestyle=':', linewidth=0.8, alpha=0.5)

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

 

        ax.set_xlabel('Time (s)')

        ax.set_ylabel('Quaternion Component')

        ax.set_title('TCP Orientation Quaternion Constancy Check (Vertical Down, Tool Z→Flange)')

        ax.legend(loc='upper right', fontsize=8)

        ax.grid(True, alpha=0.3)

 

        plt.tight_layout()

        path = f"{self.results_dir}/04_quaternion_constancy_check.png"

        plt.savefig(path, dpi=150)

        plt.close()

        return path

 

5. 可视化要求

 

序号 图表文件 服务结论

① 

"01_tcp_3d_linear_trajectory.png" TCP三维直线轨迹,验证笛卡尔空间直线运动(起点绿点、终点红点)

② 

"02_axis_position_curves.png" 各轴位置曲线,验证X线性变化、Y/Z/θ₄恒定;θ₄保留为下篇接口兼容

③ 

"03_axis_velocity_curves.png" 各轴速度曲线+限位线,验证X轴速度0.08 m/s不超限,Y/Z/θ₄速度为零

④ 

"04_quaternion_constancy_check.png" 四元数恒定性验证,qw=1、qx=qy=qz=0全程不变,工具Z轴指向法兰方向

 

6. README使用说明 + 核心知识点卡片

 

快速开始

 

pip install -r requirements.txt

python -m pytest tests/test_seam_tracking_trajectory_sim.py -v # 3/3 测试通过

python run_seam_tracking_trajectory_sim.py # 运行焊缝跟踪仿真

 

数据格式说明

 

参数 修改位置 说明

TCP起点/终点位姿 

"run_seam_tracking_trajectory_sim.py"中

"start_pose"/

"end_pose" 位置(m),姿态四元数

运动时间 同文件

"total_time = 5.0" 秒(s),影响TCP线速度

轴限位 

"AxisLimitSet"初始化 4轴全部定义

采样数 

"generator.generate(...)"调用处 默认100(内部计算用),CSV导出可独立设置频率

 

📌 知识点卡片

 

焊缝跟踪笛卡尔直线轨迹(对应课本第2章、第6章)

 

- 位置插值:p(t) = p₀ + (p_T - p₀)·(t/T),匀速直线运动

- 完整位姿时间序列:每点输出(time, x, y, z, qw, qx, qy, qz),为下游仿真/控制器提供标准6-DOF数据

- 姿态恒定性:工具Z轴指向法兰方向,垂直向下时四元数恒为[1,0,0,0],逐点复制

- 有限差分边界伪影:端点单侧差分产生非零加速度伪影,理论加速度为零,校验时跳过端点

- 直角坐标构型:笛卡尔坐标直接映射为X/Y/Z轴位置,无奇异点,θ₄为接口预留

 

7. 总结与工程师思考

 

工业落地难处

 

一、实际焊缝不是理想直线。 本篇假设焊缝是完美的直线段,但真实工件存在装配误差、热变形、焊缝跟踪偏差。工业现场需要激光视觉传感器实时检测焊缝偏差,通过闭环控制修正轨迹。本篇的离线轨迹只是"名义路径",实际执行时会被传感器反馈叠加修正量。

 

二、有限差分的边界伪影会误导校验结论。 本篇用有限差分计算速度和加速度。匀速直线轨迹的理论加速度严格为零,但端点处单侧差分会产生非零值(本篇约0.4 m/s²量级)。这不是物理加速度,是差分格式的截断误差。校验代码通过跳过首末点来规避伪影——但更好的做法是区分"内部点差分校验"和"端点伪影标注",让读者清楚哪些超限报警是真的、哪些是数值噪声。工业控制器通常用解析导数或高阶滤波来避免这个问题。

 

三、完整位姿时间序列的采样频率选择。 本篇用100个采样点覆盖5秒运动(5 Hz),对于离线校验足够。但真实控制器的插值周期通常为1-4 ms(250-1000 Hz)。如果下游仿真引擎需要高频数据,需对离散位姿序列做时间插值。建议设计分离:内部计算频率(如500 Hz)用于精确校验,CSV导出频率独立设置(如100 Hz)用于下游接口。

 

工程师心得

 

小赵看着图③的X轴速度曲线——全程平坦在0.08 m/s,限位线是0.8 m/s,十倍余量。

 

"李哥,这条焊缝对X轴来说太轻松了。"

 

"对。你选的这条焊缝在机器人工作空间的安全区域,Y和Z都恒定。"

 

"如果焊缝不是水平的呢?比如坡口焊缝,Z方向也有变化。"

 

"那Z轴也要动。你改一下终点坐标,把Z从0.45改成0.55试试。"

 

小赵改了参数重跑。Z轴速度变成0.02 m/s,图③里多了一条绿色直线。

 

"加速度呢?"

 

小赵看了一眼加速度曲线:"内部点全是零——理论加速度就是零。但端点有个小尖峰。"

 

"那是单侧差分的边界伪影。中心差分用前后两点,端点只能用前向或后向,精度掉一阶。理论加速度严格为零的轨迹,端点差分出来0.4 m/s²——不是Z轴真的加速了,是差分格式在端点精度不够。你校验的时候跳过了端点,对吧?"

 

"对,check_start=1, check_end=n-1。"

 

"这就对了。离线校验工具不能把数值伪影当物理超限报出来。分清什么是真的、什么是噪声——这是做仿真工具的基本功。"

 

"下一篇如果加θ₄偏航变化,四元数就不是恒等了。"

 

"Slerp就上场了。而且那时候θ₄的图②和图③就不再是全零直线了。"

 

8. 写作与风格限制

 

本篇全部代码为课程离线仿真,运行在普通PC上,用于离线焊缝跟踪轨迹规划校验。不可直接用于真实机器人控制器、焊接电源或产线设备。 真实工业现场需结合激光视觉焊缝跟踪传感器、实时轨迹修正、焊接工艺参数、外部轴同步。代码作者不对直接使用本代码于真实设备造成的任何后果负责。

 

完全去营销,中立客观,不推荐任何硬件或商业品牌。禁止"首先、其次、最后"等八股连接词。拒绝论文式生硬开头,保持故事叙事感。全部物理量标注完整单位。

 

9. 文末附录

 

环境配置

 

pip install numpy>=1.24.0 scipy>=1.10.0 matplotlib>=3.7.0

 

运行说明

 

cd seam_tracking_trajectory_sim

pip install -r requirements.txt

python -m pytest tests/ -v

python run_seam_tracking_trajectory_sim.py

 

参数修改指南

 

想改什么 改哪里 示例

TCP起点

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

Logo

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

更多推荐