《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解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!