《Python机器人基础原理》第十三篇:焊接产线仿真——计算雅可比矩阵行列式,依据数值判断当前构型是否靠近奇异位形

 

1. 实际工业应用场景

 

周五上午,某新能源汽车电池托盘焊接产线。一台4-DOF关节型工业机械臂搭载MIG焊枪,对托盘铝合金边框进行连续角焊缝作业。机器人构型:3旋转+1垂直平移(SCARA变体),大臂长500 mm,小臂长400 mm,基座安装高度d₁=350 mm,z轴行程±150 mm。关节硬件限位:θ₁ ±160°、θ₂ ±120°、θ₃ ±180°、d₄ ±150 mm。额定负载8 kg,焊枪及线缆总质量2.3 kg。焊接工艺要求末端线速度恒定在60~80 mm/s,电弧跟踪精度±0.3 mm。

 

上午十点一刻,焊接质量巡检员小刘拿着抽检报告找到控制工程师小王。

 

"第47号到第52号焊缝,边缘有咬边和焊瘤。"小刘指着X光片说,"全都在托盘拐角处,也就是机械臂手臂几乎伸直的位置。"

 

小王调出该工位的轨迹日志,皱眉看了半天:"轨迹规划给的末端速度曲线是恒定的,60 mm/s,没问题啊。"

 

小赵从后面走过来,看了眼屏幕:"你用的什么方法保证速度恒定?"

 

"逆雅可比算关节速度,然后直接下发给伺服。"

 

"奇异位形附近呢?"

 

小王愣了一下。

 

"你的逆雅可比在接近奇异的时候,关节速度会趋向无穷大。"小赵在纸上画了个构型,"大臂小臂几乎伸直,这时候雅可比矩阵的行列式趋近于零——你有没有做奇异检测?"

 

"没有。我以为轨迹都在工作空间里面就行。"

 

"工作空间内部也有奇异线。你需要实时算雅可比行列式,当行列式绝对值掉到某个阈值以下,就判定当前构型靠近奇异,触发降速或者切换冗余自由度。"

 

本篇解决的问题:给定一组关节角度,计算该位形下雅可比矩阵的行列式值,依据行列式数值是否低于预设阈值,判断当前构型是否靠近奇异位形。对于SCARA构型,末端位置只由θ₁和θ₂两个关节决定(腕部roll关节θ₃只改变姿态、第4关节只改变高度),因此位置奇异的本质是平面2连杆的奇异,用2×2子块行列式 |det| = L₁·L₂·|sin(θ₂)| 判定。通过在工作空间网格上扫描该指标,生成奇异区域热力图,为轨迹规划阶段的奇异规避提供离线校核依据。

 

⚠️ 声明:本篇全部代码为课程离线仿真,运行在普通PC上,用于雅可比行列式计算与奇异位形检测分析。不可直接用于真实机器人控制器、伺服驱动器或焊接产线。真实工业现场需结合实时动力学补偿、关节扭矩限幅与安全监控。代码作者不对直接使用本代码于真实设备造成的任何后果负责。

 

2. 痛点分析

 

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

第3章 连杆参数及运动学求解 → 3.5 速度雅可比 雅可比矩阵行列式趋零是逆运动学数值不稳定的根源

第3章 → 3.6 奇异位形 工作空间内部存在构型奇异线,需要行列式阈值检测

第6章 轨迹规划与控制 → 6.3 笛卡尔空间规划 逆雅可比求关节速度时,奇异附近关节速度发散导致末端失控

第2章 位姿描述与坐标变换 行列式计算依赖雅可比子块列向量的线性独立性

 

本篇代码解决 L3 离线运动学分析层。输入一组关节角度,计算雅可比2×2子块行列式值,与预设阈值比较,输出奇异判定结果。代码不是设备实时控制器,不做伺服闭环,不做在线奇异规避。它做的事情是:在离线环境中扫描工作空间各区域的行列式分布,告诉轨迹规划工程师"哪些区域是危险的奇异区,规划路径要避开"。

 

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

 

3. 核心逻辑讲解

 

物理直觉

 

雅可比矩阵的行列式衡量的是"关节速度空间到末端速度空间的映射体积缩放比"。想象一个三维的小立方体,每个顶点代表一组略微不同的关节速度组合。经过雅可比矩阵这个"变换透镜"之后,这个立方体在末端速度空间里变成了一个平行六面体。行列式就是这个平行六面体的体积。

 

当机械臂处于普通构型时,各关节对末端速度的贡献方向互不重叠,行列式值较大,映射是"健康的"。当机械臂接近奇异构型(比如大臂小臂完全伸直),两个关节对末端速度的贡献方向几乎平行——平行六面体被压扁了,体积趋近于零,行列式趋近于零。这时候逆映射(从末端速度反推关节速度)就变得极度敏感:末端速度稍有波动,算出来的关节速度就会爆掉。

 

SCARA构型的特殊性:为什么用2×2子块

 

SCARA构型的前3个旋转关节轴线全部竖直(z轴方向[0,0,1]),关节3是腕部roll回转,a₃=0(不产生位移),第4关节只沿z轴平移。这意味着:

 

- 末端位置只由θ₁和θ₂决定(平面2连杆问题)

- θ₃只改变末端姿态角,不产生线速度

- d₄只改变末端高度z,不影响xy平面位置

 

因此,SCARA的位置奇异只由θ₁和θ₂决定,与θ₃和d₄无关。雅可比线速度子块的前3列中,第3列恒为零向量(因为z₂×竖直向量=0),3×3行列式恒等于零——用3×3子块对本构型毫无意义,全部构型都会被误判为奇异。

 

正确的做法:取J_v的前2行(xy平面线速度)和前2列(θ₁、θ₂),构成2×2子块:

 

|det(J₂×₂)| = L₁·L₂·|sin(θ₂)|

 

θ₂→0°(手臂完全伸直)或θ₂→±180°(手臂完全折叠)时行列式趋零,对应两种经典奇异位形。θ₂=90°时行列式最大,构型最"健康"。

 

业务逻辑 → 代码映射

 

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

  ↓

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

  ↓

步骤2:提取大臂末端位置 p₁ 和末端位置 p_end

  ↓

步骤3:构建雅可比2×2线速度子块——

   J₂×₂ = [[∂x/∂θ₁, ∂x/∂θ₂],

            [∂y/∂θ₁, ∂y/∂θ₂]]

         = [[-L₁sinθ₁-L₂sin(θ₁+θ₂), -L₂sin(θ₁+θ₂)],

            [ L₁cosθ₁+L₂cos(θ₁+θ₂), L₂cos(θ₁+θ₂)]]

  ↓

步骤4:计算 det(J₂×₂) = L₁·L₂·sin(θ₂)

  ↓

步骤5:与阈值比较 |det| < threshold → 标记奇异

  ↓

步骤6:在工作空间(r, θ₂)网格上解析生成行列式分布热力图

 

关键公式文字翻译

 

2×2子块行列式:|det| = L₁·L₂·|sin(θ₂)|。L₁和L₂是两个臂长(m),sin(θ₂)是肘角正弦。物理含义:θ₂越接近0°或±180°,两个臂的运动方向越趋于平行,末端位置对关节角的敏感度越高,行列式越小。单位是m²(两个长度量纲相乘),不是m³/s³。

 

阈值标定:对L₁=500 mm、L₂=400 mm的构型,行列式量级在0~0.2 m²之间。取阈值1e-4 m²,对应|sin(θ₂)|<5e-4,即θ₂距奇异位置约0.03°以内才触发——这个阈值偏紧,适合高精度的焊接场景。若想更早预警,可放宽到1e-3 m²(对应θ₂距奇异约0.14°)。

 

4. OOP代码实现

 

技术栈

 

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

 

项目结构树

 

robot_singularity_sim/

├── run_singularity_sim.py

├── requirements.txt

├── singularity/

│ ├── __init__.py

│ ├── models.py

│ ├── forward_kinematics.py

│ ├── jacobian_solver.py

│ ├── singularity_detector.py

│ └── visualize.py

├── tests/

│ ├── __init__.py

│ └── test_singularity_sim.py

└── results/

 

核心源码

 

"singularity/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 SingularityThreshold:

    """

    奇异检测阈值配置

    det_threshold: 2×2子块行列式绝对值低于此值判定为奇异

    单位:m²(两个长度量纲相乘)

    默认1e-4 m²对应|sin(θ₂)|<5e-4,即θ₂距奇异约0.03°

    """

    det_threshold: float = 1e-4

 

    def __post_init__(self):

        if self.det_threshold <= 0:

            raise ValueError("det_threshold必须为正数")

        if not np.isfinite(self.det_threshold):

            raise ValueError("det_threshold必须为有限数值")

 

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

 

"""

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

"""

 

import numpy as np

from singularity.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.append(T[0:3, 2].copy())

 

        return T_list, p_list, z_list

 

    @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)

 

"singularity/jacobian_solver.py" — 雅可比矩阵求解器

 

"""

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

供奇异检测器调用

"""

 

import numpy as np

from singularity.models import SCARAParams

from singularity.forward_kinematics import ForwardKinematicsSCARA

 

 

class JacobianSolver:

    """

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

    """

 

    def __init__(self, params: SCARAParams):

        self.params = params

        self.fk = ForwardKinematicsSCARA(params)

 

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

        """

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

        """

        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_jacobian_2x2(self, joint_values) -> np.ndarray:

        """

        计算末端位置对θ₁、θ₂的2×2雅可比子块(SCARA位置奇异核心)

        x = L1*cos(θ1) + L2*cos(θ1+θ2)

        y = L1*sin(θ1) + L2*sin(θ1+θ2)

        J₂×₂ = [[-L1*sin(θ1)-L2*sin(θ1+θ2), -L2*sin(θ1+θ2)],

                [ L1*cos(θ1)+L2*cos(θ1+θ2), L2*cos(θ1+θ2)]]

        """

        if len(joint_values) != 4:

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

 

        theta1 = joint_values[0]

        theta2 = joint_values[1]

        L1 = self.params.links[0].a

        L2 = self.params.links[1].a

 

        J22 = np.array([

            [-L1*np.sin(theta1) - L2*np.sin(theta1 + theta2),

             -L2*np.sin(theta1 + theta2)],

            [L1*np.cos(theta1) + L2*np.cos(theta1 + theta2),

             L2*np.cos(theta1 + theta2)]

        ], dtype=float)

 

        return J22

 

"singularity/singularity_detector.py" — 奇异检测器(核心业务类)

 

"""

奇异检测器:计算雅可比2×2子块行列式,依据阈值判定奇异

核心业务:|det(J₂×₂)| = L₁·L₂·|sin(θ₂)| < threshold → 奇异

 

为什么用2×2而不是3×3:

SCARA前3轴全竖直,θ₃是腕部roll(a₃=0),第4关节只沿z平移

→ p_end - p₂ 是纯竖直向量

→ J_v第3列 = z₂ × (p_end - p₂) = [0,0,1] × [0,0,d₄] = [0,0,0] 恒为零

→ 3×3子块行列式恒等于零,与θ₁θ₂θ₃取值无关

→ 全部构型被误判为奇异,指标失去意义

"""

 

import numpy as np

from singularity.models import SCARAParams, SingularityThreshold

from singularity.jacobian_solver import JacobianSolver

 

 

class SingularityDetector:

    """

    基于雅可比2×2子块行列式绝对值的奇异位形检测器

    |det(J₂×₂)| = L₁·L₂·|sin(θ₂)|

    单位:m²

    """

 

    def __init__(self, params: SCARAParams,

                 threshold: SingularityThreshold = None):

        self.params = params

        self.L1 = params.links[0].a

        self.L2 = params.links[1].a

        self.threshold = threshold or SingularityThreshold()

        self.jacobian_solver = JacobianSolver(params)

 

    def compute_analytical_determinant(self, joint_values) -> float:

        """

        解析计算2×2子块行列式

        det = L₁·L₂·sin(θ₂)

        这是SCARA位置奇异的精确指标,与θ₁无关

        """

        theta2 = joint_values[1]

        return float(self.L1 * self.L2 * np.sin(theta2))

 

    def compute_jacobian_determinant(self, joint_values) -> float:

        """

        数值计算2×2子块行列式(验证用)

        取J_v的前2行(xy平面)和前2列(θ₁、θ₂)

        """

        J22 = self.jacobian_solver.compute_jacobian_2x2(joint_values)

        return float(np.linalg.det(J22))

 

    def is_singular(self, joint_values, threshold_override=None) -> tuple:

        """

        判断当前构型是否靠近奇异

        返回:(is_singular: bool, det_value: float, threshold_used: float)

        """

        thresh = threshold_override if threshold_override is not None \

                 else self.threshold.det_threshold

        # 使用解析公式(更精确,无数值误差)

        det_val = self.compute_analytical_determinant(joint_values)

        is_sing = abs(det_val) < thresh

        return is_sing, det_val, thresh

 

    def get_distance_to_singularity(self, joint_values) -> float:

        """

        计算当前构型到奇异边界的角距离(度)

        用于量化"还有多远到奇异"

        """

        theta2 = joint_values[1]

        # 奇异发生在θ₂=0或θ₂=±180°

        dist_to_zero = abs(theta2)

        dist_to_180 = abs(abs(theta2) - np.pi)

        return float(min(dist_to_zero, dist_to_180) * 180 / np.pi)

 

    def scan_workspace_grid(self, r_range, theta2_range):

        """

        在工作空间网格上解析生成行列式分布

        由于det = L₁·L₂·sin(θ₂)与θ₁无关,也近似与r无关

        (给定θ₂后r由θ₁决定,但det只依赖θ₂)

        返回:grid_data[i][j] = (r, theta2, det_val, is_singular)

        """

        grid_data = []

 

        for r in r_range:

            row = []

            for theta2 in theta2_range:

                # 解析计算行列式(与θ₁无关)

                det_val = self.L1 * self.L2 * np.sin(theta2)

                is_sing = abs(det_val) < self.threshold.det_threshold

                row.append((r, theta2, det_val, is_sing))

            grid_data.append(row)

 

        return grid_data

 

"singularity/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_determinant_vs_theta2(self, theta2_values, det_values,

                                    threshold, is_singular_list):

        """

        ① 行列式绝对值随θ₂角度变化曲线

        工程结论:θ₂→0°或±180°时行列式趋零(肘部伸直/折叠奇异)

        """

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

 

        ax.plot(np.array(theta2_values) * 180 / np.pi,

                 np.abs(det_values), 'o-', linewidth=1.5,

                 markersize=5, color='steelblue')

 

        # 奇异阈值线

        ax.axhline(y=threshold, color='red', linestyle='--',

                    linewidth=1.5, label=f'Singularity Threshold '

                    f'({threshold} m²)')

 

        # 标注奇异区域

        for i, sing in enumerate(is_singular_list):

            if sing:

                ax.axvline(x=np.array(theta2_values[i]) * 180 / np.pi,

                            color='red', linestyle=':', alpha=0.5)

 

        ax.set_xlabel('Joint2 Angle θ₂ (deg)')

        ax.set_ylabel('|det(J₂×₂)| (m²)')

        ax.set_title('Jacobian 2×2 Sub-block Determinant vs. Elbow Angle θ₂\n'

                      '(|det| = L₁·L₂·|sin(θ₂)|)')

        ax.set_yscale('log')

        ax.grid(True, alpha=0.3)

        ax.legend(fontsize=8)

 

        plt.tight_layout()

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

        plt.savefig(path, dpi=150)

        plt.close()

        return path

 

    def plot_singularity_heatmap(self, grid_data, r_range, theta2_range,

                                  weld_positions=None):

        """

        ② 工作空间奇异检测热力图

        工程结论:红色区域为奇异区,轨迹规划需避开

        标注叙事中的焊缝位置(第47~52号焊缝在拐角处)

        """

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

 

        # 构建行列式绝对值矩阵

        det_matrix = np.zeros((len(r_range), len(theta2_range)))

        sing_matrix = np.zeros((len(r_range), len(theta2_range)))

 

        for i, row in enumerate(grid_data):

            for j, (r, theta2, det_val, is_sing) in enumerate(row):

                det_matrix[i, j] = abs(det_val)

                sing_matrix[i, j] = 1 if is_sing else 0

 

        # 热力图

        R_mesh, T2_mesh = np.meshgrid(

            np.array(r_range) * 1000,

            np.array(theta2_range) * 180 / np.pi)

        c = ax.contourf(R_mesh, T2_mesh, det_matrix.T, levels=20,

                         cmap='viridis')

        # 奇异边界等高线

        ax.contour(R_mesh, T2_mesh, sing_matrix.T, levels=[0.5],

                    colors='red', linewidths=2, alpha=0.8,

                    label='Singularity Boundary')

 

        plt.colorbar(c, ax=ax, label='|det(J₂×₂)| (m²)')

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

        ax.set_ylabel('Joint2 Angle θ₂ (deg)')

        ax.set_title('Singularity Map: |det(J₂×₂)| = L₁·L₂·|sin(θ₂)|\n'

                      '(Red contour: singularity boundary)')

 

        # 标注叙事中的焊缝位置(拐角处,θ₂接近0°)

        if weld_positions:

            for wp in weld_positions:

                ax.plot(wp[0] * 1000, wp[1] * 180 / np.pi, 'r*',

                         markersize=15, label='Weld Seam 47-52' if wp == weld_positions[0] else '')

 

        ax.legend(fontsize=8)

 

        plt.tight_layout()

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

        plt.savefig(path, dpi=150)

        plt.close()

        return path

 

    def plot_det_vs_reach(self, reach_distances, det_values,

                           threshold, is_singular_list):

        """

        ③ 行列式绝对值随水平距离变化曲线

        工程结论:由于det与r无关(只依赖θ₂),曲线应为水平线

        若不同r对应的θ₂不同,则展示θ₂变化导致的行列式变化

        """

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

 

        ax.plot(np.array(reach_distances) * 1000,

                 np.abs(det_values), 'o-', linewidth=1.5,

                 markersize=5, color='steelblue')

 

        ax.axhline(y=threshold, color='red', linestyle='--',

                    linewidth=1.5, label=f'Threshold ({threshold} m²)')

 

        for i, sing in enumerate(is_singular_list):

            if sing:

                ax.plot(np.array(reach_distances[i]) * 1000,

                         abs(det_values[i]), 'ro', markersize=8,

                         fillstyle='none')

 

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

        ax.set_ylabel('|det(J₂×₂)| (m²)')

        ax.set_title('Jacobian Determinant vs. Horizontal Reach Distance\n'

                      '(det depends only on θ₂, independent of r)')

        ax.set_yscale('log')

        ax.grid(True, alpha=0.3)

        ax.legend(fontsize=8)

 

        plt.tight_layout()

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

        plt.savefig(path, dpi=150)

        plt.close()

        return path

 

    def plot_analytical_vs_numerical(self, test_configs, analytical_dets,

                                      numerical_dets):

        """

        ④ 解析公式 vs 数值计算对比

        工程结论:两者完全一致,验证解析公式正确性

        """

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

 

        n = len(test_configs)

        x = np.arange(n)

        width = 0.35

 

        ax.bar(x - width/2, np.abs(analytical_dets), width,

                label='Analytical: L₁·L₂·sin(θ₂)', alpha=0.8,

                color='steelblue', edgecolor='black')

        ax.bar(x + width/2, np.abs(numerical_dets), width,

                label='Numerical: det(J₂×₂)', alpha=0.8,

                color='orange', edgecolor='black')

 

        ax.set_xlabel('Test Configuration')

        ax.set_ylabel('|det| (m²)')

        ax.set_title('Analytical vs Numerical Jacobian Determinant')

        ax.set_xticks(x)

        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_analytical_vs_numerical.png"

        plt.savefig(path, dpi=150)

        plt.close()

        return path

 

5.

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

Logo

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

更多推荐