python的机器人基础原理工业场景模拟第九篇:装配机器人,给定末端目标位姿,调用工具箱求解逆运行学,输出全部数学解暂不筛选。
《Python机器人基础原理》第九篇:装配机器人——给定末端目标位姿,调用工具箱求解逆运动学,输出全部数学解暂不筛选
1. 实际工业应用场景
周三下午,某3C产品装配车间,手机中框与玻璃后盖的自动贴合工位。
机械臂从传送带取料,运动到贴合工装上方的预设位姿,吸盘下降、释放、完成贴合。看起来行云流水——直到产线换型,新款手机的中框尺寸变了2 mm,工装夹具重新设计,原来的示教点位全部作废。
调试员小陈站在机器人控制柜前,手里攥着一张CAD图纸,上面标着新工装上贴合面的精确位姿:x=285.3 mm, y=42.7 mm, z=380 mm,绕z轴旋转-15°。他知道该让机器人末端到这个位姿,但关节该转到多少度?
"以前都是示教器上手动挪过去,一个点一个点试。"小陈跟旁边负责工艺的李工说。
"这款中框有4个贴合工位,每个工位要取料、中转、贴合、退出四个位姿。你手动示教十六个点,每个点调半小时,两天过去了。而且换下一个型号又得重来。"
"那怎么办?"
"你给我写一个逆运动学求解器。我把目标位姿输进去,你给我算出关节该转多少度——全部可能的角度组合都给我,我自己在示教器上挑一个不撞机的。"
这就是本篇要解决的问题:给定一台4-DOF SCARA装配机器人的结构参数和末端目标位姿(x, y, z, 绕z轴姿态角φ),调用符号推导工具箱求解逆运动学,输出全部数学解(多个关节角度组合),暂不筛选最优解。不调用任何第三方机器人商业库,用sympy做符号推导获取解析表达式,numpy做数值代入。让调试工程师在换型时不再靠手感试点,而是用数学拿到全部可能的关节构型。
机器人构型参数:4-DOF SCARA装配机器人,3个旋转关节(基座回转、大臂水平旋转、小臂水平旋转,三轴竖直平行)+1个垂直平移关节(z轴升降)。连杆参数:大臂长350 mm,小臂长280 mm,z轴行程±75 mm(相对基座面),基座安装高度350 mm(编入D-H表d₁=0.350 m)。末端工具为真空吸盘,质量0.8 kg,吸盘直径120 mm。目标位姿精度要求±0.1 mm。单节拍周期4 s。
⚠️ 声明:本篇全部代码为课程离线仿真,运行在普通PC上,用于逆运动学解析解推导与验证。不可直接用于真实机器人控制器、PLC或装配产线。真实装配现场的逆解筛选需结合关节限位、奇异规避、碰撞检测与动力学约束。
2. 痛点分析
课程模块 本篇对应的工程痛点
第3章 连杆参数及运动学求解 → 3.4 逆运动学 给定末端目标位姿,求解全部可能的关节角度组合,SCARA构型存在肘部上/下翻两个解分支
第3章 连杆参数及运动学求解 → 3.2 D-H参数法 基于标准D-H参数表建立正运动学方程,作为逆解推导的起点
第3章 连杆参数及运动学求解 → 3.3 正运动学 逆解自洽校验:将求出的关节角代回正运动学,验证末端是否回到目标位姿
本篇代码解决 L1 离线仿真校核层。输入末端目标位姿+机器人D-H参数,输出全部逆解关节角度。代码不是设备实时控制器,不做解筛选(不判断哪组解更优),不做轨迹规划,不做动力学校验。它只做一件事:把笛卡尔空间的目标位姿"翻译"回关节空间的所有可能姿态——让工艺工程师拿到全部候选构型,在示教器上排除撞机的解后直接选用。
不覆盖:逆解最优筛选、关节限位检查、碰撞检测、轨迹插补、动力学、5轴以上冗余构型、数值迭代逆解(本篇用解析法)。
3. 核心逻辑讲解
物理直觉
逆运动学就是正运动学的反问题。正运动学问的是"我知道每节脊椎转了多少度,指尖在哪";逆运动学问的是"我知道指尖要在哪个位置,每节脊椎该转多少度"。
听起来只是把方程反着解,实际上难度天差地别。正运动学是"往前算"——逐级矩阵乘法,结果唯一确定。逆运动学是"倒推"——一个末端位置可能对应多种关节组合,就像你伸手拿桌上的杯子,可以从左边伸过去,也可以从右边绕过去,甚至手肘朝上翻着够。
SCARA机器人是逆解最简单的构型之一。前两个旋转关节决定末端在XY平面上的位置(纯平面几何,余弦定理直接解),第三个旋转关节决定末端姿态角(由目标姿态唯一确定),第四个平移关节决定z高度(直接减法)。所以SCARA的逆解有解析表达式,不需要迭代猜,用笔和纸就能推导出全部解。
业务逻辑 → 代码映射
输入:末端目标位姿 (x, y, z, φ) + SCARA几何参数 (L₁, L₂, d₁)
↓
步骤1:数据校验——目标位姿是否在可达空间内
水平距离 r = √(x²+y²),检查 |L₁-L₂| ≤ r ≤ L₁+L₂
↓
步骤2:平面几何求解关节2角度(肘角)
- 余弦定理:r² = L₁² + L₂² + 2·L₁·L₂·cos(θ₂)
- 两个解分支:肘部"下翻"θ₂=-acos(...) 与"上翻"θ₂=+acos(...)
↓
步骤3:求解关节1角度
- θ₁ = atan2(y, x) - atan2(L₂·sin(θ₂), L₁+L₂·cos(θ₂))
- 每个θ₂分支对应唯一θ₁
↓
步骤4:求解关节3角度(姿态约束)
- 目标姿态 φ = θ₁ + θ₂ + θ₃ → θ₃ = φ - θ₁ - θ₂
- 姿态角唯一确定,无±π二义性(加π会改变末端朝向)
↓
步骤5:求解关节4(z轴平移)
- d₄ = z_target - d₁(基座高度编入D-H表d₁,保证FK自洽)
↓
步骤6:组合全部分支 → 2组数学解(肘下翻/肘上翻)
↓
步骤7:正运动学自洽校验(将每组解代回正运动学验证误差)
↓
输出:全部数学解(2组关节角度)+ 校验结果
为什么适合离线仿真
- 逆解解析表达式一旦推导出来就是纯代数运算,零迭代、零微分方程
- sympy符号推导确保公式正确性,避免手算抄错符号
- 全部解输出后工程师可以人工筛选——排除超出关节限位的解、排除导致奇异的解
- 普通PC毫秒级完成,换型时批量求解多个目标位姿毫无压力
关键公式文字翻译
平面距离约束:末端到基座中心的水平距离 r = √(x²+y²) 必须介于 |L₁-L₂| 和 L₁+L₂ 之间,否则够不着。这是可达性的充要条件(圆环域)。
余弦定理求肘角:已知三角形三边(大臂L₁、小臂L₂、水平距离r),用余弦定理 r² = L₁²+L₂²+2·L₁·L₂·cos(θ₂) 直接算出肘关节角θ₂。θ₂=0时大小臂完全伸直,θ₂=π时完全折叠。存在两个方向——肘部向上翻(+acos)和向下弯(-acos),对应逆解的两个分支。
基座角求解:θ₁ = 目标方位角atan2(y,x) 减去 小臂末端方位角atan2(L₂·sinθ₂, L₁+L₂·cosθ₂)。几何意义是从基座到末端的总方位角中扣除小臂带来的偏转角。
姿态角叠加:SCARA末端绕z轴的姿态角φ等于三个旋转关节角之和(φ = θ₁+θ₂+θ₃)。已知φ,反推θ₃唯一确定,不存在±π二义性——因为旋转一周后末端朝向会改变,不再是同一个位姿。
z轴直接映射:垂直平移关节的目标位置 d₄ = z_target - d₁,线性关系,唯一解。d₁编入D-H表第一连杆偏距,保证正运动学z = d₁ + d₄ = z_target。
4. OOP代码实现
技术栈
严格限定:numpy, scipy, matplotlib, sympy。不引入pybullet、roboticstoolbox等任何第三方机器人库。sympy用于符号推导逆解解析表达式(推导主值分支,第二分支由几何对称性手工补充),numpy用于数值代入和正运动学校验。
项目结构树
robot_inverse_kinematics/
├── run_inverse_kinematics.py
├── requirements.txt
├── ik/
│ ├── __init__.py
│ ├── models.py
│ ├── dh_transform.py
│ ├── forward_kinematics.py
│ ├── inverse_kinematics.py
│ └── visualize.py
├── tests/
│ ├── __init__.py
│ └── test_inverse_kinematics.py
└── results/
核心源码
"ik/models.py" — dataclass数据模型
"""
4-DOF SCARA装配机器人数据模型
连杆参数、D-H表、末端目标位姿结构体
"""
from dataclasses import dataclass, field
from typing import List
import numpy as np
@dataclass
class DHParam:
"""
标准D-H参数(Denavit-Hartenberg经典形式)
SCARA前3轴为旋转关节,第4轴为平移关节
"""
theta: float # 关节角 (rad),旋转关节变量
d: float # 连杆偏距 (m),平移关节变量
a: float # 连杆长度 (m)
alpha: float # 连杆扭角 (rad)
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}必须为有限数值, 实际{val}")
@dataclass
class SCARAParams:
"""
4-DOF SCARA装配机器人完整参数
3旋转+1垂直平移,α序列 [0, 0, 0, -π/2]
前3轴z轴平行(水平面内旋转),第4轴z翻转为垂直平移
零位:大臂+小臂水平伸直,末端z = d₁ = 0.350 m
"""
links: List[DHParam]
joint_names: List[str] = field(default_factory=lambda: [
'joint1_base_yaw',
'joint2_elbow_yaw',
'joint3_wrist_roll',
'joint4_z_lift'
])
tool_mass: float = 0.8 # 真空吸盘质量 (kg)
tool_diameter: float = 0.120 # 吸盘直径 (m)
def __post_init__(self):
if len(self.links) != 4:
raise ValueError(f"4-DOF SCARA需要4组D-H参数, 实际{len(self.links)}组")
if len(self.joint_names) != 4:
raise ValueError(f"需要4个关节名称, 实际{len(self.joint_names)}个")
@classmethod
def default_assembly_scar(cls):
"""
默认4-DOF SCARA装配机器人参数
大臂350mm, 小臂280mm, 基座高度d₁=350mm编入D-H表
α序列 [0, 0, 0, -π/2]:前3轴z轴平行(水平旋转),第4轴垂直平移
"""
pi = 3.141592653589793
links = [
# theta, d, a, alpha
DHParam(0.0, 0.350, 0.350, 0.0),
DHParam(0.0, 0.0, 0.280, 0.0),
DHParam(0.0, 0.0, 0.0, 0.0),
DHParam(0.0, 0.0, 0.0, -pi/2),
]
return cls(links=links, tool_mass=0.8, tool_diameter=0.120)
def get_reach_max(self):
"""最大水平可达半径 = L1 + L2"""
return self.links[0].a + self.links[1].a
def get_reach_min(self):
"""最小水平可达半径 = |L1 - L2|"""
return abs(self.links[0].a - self.links[1].a)
def get_base_height(self):
"""基座高度 = d₁"""
return self.links[0].d
@dataclass
class TargetPose:
"""
末端目标位姿
x, y, z: 基座标系下位置 (m)
phi: 绕基座z轴的目标姿态角 (rad),用φ避免与第4关节d₄混淆
"""
x: float
y: float
z: float
phi: float
def __post_init__(self):
for name, val in [('x', self.x), ('y', self.y), ('z', self.z)]:
if not np.isfinite(val):
raise ValueError(f"TargetPose.{name}必须为有限数值")
if not np.isfinite(self.phi):
raise ValueError("TargetPose.phi必须为有限数值")
def get_horizontal_distance(self):
"""到基座中心的水平距离"""
return np.sqrt(self.x**2 + self.y**2)
"ik/dh_transform.py" — 标准D-H变换矩阵
"""
标准D-H变换矩阵计算
SCARA构型:前3轴旋转,第4轴平移
"""
import numpy as np
from ik.models import DHParam
class DHTransform:
"""单级标准D-H变换矩阵生成器"""
@staticmethod
def compute_T(theta, d, a, alpha):
"""
标准D-H齐次变换矩阵 T_{i-1}^{i}
变换顺序:绕z转θ → 沿z移d → 沿x移a → 绕x转α
"""
ct = np.cos(theta)
st = np.sin(theta)
ca = np.cos(alpha)
sa = np.sin(alpha)
return np.array([
[ct, -st * ca, st * sa, a * ct],
[st, ct * ca, -ct * sa, a * st],
[0.0, sa, ca, d],
[0.0, 0.0, 0.0, 1.0]
], dtype=float)
@staticmethod
def compute_T_from_param(param: DHParam):
"""从DHParam对象计算变换矩阵"""
return DHTransform.compute_T(
param.theta, param.d, param.a, param.alpha)
"ik/forward_kinematics.py" — 正运动学(用于逆解自洽校验)
"""
正运动学求解器:逐级左乘D-H变换矩阵
用于逆解校验——将求出的关节角代回,验证末端是否回到目标位姿
"""
import numpy as np
from ik.models import SCARAParams
from ik.dh_transform import DHTransform
class ForwardKinematicsSCARA:
"""4-DOF SCARA正运动学求解器"""
def __init__(self, params: SCARAParams):
self.params = params
def compute_pose(self, joint_values):
"""
joint_values: [θ1, θ2, θ3, d4]
θ1,θ2,θ3: 旋转关节角 (rad)
d4: 第4轴平移量 (m)
返回:末端齐次矩阵 T_0^4
"""
if len(joint_values) != 4:
raise ValueError(f"需要4个关节值, 实际{len(joint_values)}个")
T = np.eye(4)
# 前3轴为旋转关节:将关节值写入params.links供矩阵计算读取
for i in range(3):
self.params.links[i].theta = joint_values[i]
T = T @ DHTransform.compute_T_from_param(self.params.links[i])
# 第4轴为平移关节
self.params.links[3].d = joint_values[3]
T = T @ DHTransform.compute_T_from_param(self.params.links[3])
return T
def get_position_and_yaw(self, joint_values):
"""
提取末端位置(x,y,z)和绕z轴姿态角φ
返回:(x, y, z, phi)
姿态角 φ = θ1+θ2+θ3,归一化到 [-π, π]
"""
T = self.compute_pose(joint_values)
x = T[0, 3]
y = T[1, 3]
z = T[2, 3]
phi = joint_values[0] + joint_values[1] + joint_values[2]
phi = np.arctan2(np.sin(phi), np.cos(phi))
return x, y, z, phi
"ik/inverse_kinematics.py" — 逆运动学求解器(核心)
"""
逆运动学求解器:给定末端目标位姿,求解全部数学解
SCARA构型解析解:平面几何法 + sympy符号推导
输出全部解分支(2组:肘部下翻/上翻),暂不筛选
"""
import numpy as np
import sympy as sp
from ik.models import SCARAParams, TargetPose
from ik.forward_kinematics import ForwardKinematicsSCARA
class InverseKinematicsSCARA:
"""
4-DOF SCARA逆运动学解析求解器
基于平面几何(余弦定理)+ sympy符号推导主值分支
输出全部数学解(2组),不做筛选
"""
def __init__(self, params: SCARAParams):
self.params = params
# FK实例与IK共享同一个params对象
# IK求解时写入theta/d到params.links,FK验证时读取同一状态
# 这是设计选择:单目标求解场景下保证正逆闭环数据一致性
self.fk = ForwardKinematicsSCARA(params)
self.L1 = params.links[0].a # 大臂长 (m)
self.L2 = params.links[1].a # 小臂长 (m)
self.d1 = params.links[0].d # 基座高度 (m),编入D-H表d₁
# sympy符号推导:建立逆解解析表达式(主值分支)
self._build_symbolic_solver()
def _build_symbolic_solver(self):
"""
用sympy建立逆运动学解析解的通用表达式
推导主值分支(肘部下翻),第二分支由几何对称性手工补充
输入符号:x, y, L1, L2
输出符号:θ1, θ2(主值)
"""
x, y, L1, L2 = sp.symbols('x y L1 L2', real=True)
# 水平距离
r = sp.sqrt(x**2 + y**2)
# 余弦定理求肘角θ2(内角)
# r² = L1² + L2² - 2·L1·L2·cos(π-θ2) = L1²+L2²+2·L1·L2·cos(θ2)
# → cos(θ2) = (r² - L1² - L2²) / (2·L1·L2)
cos_theta2 = (r**2 - L1**2 - L2**2) / (2 * L1 * L2)
theta2_elbow = sp.acos(cos_theta2) # 主值:肘部"下翻"分支
# θ1 = atan2(y, x) - atan2(L2·sin(θ2), L1+L2·cos(θ2))
phi = sp.atan2(y, x)
psi = sp.atan2(L2 * sp.sin(theta2_elbow),
L1 + L2 * sp.cos(theta2_elbow))
theta1_sol = phi - psi
# 存储符号表达式供数值代入
self._sym_theta1 = sp.lambdify((x, y, L1, L2), theta1_sol, 'numpy')
self._sym_theta2_elbow = sp.lambdify((x, y, L1, L2),
theta2_elbow, 'numpy')
self._sym_cos_theta2 = sp.lambdify((x, y, L1, L2),
cos_theta2, 'numpy')
def solve_all(self, target: TargetPose):
"""
求解全部数学解
SCARA构型共2组解:
分支A: 肘部下翻(θ2=-acos(...)),θ3=φ-θ1-θ2
分支B: 肘部上翻(θ2=+acos(...)),θ3=φ-θ1-θ2
θ3由姿态约束唯一确定,无±π二义性
返回:列表 of [θ1, θ2, θ3, d4],每个元素单位rad/rad/rad/m
"""
x, y, z = target.x, target.y, target.z
phi = target.phi # 目标姿态角,用φ避免与关节序号混淆
# 可达性检查
r = np.sqrt(x**2 + y**2)
r_max = self.L1 + self.L2
r_min = abs(self.L1 - self.L2)
if r > r_max + 1e-6:
raise ValueError(
f"目标点水平距离{r*1000:.1f}mm超出最大可达半径"
f"{(r_max)*1000:.1f}mm")
if r < r_min - 1e-6:
raise ValueError(
f"目标点水平距离{r*1000:.1f}mm小于最小可达半径"
f"{(r_min)*1000:.1f}mm")
# 数值代入符号表达式(主值分支)
cos_t2 = float(self._sym_cos_theta2(x, y, self.L1, self.L2))
cos_t2 = np.clip(cos_t2, -1.0, 1.0)
# 肘部下翻分支(主值,θ2取负)
theta2_elbow = -np.arccos(cos_t2)
theta1_elbow = float(self._sym_theta1(x, y, self.L1, self.L2))
# 姿态约束唯一确定θ3
theta3_elbow = phi - theta1_elbow - theta2_elbow
# 肘部上翻分支(几何对称,θ2取正)
theta2_shoulder = np.arccos(cos_t2)
# 对应θ1:用对称性重新计算
phi_angle = np.arctan2(y, x)
psi_shoulder = np.arctan2(self.L2 * np.sin(theta2_shoulder),
self.L1 + self.L2 * np.cos(theta2_shoulder))
theta1_shoulder = phi_angle - psi_shoulder
theta3_shoulder = phi - theta1_shoulder - theta2_shoulder
# z轴平移:d4 = z_target - d1(d1编入D-H表,保证FK自洽)
d4 = z - self.d1
solutions = []
solutions.append([theta1_elbow, theta2_elbow,
theta3_elbow, d4]) # 分支A:肘部下翻
solutions.append([theta1_shoulder, theta2_shoulder,
theta3_shoulder, d4]) # 分支B:肘部上翻
return solutions
def verify_solution(self, joint_values, target: TargetPose,
tol=1e-6):
"""
正运动学自洽校验:将关节角代回正运动学,验证末端位姿
z方向:FK z = d1 + d4 = d1 + (z_target - d1) = z_target ✓
姿态:φ = θ1+θ2+θ3 ✓
"""
x_calc, y_calc, z_calc, phi_calc = \
self.fk.get_position_and_yaw(joint_values)
pos_error = np.sqrt((x_calc - target.x)**2 +
(y_calc - target.y)**2 +
(z_calc - target.z)**2)
# 姿态角误差(考虑2π周期)
yaw_error = abs(phi_calc - target.phi)
yaw_error = min(yaw_error, 2*np.pi - yaw_error)
return {
'pass': pos_error < tol and yaw_error < tol,
'position_error_m': float(pos_error),
'yaw_error_rad': float(yaw_error),
'computed_pose': (x_calc, y_calc, z_calc, phi_calc)
}
"ik/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_all_solutions_positions(self, solutions, target, fk_solver):
"""
① 全部逆解末端位置散点验证图
工程结论:两组解通过正运动学算出的末端位置应汇聚于同一目标点
(散点重合=逆解正确),若偏差大则说明数值计算有误
"""
fig, ax = plt.subplots(figsize=(8, 8))
# 目标位置(红色星标)
ax.scatter(target.x * 1000, target.y * 1000,
color='red', s=200, marker='*',
label='Target Position', zorder=5)
# 各解末端位置(通过正运动学算出实际位置)
colors = ['blue', 'green']
labels = ['Sol 1 (Elbow ↓)', 'Sol 2 (Elbow ↑)']
for i, sol in enumerate(solutions):
x_calc, y_calc, z_calc, phi_calc = \
fk_solver.get_position_and_yaw(sol)
ax.scatter(x_calc * 1000, y_calc * 1000,
color=colors[i], s=100, marker='o',
label=labels[i],
zorder=4, alpha=0.8)
# 坐标标注放在散点旁边,不塞进legend
ax.annotate(f'({x_calc*1000:.1f}, {y_calc*1000:.1f})',
(x_calc * 1000, y_calc * 1000),
textcoords="offset points", xytext=(0, 10),
ha='center', fontsize=7, color=colors[i])
ax.set_xlabel('X (mm)')
ax.set_ylabel('Y (mm)')
ax.set_title('All IK Solutions — End Effector Position Verification\n'
'(Both solutions converge to target = IK correct)')
ax.legend(fontsize=8)
ax.grid(True, alpha=0.3)
ax.set_aspect('equal')
plt.tight_layout()
path = f"{self.results_dir}/01_all_solutions_positions.png"
plt.savefig(path, dpi=150)
plt.close()
return path
def plot_joint_angles_comparison(self, solutions):
"""
② 各解关节角度对比柱状图
工程结论:直观对比2组解的关节角差异,
帮助工程师判断哪组解关节运动范围更小、更接近当前构型
"""
labels = ['Sol 1 (Elbow ↓)', 'Sol 2 (Elbow ↑)']
joint_names = ['θ₁ (rad)', 'θ₂ (rad)', 'θ₃ (rad)', 'd₄ (mm)']
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
axes = axes.flatten()
colors = ['steelblue', 'orange', 'green', 'red']
for j in range(4):
vals = [sol[j] if j < 3 else sol[j]*1000
for sol in solutions]
ax = axes[j]
bars = ax.bar(labels, vals, color=colors[j], alpha=0.8,
edgecolor='black')
ax.set_title(f'Joint {joint_names[j]} Across Solutions')
ax.set_ylabel(joint_names[j])
ax.grid(True, alpha=0.3, axis='y')
for bar, val in zip(bars, vals):
offset = 0.01 * max(abs(min(vals)), abs(max(vals)), 1e-6)
ax.text(bar.get_x() + bar.get_width()/2,
bar.get_height() + offset,
f'{val:.3f}', ha='center', va='bottom',
fontsize=8, fontweight='bold')
plt.tight_layout()
path = f"{self.results_dir}/02_joint_angles_comparison.png"
plt.savefig(path, dpi=150)
plt.close()
return path
def plot_workspace_boundary(self, params, target):
"""
③ 工作空间边界图
工程结论:绘制SCARA可达圆环(内半径|L1-L2|, 外半径L1+L2),
标注目标点是否在可达范围内
"""
fig, ax = plt.subplots(figsize=(8, 8))
L1 = params.links[0].a
L2 = params.links[1].a
r_min = abs(L1 - L2)
r_max = L1 + L2
theta_circle = np.linspace(0, 2*np.pi, 200)
ax.plot(r_max * np.cos(theta_circle) * 1000,
r_max * np.sin(theta_circle) * 1000,
'b-', linewidth=2, label=f'Max Reach {(r_max*1000):.0f} mm')
ax.plot(r_min * np.cos(theta_circle) * 1000,
r_min * np.sin(theta_circle) * 1000,
'r--', linewidth=2, label=f'Min Reach {(r_min*1000):.0f} mm')
# 标注目标点
r_target = np.sqrt(target.x**2 + target.y**2) * 1000
ax.plot(target.x * 1000, target.y * 1000,
'r*', markersize=15,
label=f'Target (r={r_target:.1f} mm)')
ax.set_xlabel('X (mm)')
ax.set_ylabel('Y (mm)')
ax.set_title('SCARA Workspace Boundary (Horizontal Plane)')
ax.legend(fontsize=8)
ax.grid(True, alpha=0.3)
ax.set_aspect('equal')
plt.tight_layout()
path = f"{self.results_dir}/03_workspace_boundary.png"
plt.savefig(path, dpi=150)
plt.close()
return path
def plot_solution_distribution(self, solutions):
"""
④ 解分支分布图(θ1, θ2对比)
工程结论:用分组柱状图展示2组解在(θ1, θ2)上的差异,
直观显示构型差异——分离越远说明肘部姿态区别越大
"""
theta1_vals = [sol[0] for sol in solutions]
theta2_vals = [sol[1] for sol in solutions]
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
labels = ['Elbow ↓', 'Elbow ↑']
x_pos = np.arange(len(labels))
bars1 = ax1.bar(x_pos, theta1_vals, color='steelblue',
edgecolor='black', alpha=0.8, width=0.5)
ax1.set_xticks(x_pos)
ax1.set_xticklabels(labels)
ax1.set_ylabel('θ₁ (rad)')
ax1.set_title('θ₁ Across Solution Branches')
ax1.grid(True, alpha=0.3, axis='y')
for bar, val in zip(bars1, theta1_vals):
ax1.text(bar.get_x() + bar.get_width()/2,
bar.get_height() + 0.05,
f'{val:.3f}', ha='center', va='bottom', fontsize=9)
bars2 = ax2.bar(x_pos, theta2_vals, color='orange',
edgecolor='black', alpha=0.8, width=0.5)
ax2.set_xticks(x_pos)
ax2.set_xticklabels(labels)
ax2.set_ylabel('θ₂ (rad)')
ax2.set_title('θ₂ Across Solution Branches')
ax2.grid(True, alpha=0.3, axis='y')
for bar, val in zip(bars2, theta2_vals):
ax2.text(bar.get_x() + bar.get_width()/2,
bar.get_height() + 0.05,
f'{val:.3f}', ha='center', va='bottom', fontsize=9)
plt.tight_layout()
path = f"{self.results_dir}/04_solution_distribution.png"
利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)