《Python机器人基础原理》第十二篇:工业机械臂仿真——输入关节角度计算雅可比矩阵,解析末端运动速度

 

1. 实际工业应用场景

 

周三下午,某汽车零部件焊接产线。一台4-DOF关节型工业机械臂正在对变速箱壳体进行点焊作业。机器人构型:3旋转+1垂直平移(SCARA变体),大臂长500 mm,小臂长400 mm,基座安装高度d₁=350 mm,z轴行程±150 mm。关节硬件限位:θ₁ ±160°、θ₂ ±120°、θ₃ ±180°、d₄ ±150 mm。额定负载12 kg,末端焊枪质量1.5 kg。焊接工艺要求末端线速度控制在80~120 mm/s之间。

 

下午两点半,控制工程师小王盯着示波器上末端实际速度曲线,眉头拧成了疙瘩。

 

"又是这样。"他对旁边的运动规划工程师小赵说,"轨迹规划给的关节速度曲线看起来很平滑,但末端实际速度在靠近工作空间边界的时候忽快忽慢,焊点质量不稳定。"

 

小赵凑过来看了一眼:"你用的末端速度是直接拿关节速度乘以臂长粗算的?"

 

"对,我给θ₂_dot设了0.8 rad/s,按小臂力臂0.4 m算出来末端线速度应该是320 mm/s——结果实际焊的时候速度飘到了工艺窗口外面。"

 

"问题就在这。0.8 rad/s的肘关节速度,在r=850 mm的位置末端速度确实大约是320 mm/s——但那是纯肘关节贡献的分量。你没算基座旋转和肘关节的耦合项。真正的总末端速度需要雅可比矩阵把四个关节的速度全部线性叠加。"

 

"雅可比?那不是课本第3章的东西吗?"

 

"第3章连杆参数和运动学求解的延伸。你现在做离线仿真验证,正好可以把雅可比算出来,看看每个工作点的速度放大倍数到底是多少,哪些位形下末端速度会突破80~120 mm/s的焊接窗口。"

 

本篇解决的问题:给定一组关节角度,计算该位形下的解析雅可比矩阵(6×4),将关节空间速度向量(θ₁dot, θ₂dot, θ₃dot, d₄dot)映射到末端笛卡尔速度(v_x, v_y, v_z, ω_x, ω_y, ω_z)。通过雅可比矩阵的条件数和各列向量模长,分析不同工作区域内末端速度的放大/缩小效应,为轨迹规划阶段的关节速度限幅提供依据。

 

⚠️ 声明:本篇全部代码为课程离线仿真,运行在普通PC上,用于雅可比矩阵计算与速度映射分析。不可直接用于真实机器人控制器、伺服驱动器或焊接产线。真实工业现场需结合实时动力学补偿、关节扭矩限幅与安全监控。

 

2. 痛点分析

 

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

第3章 连杆参数及运动学求解 → 3.5 速度雅可比 关节空间速度到末端笛卡尔速度的线性映射,需要解析雅可比矩阵

第3章 连杆参数及运动学求解 → 3.3 正运动学 雅可比矩阵各列由正运动学齐次变换矩阵推导,依赖D-H参数链

第6章 轨迹规划与控制 → 6.2 关节空间规划 轨迹规划阶段需要逆雅可比(或伪逆)将末端速度需求转换为关节速度

第2章 位姿描述与坐标变换 雅可比矩阵各列涉及旋转轴方向向量在不同坐标系间的变换

 

本篇代码解决 L3 离线运动学分析层。输入一组关节角度,输出该位形下的6×4雅可比矩阵,以及给定关节速度下的末端速度向量。代码不是设备实时控制器,不做伺服闭环,不做轨迹插补。它做的事情是:告诉运动规划工程师"在这个位形下,关节速度到末端速度的映射关系是什么,哪些方向被放大了,哪些被缩小了",并量化哪些工作区域满足80~120 mm/s的焊接速度窗口。

 

不覆盖:逆雅可比求解(伪逆/阻尼最小二乘)、动力学计算、碰撞检测、实时控制、6轴冗余机械臂、力控制。

 

3. 核心逻辑讲解

 

物理直觉

 

雅可比矩阵本质上是"速度版的传动比表"。想象一辆自行车:你蹬踏板的角速度和车轮线速度之间有一个固定的比例关系,这个比例由齿轮比和轮径决定。机械臂的雅可比矩阵就是每一对"关节运动→末端运动"之间的传动比——只不过机械臂有4个关节、末端有6个速度分量(3个线速度+3个角速度),所以传动比表是一个6行4列的矩阵。

 

每一列代表"如果只让第i个关节以单位速度运动、其他关节不动,末端会产生怎样的速度"。第1列告诉你基座旋转对末端的影响(力臂=末端到基座z轴的水平距离),第2列告诉你肘关节旋转的影响(力臂≈小臂长度),以此类推。把4个关节的速度同时乘以各自那列再加起来,就得到末端的总速度。

 

业务逻辑 → 代码映射

 

输入:一组关节角度 q = [θ₁, θ₂, θ₃, d₄]

  ↓

步骤1:正运动学计算各连杆变换矩阵 T₀¹, T₁², T₂³, T₃⁴

  ↓

步骤2:提取各连杆坐标系原点位置 p_i 和旋转轴方向 z_i

  ↓

步骤3:构建雅可比矩阵各列——

   线速度列 jv_i = z_{i-1} × (p_end - p_{i-1}) (旋转关节)

   线速度列 jv_i = z_3 (平移关节,沿第3连杆z轴)

   角速度列 jw_i = z_{i-1} (旋转关节)

   角速度列 jw_i = [0,0,0] (平移关节,不产生角速度)

  ↓

步骤4:组装 J = [jv₁ jv₂ jv₃ jv₄; jw₁ jw₂ jw₃ jw₄]

  ↓

步骤5:输入关节速度 q_dot,计算末端速度 v_end = J × q_dot

  ↓

步骤6:计算条件数 cond(J) = σ_max / σ_min 评估该位形的速度映射"健康度"

 

关键公式文字翻译

 

旋转关节线速度列:z{i-1} × (p_end - p{i-1})。z{i-1}是第i个关节的旋转轴方向向量(在基座标系中表示),p_end是末端位置,p{i-1}是第i-1个连杆坐标系原点。叉乘的物理含义:绕z轴旋转时,距离旋转轴|p_end - p_{i-1}|远的位置,线速度大小等于角速度乘以力臂长度,方向垂直于"从旋转轴到末端的连线"和"旋转轴"构成的平面。

 

旋转关节角速度列:直接就是z_{i-1}。因为绕某个轴旋转时,末端角速度就等于该关节的角速度向量(叠加原理)。

 

平移关节:线速度列等于第3连杆的z轴方向单位向量(因为第4级D-H变换中α₄=-π/2会翻转z轴,平移方向应取翻转前的第3连杆z轴),角速度列为全零(平移不产生旋转)。

 

条件数:cond(J) = σ_max / σ_min,即最大奇异值除以最小奇异值。不做奇异值过滤——当构型接近奇异时σ_min→0,cond→∞,这正是需要观测的物理趋势。过滤掉趋零奇异值会掩盖奇异,与"条件数越大越接近奇异"的核心叙事矛盾。

 

4. OOP代码实现

 

技术栈

 

严格限定:numpy, scipy, matplotlib。不引入pybullet、roboticstoolbox等任何第三方机器人库。sympy仅用于符号推导验证,本篇核心计算全部使用数值方法。

 

项目结构树

 

robot_jacobian_sim/

├── run_jacobian_sim.py

├── requirements.txt

├── jacobian/

│ ├── __init__.py

│ ├── models.py

│ ├── forward_kinematics.py

│ ├── jacobian_solver.py

│ └── visualize.py

├── tests/

│ ├── __init__.py

│ └── test_jacobian_sim.py

└── results/

 

核心源码

 

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

 

"""

4-DOF工业机械臂数据模型

连杆参数、D-H表、关节限位、速度向量结构体

"""

 

from dataclasses import dataclass, field

from typing import List, Tuple

import numpy as np

 

 

@dataclass

class JointLimit:

    """

    单关节硬件限位

    min_val: 最小允许值 (rad 或 m)

    max_val: 最大允许值 (rad 或 m)

    """

    min_val: float

    max_val: float

    joint_name: str = ""

 

    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:

        """

        判断关节值是否在限位区间内

        浮点比较使用1e-10容差,避免边界舍入误判

        """

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

 

 

@dataclass

class JointLimitSet:

    """

    完整关节限位集合(4-DOF工业机械臂)

    """

    limits: List[JointLimit]

 

    def __post_init__(self):

        if len(self.limits) != 4:

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

 

    @classmethod

    def default_4dof_welding(cls):

        """

        默认4-DOF焊接机械臂关节限位

        θ1: ±160° θ2: ±120° θ3: ±180° d4: ±150 mm

        """

        pi = 3.141592653589793

        return cls(limits=[

            JointLimit(-160 * pi / 180, 160 * pi / 180, 'joint1_base_yaw'),

            JointLimit(-120 * pi / 180, 120 * pi / 180, 'joint2_elbow_yaw'),

            JointLimit(-180 * pi / 180, 180 * pi / 180, 'joint3_wrist_roll'),

            JointLimit(-0.150, 0.150, 'joint4_z_lift'),

        ])

 

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

        """

        检查全部关节是否在限位内

        返回:(全部通过?, 超限关节索引列表)

        使用索引而非字符串匹配,避免子串匹配脆弱性

        """

        if len(joint_values) != 4:

            raise ValueError(f"需要4个关节值, 实际{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

 

 

@dataclass

class DHParam:

    """标准D-H参数"""

    theta: float

    d: float

    a: float

    alpha: float

 

    def __post_init__(self):

        for name, val in [('theta', self.theta), ('d', self.d),

                           ('a', self.a), ('alpha', self.alpha)]:

            if not np.isfinite(val):

                raise ValueError(f"DHParam.{name}必须为有限数值")

 

 

@dataclass

class SCARAParams:

    """

    4-DOF工业机械臂完整参数

    大臂500mm, 小臂400mm, d1=350mm

    """

    links: List[DHParam]

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

        'joint1_base_yaw', 'joint2_elbow_yaw',

        'joint3_wrist_roll', 'joint4_z_lift'

    ])

 

    @classmethod

    def default_welding(cls):

        pi = 3.141592653589793

        links = [

            DHParam(0.0, 0.350, 0.500, 0.0),

            DHParam(0.0, 0.0, 0.400, 0.0),

            DHParam(0.0, 0.0, 0.0, 0.0),

            DHParam(0.0, 0.0, 0.0, -pi/2),

        ]

        return cls(links=links)

 

 

@dataclass

class JointVelocity:

    """

    关节速度向量 (rad/s 或 m/s)

    """

    values: List[float]

 

    def __post_init__(self):

        if len(self.values) != 4:

            raise ValueError(f"需要4个关节速度值, 实际{len(self.values)}个")

        for i, v in enumerate(self.values):

            if not np.isfinite(v):

                raise ValueError(f"JointVelocity.values[{i}]必须为有限数值")

 

    def as_array(self) -> np.ndarray:

        return np.array(self.values, dtype=float)

 

"jacobian/forward_kinematics.py" — 正运动学求解器

 

"""

正运动学求解器:计算各连杆变换矩阵,供雅可比计算使用

"""

 

import numpy as np

from jacobian.models import SCARAParams

 

 

class ForwardKinematicsSCARA:

    """

    4-DOF SCARA正运动学求解器

    输出各连杆在基座标系中的变换矩阵列表

    """

 

    def __init__(self, params: SCARAParams):

        self.params = params

 

    def compute_all_transforms(self, joint_values):

        """

        计算从基座到各连杆的齐次变换矩阵

        返回:T_list[i] = T_0^{i+1},i=0,1,2,3

        同时返回各连杆坐标系原点在基座标系中的位置 p_list[i]

        以及各连杆z轴方向在基座标系中的单位向量 z_list[i]

 

        注意:z_list[3]是第4连杆系z轴(经α₄=-π/2翻转),

        不是关节4平移方向;平移方向用z_list[2](第3连杆z轴)

        """

        if len(joint_values) != 4:

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

 

        T_list = []

        p_list = []

        z_list = []

 

        T = np.eye(4)

        for i in range(3):

            self.params.links[i].theta = joint_values[i]

            T_i = self._dh_matrix(self.params.links[i])

            T = T @ T_i

            T_list.append(T.copy())

            p_list.append(T[0:3, 3].copy())

            z_list.append(T[0:3, 2].copy())

 

        # 第4关节:平移,α₄=-π/2

        self.params.links[3].d = joint_values[3]

        T_3 = self._dh_matrix(self.params.links[3])

        T = T @ T_3

        T_list.append(T.copy())

        p_list.append(T[0:3, 3].copy())

        # 注意:z_list[3]是第4连杆系z轴(经α₄=-π/2翻转),

        # 不是关节4平移方向;平移方向用z_list[2]

        z_list.append(T[0:3, 2].copy())

 

        return T_list, p_list, z_list

 

    def compute_end_effector_pose(self, joint_values):

        """计算末端执行器位姿"""

        T_list, p_list, z_list = self.compute_all_transforms(joint_values)

        T_end = T_list[-1]

        x, y, z = T_end[0, 3], T_end[1, 3], T_end[2, 3]

        yaw = joint_values[0] + joint_values[1] + joint_values[2]

        yaw = np.arctan2(np.sin(yaw), np.cos(yaw))

        return x, y, z, yaw

 

    @staticmethod

    def _dh_matrix(param):

        ct, st = np.cos(param.theta), np.sin(param.theta)

        ca, sa = np.cos(param.alpha), np.sin(param.alpha)

        return np.array([

            [ct, -st*ca, st*sa, param.a*ct],

            [st, ct*ca, -ct*sa, param.a*st],

            [0, sa, ca, param.d],

            [0, 0, 0, 1]

        ], dtype=float)

 

"jacobian/jacobian_solver.py" — 雅可比矩阵求解器(核心业务类)

 

"""

雅可比矩阵求解器:输入关节角度,计算解析雅可比矩阵

基于运动学递推公式:J_i = [z_{i-1} × (p_end - p_{i-1}); z_{i-1}] (旋转关节)

 

本篇计算正映射 J·q_dot → 末端速度

逆映射(伪逆/DLS)为下一步,本篇不覆盖

"""

 

import numpy as np

from jacobian.models import SCARAParams

from jacobian.forward_kinematics import ForwardKinematicsSCARA

 

 

class JacobianSolver:

    """

    4-DOF SCARA雅可比矩阵解析求解器

    输入:关节角度 q = [θ1, θ2, θ3, d4]

    输出:6×4雅可比矩阵 J

    """

 

    def __init__(self, params: SCARAParams):

        self.params = params

        self.fk = ForwardKinematicsSCARA(params)

 

    def compute_jacobian(self, joint_values) -> np.ndarray:

        """

        计算给定关节角度下的6×4雅可比矩阵

        返回:J (6×4 numpy array)

        """

        if len(joint_values) != 4:

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

 

        T_list, p_list, z_list = self.fk.compute_all_transforms(joint_values)

 

        p_end = p_list[-1]

        z_base = np.array([0.0, 0.0, 1.0])

 

        J = np.zeros((6, 4))

 

        # 第1列:joint1 (旋转关节,绕基座z轴)

        J[0:3, 0] = np.cross(z_base, p_end - np.zeros(3))

        J[3:6, 0] = z_base

 

        # 第2列:joint2 (旋转关节,绕第1连杆z轴)

        J[0:3, 1] = np.cross(z_list[0], p_end - p_list[0])

        J[3:6, 1] = z_list[0]

 

        # 第3列:joint3 (旋转关节,绕第2连杆z轴)

        J[0:3, 2] = np.cross(z_list[1], p_end - p_list[1])

        J[3:6, 2] = z_list[1]

 

        # 第4列:joint4 (平移关节,沿第3连杆z轴方向)

        # 注意:用z_list[2](第3连杆z轴),不用z_list[3](经α₄翻转)

        J[0:3, 3] = z_list[2]

        J[3:6, 3] = np.array([0.0, 0.0, 0.0])

 

        return J

 

    def compute_end_velocity(self, joint_values, joint_velocities):

        """

        给定关节角度和关节速度,计算末端笛卡尔速度

        v_end = J(q) × q_dot

        返回:(v_end (6×1), J (6×4))

        """

        J = self.compute_jacobian(joint_values)

        q_dot = np.array(joint_velocities, dtype=float)

        if q_dot.shape[0] != 4:

            raise ValueError(f"需要4个关节速度值, 实际{q_dot.shape[0]}个")

 

        v_end = J @ q_dot

        return v_end, J

 

    def compute_condition_number(self, joint_values) -> float:

        """

        计算雅可比矩阵的条件数(使用2-范数奇异值)

        cond(J) = σ_max / σ_min

        不做奇异值过滤——σ_min→0时cond→∞正是接近奇异的物理趋势

        过滤趋零奇异值会掩盖奇异,与核心叙事矛盾

        """

        J = self.compute_jacobian(joint_values)

        singular_values = np.linalg.svd(J, compute_uv=False)

        # 6×4矩阵最多4个非零奇异值,直接取max/min

        if singular_values[-1] < 1e-15:

            return float('inf')

        return float(singular_values[0] / singular_values[-1])

 

    def get_min_singular_value(self, joint_values) -> float:

        """

        返回最小奇异值(辅助指标,独立输出)

        """

        J = self.compute_jacobian(joint_values)

        singular_values = np.linalg.svd(J, compute_uv=False)

        return float(singular_values[-1])

 

    def analyze_jacobian_columns(self, joint_values) -> dict:

        """

        分析雅可比各列向量的模长

        反映每个关节对末端各方向速度的贡献大小

        """

        J = self.compute_jacobian(joint_values)

        analysis = {

            'column_norms': [],

            'linear_velocity_contributions': [],

            'angular_velocity_contributions': []

        }

 

        for i in range(4):

            col = J[:, i]

            analysis['column_norms'].append(float(np.linalg.norm(col)))

            analysis['linear_velocity_contributions'].append(

                float(np.linalg.norm(col[0:3])))

            analysis['angular_velocity_contributions'].append(

                float(np.linalg.norm(col[3:6])))

 

        return analysis

 

"jacobian/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_condition_number_vs_reach(self, reach_distances, condition_numbers,

                                        min_singular_values=None):

        """

        ① 条件数随水平距离变化曲线

        工程结论:距离工作空间边界越近,条件数越大,速度映射越不均匀

        """

        fig, ax1 = plt.subplots(figsize=(10, 5))

 

        color1 = 'tab:blue'

        ax1.plot(np.array(reach_distances) * 1000, condition_numbers,

                  color=color1, linewidth=1.5, marker='o', markersize=4)

        ax1.set_xlabel('Horizontal Reach Distance r (mm)')

        ax1.set_ylabel('Jacobian Condition Number', color=color1)

        ax1.tick_params(axis='y', labelcolor=color1)

        ax1.grid(True, alpha=0.3)

 

        # 标注工作空间边界

        max_reach = max(reach_distances) * 1000

        min_reach = min(reach_distances) * 1000

        ax1.axvline(x=max_reach, color='red', linestyle='--',

                     linewidth=1, alpha=0.7, label=f'Max Reach {max_reach:.0f}mm')

        ax1.axvline(x=min_reach, color='red', linestyle='--',

                     linewidth=1, alpha=0.7, label=f'Min Reach {min_reach:.0f}mm')

 

        # 双y轴:最小奇异值

        if min_singular_values is not None:

            ax2 = ax1.twinx()

            color2 = 'tab:orange'

            ax2.plot(np.array(reach_distances) * 1000, min_singular_values,

                      color=color2, linewidth=1, marker='s', markersize=3,

                      alpha=0.7)

            ax2.set_ylabel('Min Singular Value', color=color2)

            ax2.tick_params(axis='y', labelcolor=color2)

 

        plt.title('Jacobian Condition Number vs. Horizontal Reach Distance')

        fig.tight_layout()

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

        plt.savefig(path, dpi=150)

        plt.close()

        return path

 

    def plot_jacobian_column_norms(self, test_configs, column_data):

        """

        ② 雅可比各列模长对比柱状图

        工程结论:展示各关节在不同位形下对末端速度的贡献大小

        """

        fig, axes = plt.subplots(2, 2, figsize=(12, 9))

        axes = axes.flatten()

 

        joint_names = ['Joint1 (Base Yaw)', 'Joint2 (Elbow)',

                       'Joint3 (Wrist)', 'Joint4 (Z Lift)']

 

        for j in range(4):

            ax = axes[j]

            norms = [col_data[i][j] for i in range(len(col_data))]

            labels = [f'C{cfg["label"]}' for cfg in test_configs]

 

            bars = ax.bar(range(len(norms)), norms, alpha=0.8,

                           edgecolor='black')

            for bar, norm in zip(bars, norms):

                if norm > 1.5:

                    bar.set_color('red')

                elif norm > 0.8:

                    bar.set_color('orange')

                else:

                    bar.set_color('steelblue')

 

            ax.set_xticks(range(len(labels)))

            ax.set_xticklabels(labels, rotation=45, ha='right', fontsize=7)

            ax.set_ylabel('Column Norm ||J_i||')

            ax.set_title(f'{joint_names[j]}\nVelocity Contribution Magnitude')

            ax.grid(True, alpha=0.3, axis='y')

 

        plt.tight_layout()

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

        plt.savefig(path, dpi=150)

        plt.close()

        return path

 

    def plot_end_velocity_vectors(self, test_configs, velocities,

                                   velocity_magnitudes):

        """

        ③ 末端速度向量分量对比 + 速度窗口分析

        工程结论:给定相同关节速度,不同位形下末端速度差异巨大;

        标注80~120 mm/s焊接速度窗口

        """

        fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(11, 10))

 

        # 上图:速度分量

        n = len(test_configs)

        x = np.arange(n)

        width = 0.12

 

        vx = [vel[0] for vel in velocities]

        vy = [vel[1] for vel in velocities]

        vz = [vel[2] for vel in velocities]

        wx = [vel[3] for vel in velocities]

        wy = [vel[4] for vel in velocities]

        wz = [vel[5] for vel in velocities]

 

        ax1.bar(x - 2.5*width, vx, width, label='v_x (m/s)', alpha=0.8)

        ax1.bar(x - 1.5*width, vy, width, label='v_y (m/s)', alpha=0.8)

        ax1.bar(x - 0.5*width, vz, width, label='v_z (m/s)', alpha=0.8)

        ax1.bar(x + 0.5*width, wx, width, label='ω_x (rad/s)', alpha=0.8)

        ax1.bar(x + 1.5*width, wy, width, label='ω_y (rad/s)', alpha=0.8)

        ax1.bar(x + 2.5*width, wz, width, label='ω_z (rad/s)', alpha=0.8)

 

        ax1.set_xticks(x)

        ax1.set_xticklabels([cfg['label'] for cfg in test_configs],

                            rotation=45, ha='right')

        ax1.set_ylabel('Velocity Component Magnitude')

        ax1.set_title('End-Effector Velocity Components under Identical Joint Speeds'

                       '\n([θ1_dot=0.5, θ2_dot=0.3, θ3_dot=0.2, d4_dot=0.1])')

        ax1.legend(fontsize=7)

        ax1.grid(True, alpha=0.3, axis='y')

 

        # 下图:末端线速度模长 + 焊接窗口

        speed_mm_s = [mag * 1000 for mag in velocity_magnitudes]

        colors = ['green' if 80 <= s <= 120 else 'red' for s in speed_mm_s]

        ax2.bar(x, speed_mm_s, color=colors, alpha=0.8, edgecolor='black')

        ax2.axhline(y=80, color='green', linestyle='--', linewidth=1.5,

                     label='Welding Window Min (80 mm/s)')

        ax2.axhline(y=120, color='green', linestyle='--', linewidth=1.5,

                     label='Welding Window Max (120 mm/s)')

        ax2.set_xticks(x)

        ax2.set_xticklabels([cfg['label'] for cfg in test_configs],

                            rotation=45, ha='right')

        ax2.set_ylabel('End-Effector Linear Speed (mm/s)')

        ax2.set_title('End-Effector Speed vs. Welding Window (80~120 mm/s)')

        ax2.legend(fontsize=8)

        ax2.grid(True, alpha=0.3, axis='y')

 

        plt.tight_layout()

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

        plt.savefig(path, dpi=150)

        plt.close()

        return path

 

    def plot_singular_value_spectrum(self, test_configs, svd_data):

        """

        ④ 雅可比矩阵奇异值谱

        工程结论:奇异值分布越不均匀(最小奇异值趋近0),构型越接近奇异

        """

        fig, ax = plt.subplots(figsize=(10, 6))

 

        n = len(test_configs)

        x = np.arange(n)

        width = 0.2

 

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

        labels = ['σ₁ (max)', 'σ₂', 'σ₃', 'σ₄ (min)']

 

        for i in range(4):

            sv = [svd_data[j][i] if i < len(svd_data[j]) else 0

                   for j in range(n)]

            ax.bar(x + i*width, sv, width,

                    label=labels[i], color=colors[i], alpha=0.8,

                    edgecolor='black')

 

        ax.set_xlabel('Test Configuration')

        ax.set_ylabel('Singular Value Magnitude')

        ax.set_title('Jacobian Singular Value Spectrum per Configuration')

        ax.set_xticks(x + 1.5*width)

        ax.set_xticklabels([cfg['label'] for cfg in test_configs],

                           rotation=45, ha='right')

        ax.legend(fontsize=8)

        ax.grid(True, alpha=0.3, axis='y')

 

        plt.tight_layout()

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

        plt.savefig(path, dpi=150)

        plt.close()

        return path

 

5. 可视化要求

 

序号 图表文件 服务结论

① 

"01_condition_number_vs_reach.png" 条件数+最小奇异值随水平距离变化,展示工作空间边界附近的奇异趋势

② 

"02_jacobian_column_norms.png" 各关节在不同位形下对末端速度的贡献大小对比

③ 

"03_end_ve

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

Logo

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

更多推荐