《Python机器人基础原理》第五十五篇:奇异区轨迹测试——在靠近奇异位形处生成笛卡尔轨迹,观察逆运动学求解波动

 

1. 实际工业应用场景

 

周三下午,某3C电子元件精密装配产线。一台4-DOF SCARA机器人(大臂长0.40 m,小臂长0.30 m,额定负载3 kg,重复定位精度±0.01 mm)正在执行PCB板上微型连接器插装任务。工艺要求末端执行器沿一条近似直线的下插轨迹运动,轨迹末端恰好经过工作空间的内圈区域——即大臂和小臂几乎完全折叠的位置。

 

调试工程师小李盯着上位机监控画面,眉头紧锁:"师傅,你看这个。轨迹规划没问题,笛卡尔空间路径是直线,速度也匀速。但一到靠近内圈这个区域,关节2的角速度曲线就出现剧烈尖峰,从0.3 rad/s瞬间飙到3.7 rad/s,然后又掉回来。示教器上关节扭矩也跟着飙升。"

 

旁边的主任工程师老刘拉过椅子坐下:"你把轨迹末端的笛卡尔坐标报一下。"

 

"X=0.108 m,Y=0.025 m,Z=0.080 m。差不多就在基座中心附近偏一点的位置。"

 

老刘点了点头:"0.108和0.025,到基座中心的距离大约0.111 m。你的大臂0.40 m,小臂0.30 m,两者之差是0.10 m。末端跑到离基座中心刚过0.10 m的地方——这就是肘部奇异区。两臂几乎折叠,雅可比矩阵在这个位置接近奇异,条件数飙升。逆运动学求解时,末端微小的笛卡尔位置误差会被放大成巨大的关节角度变化。你看到的速度尖峰就是IK求解器对末端直线插补微小量化误差的放大效应。"

 

"那怎么测?"

 

"离线仿真里,你在肘部奇异区附近生成一条笛卡尔直线轨迹,逐点做逆运动学,记录每个点的关节角、关节角速度,再算条件数。你会看到条件数从正常区域的个位数飙升到几百的时候,关节角速度就开始出现尖锐峰值。这就是奇异位形对轨迹规划的影响——不是你的轨迹有问题,是工作空间选错了。"

 

"那产线上怎么办?"

 

"两个思路。一是轨迹规划阶段就避开奇异区,让末端路径离内圈保持最小安全距离。二是用阻尼最小二乘法做逆运动学,给雅可比伪逆加阻尼项,牺牲一点末端精度换取关节运动的平滑性。今天你先在仿真里把现象复现出来,下周我们再看DLS的实现。"

 

构型约定说明:本篇采用4-DOF SCARA简化模型(2个水平旋转关节+1个垂直移动关节+1个末端旋转关节),用于奇异区逆运动学波动的算法验证。连杆参数:大臂长0.40 m、质量1.5 kg、质心位于0.20 m处、绕Z轴惯量0.032 kg·m²;小臂长0.30 m、质量1.0 kg、质心位于0.15 m处、绕Z轴惯量0.015 kg·m²;升降关节行程0.25 m。全部参数内置,自包含可复现。通用DH正运动学复用公共模块(

"robot/common/kinematics.py")。

⚠️ 声明:本篇全部代码为课程离线仿真,运行在普通PC上,用于奇异位形附近逆运动学求解波动的算法验证。不可直接用于真实机器人控制器或产线轨迹规划。真实工业机器人的逆运动学求解在控制器中以固件方式实时执行,本仿真未考虑关节限位保护、碰撞检测等安全机制。代码作者不对直接使用本代码于真实设备造成的任何后果负责。

 

2. 痛点分析

 

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

第3章 连杆参数及运动学求解 → 3.3 逆运动学 解析逆运动学在肘部奇异区求解不稳定,微小位置误差放大为关节速度尖峰

第3章 连杆参数及运动学求解 → 3.2 正运动学 雅可比矩阵条件数计算、奇异判定

第6章 轨迹规划与控制 → 6.2 笛卡尔空间轨迹规划 直线插补轨迹经过内圈折叠区导致关节速度发散

第2章 位姿描述与坐标变换 笛卡尔空间位置误差→关节空间角度放大的几何解释

 

本篇代码解决 L2 离线奇异区探测层。输入:笛卡尔空间直线轨迹(起点→终点,N个插值点)。处理:逐点做解析逆运动学(固定elbow-up单分支)→计算雅可比条件数→数值差分求关节速度→统计波动幅度。输出:奇异区判定报告+可视化曲线。

 

不覆盖:阻尼最小二乘法(DLS)实时求解、elbow-up/down分支切换导致的跳变、冗余自由度自运动优化、动力学约束下的奇异规避。

 

关于"振荡"与"尖峰"的区分:本篇使用固定分支解析IK,复现的是奇异区附近的关节速度尖锐峰值(确定性函数对末端量化误差的放大),而非控制器在分支间跳变或数值迭代不收敛导致的振荡。振荡现象需要分支切换或数值IK才能复现,留待下一篇(DLS+分支切换)正面处理。

 

3. 核心逻辑讲解

 

物理直觉

 

奇异位形是机器人工作空间中一类特殊位形,在这个位置上机器人的某些自由度"丢失"了——末端在某个方向上无法产生速度,即使关节在快速转动。SCARA机器人有两类典型奇异:

 

- 肘部奇异(内圈折叠):大臂和小臂几乎完全折叠,r≈|a1-a2|,此时θ2≈π,雅可比接近降秩。末端在内圈附近沿径向的微小移动需要关节2产生巨大角速度。

- 伸直奇异(外圈伸直):大臂和小臂几乎完全伸直,r≈a1+a2,此时θ2≈0,同样导致雅可比接近降秩。

 

数学上,末端速度v和关节速度θ̇之间的关系是 v = J·θ̇,其中J是雅可比矩阵。正常位形下J满秩,可以从θ̇ = J⁻¹·v稳定求解。但在奇异区,J的条件数(最大奇异值/最小奇异值)飙升到几百甚至上千。此时J⁻¹的微小数值误差会被放大成巨大的θ̇——末端移动1 mm,关节可能要转几度。这就是"速度尖峰"的本质。

 

业务逻辑 → 代码映射

 

输入:笛卡尔直线轨迹(起点X0,Y0,Z0 → 终点X1,Y1,Z1,N个插值点)

      机器人DH参数(大臂长a1=0.40 m, 小臂长a2=0.30 m)

  ↓

【步骤1:逐点生成笛卡尔位姿】

  直线插值:P_i = P_start + i/(N-1) * (P_end - P_start)

  ↓

【步骤2:对每个点做解析逆运动学】

  余弦定理求θ2:cos(θ2) = (r²-a1²-a2²)/(2·a1·a2)

  θ1 = atan2(y,x) - atan2(a2·sinθ2, a1+a2·cosθ2)

  固定elbow-up分支(θ2>0)

  记录θ1_i, θ2_i;不可达点标记

  ↓

【步骤3:计算雅可比条件数】

  J = ∂(x,y)/∂(θ1,θ2) 的2×2雅可比

  cond(J) = σ_max / σ_min(SVD分解)

  标记 cond > 阈值(如100)的点为"接近奇异"

  不可达点单独统计,不与奇异混淆

  ↓

【步骤4:分段数值差分求关节速度】

  对可达点区间做中心差分:ω_i = (θ_{i+1} - θ_{i-1}) / (2·Δt)

  不可达区间断开,不填0

  ↓

【步骤5:统计波动】

  分别计算关节1/关节2的速度标准差和峰值

  条件数 vs 关节速度散点图

  ↓

输出:奇异区报告 + 可视化

 

关键公式文字翻译

 

2R平面逆运动学(解析解):已知末端位置(x,y),求θ1和θ2。先算末端到基座的距离r=√(x²+y²)。用余弦定理求θ2:cos(θ2) = (r²-a1²-a2²)/(2·a1·a2)。当r接近|a1-a2|时(肘部奇异),cos(θ2)接近-1,θ2接近π,分母接近零,θ1对(x,y)极度敏感。当r接近a1+a2时(伸直奇异),cos(θ2)接近1,θ2接近0。θ1 = atan2(y,x) - atan2(a2·sinθ2, a1+a2·cosθ2)。

 

雅可比矩阵(2×2):第一行是末端X位置对θ1和θ2的偏导:∂x/∂θ1 = -a1·sin(θ1) - a2·sin(θ1+θ2),∂x/∂θ2 = -a2·sin(θ1+θ2)。第二行是Y位置偏导:∂y/∂θ1 = a1·cos(θ1) + a2·cos(θ1+θ2),∂y/∂θ2 = a2·cos(θ1+θ2)。本篇仅用旋转关节子块(2×2),各列均为长度量纲(m),条件数无量纲化问题不显著。

 

条件数:cond(J) = σ_max/σ_min,通过SVD分解得到。cond=1表示最优(各向同性),cond>100表示严重各向异性(接近奇异),cond→∞表示完全奇异。

 

4. OOP代码实现

 

技术栈

 

严格限定:numpy, matplotlib。不引入pybullet、roboticstoolbox等任何第三方机器人库。通用DH正运动学复用公共模块。

 

项目结构树

 

singularity_trajectory_sim/

├── run_singularity_test.py

├── requirements.txt

├── robot/

│ ├── __init__.py

│ ├── common/

│ │ ├── __init__.py

│ │ └── kinematics.py

│ ├── models.py

│ ├── scara_ik.py

│ ├── jacobian.py

│ ├── singularity_checker.py

│ └── visualize_singularity.py

├── tests/

│ ├── __init__.py

│ └── test_singularity_trajectory.py

└── results/

 

核心源码

 

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

 

"""

数据模型:4-DOF SCARA机械臂连杆参数、笛卡尔轨迹配置

全部参数内置,自包含可复现

本篇聚焦运动学奇异分析,mass/com/inertia为后续动力学篇预留

"""

 

 

from dataclasses import dataclass, field

from typing import List, Tuple

 

 

@dataclass

class LinkConfig:

    """单连杆物理参数"""

    length: float = 0.0 # 连杆长度 (m)

    mass: float = 0.0 # 连杆质量 (kg),本篇未使用,为动力学篇预留

    com_position: float = 0.0 # 质心位置 (m),本篇未使用

    inertia_zz: float = 0.0 # 绕Z轴惯量 (kg·m²),本篇未使用

    joint_limits: Tuple[float, float] = (-3.14, 3.14)

 

 

@dataclass

class CartesianWaypoint:

    """单个笛卡尔路径点"""

    x: float = 0.0 # X坐标 (m)

    y: float = 0.0 # Y坐标 (m)

    z: float = 0.0 # Z坐标 (m)

    theta: float = 0.0 # 末端旋转角 (rad)

 

 

@dataclass

class CartesianTrajectoryConfig:

    """笛卡尔空间直线轨迹配置"""

    start_point: CartesianWaypoint = field(default_factory=CartesianWaypoint)

    end_point: CartesianWaypoint = field(default_factory=CartesianWaypoint)

    num_points: int = 50 # 插值点数量

    duration: float = 5.0 # 轨迹总时长 (s)

 

    def __post_init__(self):

        if self.num_points < 2:

            raise ValueError("插值点数量必须≥2")

 

 

@dataclass

class ScaraArmConfig:

    """

    4-DOF SCARA机械臂配置

    关节1:大臂旋转(肩),关节2:小臂旋转(肘)

    关节3:升降移动,关节4:末端旋转

    """

    links: List[LinkConfig] = field(default_factory=list)

    base_height: float = 0.30 # 基座高度 (m)

 

    def __post_init__(self):

        if not self.links:

            self.links = [

                LinkConfig(length=0.40, mass=1.5, com_position=0.20,

                          inertia_zz=0.032, joint_limits=(-3.14, 3.14)),

                LinkConfig(length=0.30, mass=1.0, com_position=0.15,

                          inertia_zz=0.015, joint_limits=(-3.14, 3.14)),

                LinkConfig(length=0.25, mass=0.5, com_position=0.125,

                          inertia_zz=0.005, joint_limits=(0.0, 0.25)),

                LinkConfig(length=0.0, mass=0.2, com_position=0.0,

                          inertia_zz=0.001, joint_limits=(-3.14, 3.14)),

            ]

 

"robot/scara_ik.py" — SCARA逆运动学(解析解)

 

"""

4-DOF SCARA逆运动学求解

平面2R部分解析求解(余弦定理),升降和末端旋转直接映射

固定elbow-up分支(θ2>0),复现奇异区速度尖峰

"""

 

 

import numpy as np

 

 

class ScaraInverseKinematics:

    """

    SCARA逆运动学求解器

    输入:末端笛卡尔位置 (x, y, z, theta)

    输出:关节角度 [θ1, θ2, d3, θ4]

    注意:本篇使用固定elbow-up单分支解析解,用于复现奇异区速度尖峰。

    分支切换导致的振荡现象需DLS+分支管理,留待下一篇。

    """

 

    def __init__(self, a1: float, a2: float):

        """

        a1: 大臂长度 (m)

        a2: 小臂长度 (m)

        """

        if a1 <= 0 or a2 <= 0:

            raise ValueError("连杆长度必须为正数")

        self.a1 = a1

        self.a2 = a2

        self.reach_max = a1 + a2

        self.reach_min = abs(a1 - a2)

 

    def solve_2r_analytical(self, x: float, y: float, elbow_up: bool = True):

        """

        2R平面解析逆解

        elbow_up=True时θ2>0(SCARA俯视平面中肘部向一侧弯曲)

        返回 (theta1, theta2) 或 None(不可达)

        """

        r_sq = x*x + y*y

        r = np.sqrt(r_sq)

 

        # 可达性检查:末端必须在工作空间圆环内

        if r > self.reach_max or r < self.reach_min:

            return None

 

        # 余弦定理求θ2

        cos_theta2 = (r_sq - self.a1**2 - self.a2**2) / (2 * self.a1 * self.a2)

 

        # 数值稳定性:裁剪到[-1, 1]

        cos_theta2 = np.clip(cos_theta2, -1.0, 1.0)

 

        if elbow_up:

            theta2 = np.arccos(cos_theta2) # θ2 > 0

        else:

            theta2 = -np.arccos(cos_theta2) # θ2 < 0

 

        # 求θ1

        alpha = np.arctan2(y, x)

        beta = np.arctan2(self.a2 * np.sin(theta2),

                          self.a1 + self.a2 * np.cos(theta2))

        theta1 = alpha - beta

 

        return theta1, theta2

 

    def solve_full(self, x: float, y: float, z: float,

                   theta: float, elbow_up: bool = True):

        """

        完整4-DOF逆解

        返回 [θ1, θ2, d3, θ4]

        """

        result_2r = self.solve_2r_analytical(x, y, elbow_up)

        if result_2r is None:

            return None

 

        theta1, theta2 = result_2r

 

        # 升降关节直接映射

        d3 = z

 

        # 末端旋转直接映射

        theta4 = theta

 

        return [theta1, theta2, d3, theta4]

 

    def solve_trajectory(self, waypoints, elbow_up: bool = True):

        """

        对整条轨迹逐点求解

        返回 list of (joint_angles, reachable)

        """

        results = []

        for wp in waypoints:

            if len(wp) == 4:

                x, y, z, theta = wp

            else:

                x, y = wp[0], wp[1]

                z = wp[2] if len(wp) > 2 else 0.0

                theta = wp[3] if len(wp) > 3 else 0.0

 

            angles = self.solve_full(x, y, z, theta, elbow_up)

            reachable = angles is not None

            results.append((angles, reachable))

 

        return results

 

"robot/jacobian.py" — 雅可比矩阵与条件数

 

"""

SCARA 2R平面雅可比矩阵计算与条件数分析

用于判定奇异位形(肘部奇异/伸直奇异)

"""

 

 

import numpy as np

 

 

class ScaraJacobian:

    """

    2R平面雅可比矩阵(仅XY平面旋转关节,忽略升降和末端旋转)

    J = ∂(x,y)/∂(θ1,θ2)

    本篇仅用2×2旋转子块,各列均为长度量纲(m),条件数无需额外尺度归一化

    """

 

    def __init__(self, a1: float, a2: float):

        if a1 <= 0 or a2 <= 0:

            raise ValueError("连杆长度必须为正数")

        self.a1 = a1

        self.a2 = a2

 

    def compute_jacobian(self, theta1: float, theta2: float):

        """

        计算2×2雅可比矩阵

        J[0,0] = ∂x/∂θ1, J[0,1] = ∂x/∂θ2

        J[1,0] = ∂y/∂θ1, J[1,1] = ∂y/∂θ2

        """

        J = np.zeros((2, 2))

 

        J[0, 0] = (-self.a1 * np.sin(theta1)

                   - self.a2 * np.sin(theta1 + theta2))

        J[0, 1] = -self.a2 * np.sin(theta1 + theta2)

        J[1, 0] = (self.a1 * np.cos(theta1)

                   + self.a2 * np.cos(theta1 + theta2))

        J[1, 1] = self.a2 * np.cos(theta1 + theta2)

 

        return J

 

    def condition_number(self, theta1: float, theta2: float,

                         cond_threshold: float = 100.0):

        """

        计算雅可比矩阵条件数(2-范数)

        cond = σ_max / σ_min

        返回 (cond, singular_flag, sigma_min, sigma_max)

        """

        J = self.compute_jacobian(theta1, theta2)

 

        try:

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

            sigma_max = singular_values[0]

            sigma_min = singular_values[1]

 

            if sigma_min < 1e-12:

                return float('inf'), True, sigma_min, sigma_max

 

            cond = sigma_max / sigma_min

            singular_flag = cond > cond_threshold

            return cond, singular_flag, sigma_min, sigma_max

 

        except np.linalg.LinAlgError:

            return float('inf'), True, 0.0, 0.0

 

    def manipulability_index(self, theta1: float, theta2: float):

        """

        可操作度指标 w = sqrt(det(J·J^T))

        值越小越接近奇异

        """

        J = self.compute_jacobian(theta1, theta2)

        w = np.sqrt(abs(np.linalg.det(J @ J.T)))

        return w

 

"robot/singularity_checker.py" — 奇异区检测与波动统计

 

"""

奇异区轨迹检测与逆解波动统计

不可达点与奇异点分开统计,分段差分避免虚假尖峰

"""

 

 

import numpy as np

 

 

class SingularityChecker:

    """

    奇异区检测器

    不可达(工作空间之外)与接近奇异(条件数>阈值)独立判定

    """

 

    def __init__(self, a1: float, a2: float,

                 cond_threshold: float = 100.0):

        self.jacobian = ScaraJacobian(a1, a2)

        self.cond_threshold = cond_threshold

 

    def analyze_trajectory(self, waypoints, joint_angles_list,

                           reachable_flags, dt: float):

        """

        分析整条轨迹

        waypoints: [(x,y,z,theta), ...]

        joint_angles_list: [[θ1,θ2,d3,θ4], ...]

        reachable_flags: [bool, ...]

        dt: 时间步长 (s)

        """

        n = len(waypoints)

        condition_numbers = []

        singular_flags = []

        manipulability = []

        sigma_mins = []

        sigma_maxs = []

 

        # 逐点计算条件数(仅对可达点)

        for i, (angles, reachable) in enumerate(

            zip(joint_angles_list, reachable_flags)

        ):

            if not reachable or angles is None:

                condition_numbers.append(float('inf'))

                singular_flags.append(False) # 不可达≠奇异

                manipulability.append(0.0)

                sigma_mins.append(0.0)

                sigma_maxs.append(0.0)

                continue

 

            theta1, theta2 = angles[0], angles[1]

            cond, is_singular, sig_min, sig_max = (

                self.jacobian.condition_number(

                    theta1, theta2, self.cond_threshold

                )

            )

            w = self.jacobian.manipulability_index(theta1, theta2)

 

            condition_numbers.append(cond)

            singular_flags.append(is_singular)

            manipulability.append(w)

            sigma_mins.append(sig_min)

            sigma_maxs.append(sig_max)

 

        # 分段数值差分求关节速度(中心差分,跳过不可达点)

        joint_velocities = [[], []]

        joint_accelerations = [[], []]

 

        for j in range(2): # θ1, θ2

            vel_j = [float('nan')] * n

            angles_j = [angles[j] if (angles and reachable_flags[i])

                        else float('nan')

                        for i, angles in enumerate(joint_angles_list)]

 

            # 找可达区间,在区间内做差分

            i = 0

            while i < n:

                if not reachable_flags[i]:

                    i += 1

                    continue

                # 找到当前可达区间的起始

                start = i

                while i < n and reachable_flags[i]:

                    i += 1

                end = i - 1

 

                # 区间内有至少2个点才做差分

                if end - start >= 1:

                    # 首点:前向差分

                    vel_j[start] = (

                        (angles_j[start+1] - angles_j[start]) / dt

                    )

                    # 中间点:中心差分

                    for k in range(start+1, end):

                        vel_j[k] = (

                            (angles_j[k+1] - angles_j[k-1]) / (2.0 * dt)

                        )

                    # 末点:后向差分

                    if end > start:

                        vel_j[end] = (

                            (angles_j[end] - angles_j[end-1]) / dt

                        )

 

            joint_velocities[j] = vel_j

 

        # 加速度(对速度差分,同样分段)

        for j in range(2):

            acc_j = [float('nan')] * n

            vel_j = joint_velocities[j]

 

            i = 0

            while i < n:

                if not reachable_flags[i]:

                    i += 1

                    continue

                start = i

                while i < n and reachable_flags[i]:

                    i += 1

                end = i - 1

 

                if end - start >= 2:

                    acc_j[start] = (vel_j[start+1] - vel_j[start]) / dt

                    for k in range(start+1, end):

                        acc_j[k] = (

                            (vel_j[k+1] - vel_j[k-1]) / (2.0 * dt)

                        )

                    acc_j[end] = (vel_j[end] - vel_j[end-1]) / dt

 

            joint_accelerations[j] = acc_j

 

        # 波动统计(分别统计两关节,排除nan)

        vel_std_j1 = float('nan')

        vel_std_j2 = float('nan')

        vel_peak_j1 = 0.0

        vel_peak_j2 = 0.0

 

        valid_vels_j1 = [v for v in joint_velocities[0]

                         if not np.isnan(v)]

        valid_vels_j2 = [v for v in joint_velocities[1]

                         if not np.isnan(v)]

 

        if valid_vels_j1:

            vel_std_j1 = float(np.std(valid_vels_j1))

            vel_peak_j1 = float(max(abs(v) for v in valid_vels_j1))

        if valid_vels_j2:

            vel_std_j2 = float(np.std(valid_vels_j2))

            vel_peak_j2 = float(max(abs(v) for v in valid_vels_j2))

 

        valid_conds = [c for c in condition_numbers

                       if c != float('inf')]

        cond_peak = max(valid_conds) if valid_conds else float('inf')

 

        singular_count = sum(1 for s in singular_flags if s)

        unreachable_count = sum(1 for r in reachable_flags if not r)

 

        return {

            'condition_numbers': condition_numbers,

            'singular_flags': singular_flags,

            'manipulability': manipulability,

            'sigma_mins': sigma_mins,

            'sigma_maxs': sigma_maxs,

            'joint_velocities': joint_velocities,

            'joint_accelerations': joint_accelerations,

            'vel_std_j1': vel_std_j1,

            'vel_std_j2': vel_std_j2,

            'vel_peak_j1': vel_peak_j1,

            'vel_peak_j2': vel_peak_j2,

            'cond_peak': cond_peak,

            'singular_count': singular_count,

            'unreachable_count': unreachable_count,

            'total_points': n

        }

 

    def generate_report(self, analysis_result):

        """生成奇异区检测报告(不可达与奇异分开统计)"""

        lines = ["=" * 60, "奇异区轨迹检测报告", "=" * 60]

        lines.append(f"总路径点: {analysis_result['total_points']}")

        lines.append(

            f"不可达点数: {analysis_result['unreachable_count']}"

        )

        lines.append(

            f"接近奇异点数: {analysis_result['singular_count']}"

        )

        if analysis_result['total_points'] > 0:

            pct = (100.0 * analysis_result['singular_count']

                   / analysis_result['total_points'])

            lines.append(f" ({pct:.1f}%的路径点接近奇异)")

        lines.append(

            f"峰值条件数: {analysis_result['cond_peak']:.1f}"

        )

        if not np.isnan(analysis_result['vel_std_j1']):

            lines.append(

                f"关节1速度标准差: "

                f"{analysis_result['vel_std_j1']:.4f} rad/s"

            )

            lines.append(

                f"关节1速度峰值: "

                f"{analysis_result['vel_peak_j1']:.4f} rad/s"

            )

        if not np.isnan(analysis_result['vel_std_j2']):

            lines.append(

                f"关节2速度标准差: "

                f"{analysis_result['vel_std_j2']:.4f} rad/s"

            )

            lines.append(

                f"关节2速度峰值: "

                f"{analysis_result['vel_peak_j2']:.4f} rad/s"

            )

        lines.append("-" * 60)

 

        if analysis_result['unreachable_count'] > 0:

            lines.append("✗ 存在不可达点(末端超出工作空间圆环)")

        if analysis_result['singular_count'] > 0:

            lines.append(

                "⚠ 轨迹经过肘部/伸直奇异区,"

                "逆运动学求解存在速度尖峰风险"

            )

            lines.append(

                " 建议:调整路径远离内圈(r≈|a1-a2|)或外圈(r≈a1+a2),"

                "或采用阻尼最小二乘法"

            )

        if (analysis_result['unreachable_count'] == 0

                and analysis_result['singular_count'] == 0):

            lines.append("✓ 轨迹全程远离奇异区,IK求解稳定")

 

        return "\n".join(lines)

 

"robot/visualize_singularity.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 SingularityVisualizer:

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

        self.results_dir = results_dir

        import os

        os.makedirs(resul

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

Logo

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

更多推荐