《Python机器人基础原理》第八十九篇(修正版):码垛仿真——关节限位约束下的工作空间采样与可视化
声明:本篇全部为课程离线仿真代码,不能直接用于真实机器人控制器,规避工程误导风险。
1. 实际工业应用场景
周五下午,电商仓储分拣中心。一台4-DOF码垛机械臂(J1基座回转,工作范围±165°,额定扭矩320 N·m;J2大臂俯仰,范围-20°~+85°,额定扭矩450 N·m;J3小臂俯仰,范围-90°~+60°,额定扭矩280 N·m;J4末端翻转,范围±180°,额定扭矩80 N·m;连杆长度L₁=0.45 m,L₂=0.55 m,L₃=0.45 m,末端执行器偏移d₄=0.15 m;末端执行器质量2.5 kg,额定负载10.0 kg;重复定位精度±0.5 mm)正在做新垛型的离线轨迹规划。
调试工程师阿芳盯着屏幕上密密麻麻的工作空间散点云:"我用蒙特卡洛法随机撒了50000个关节角样本,算出来的工作空间边界看起来没问题,但导入轨迹规划器之后,有些路径点机械臂根本到不了——报错说关节超限。"
旁边的工程师大刘凑过来:"你撒样本的时候用了什么区间?"
"全域
"[-π, π]"均匀撒的,然后……"
"那问题就在这。全域撒点里大量样本落在限位之外——J2只能-20°~+85°,J3只能-90°~+60°,你撒出来的点里将近九成是废的。更关键的是,如果你不做限位剔除直接算正运动学,那些超限点会生成虚假的工作空间边界,轨迹规划器以为那里可达。"
"所以正确做法是?"
"两种思路。第一种:直接在合法区间内采样——
"np.random.uniform(lower, upper)"对每个关节独立抽,这样每个样本天生合法,剔除率为0,效率最高。第二种:全域撒点后做拒绝采样,超限的扔掉——这样能精确统计'如果不加限位会浪费多少算力'。两种我都给你实现了,对比着看。"
"等等,那J4呢?它绕Z轴转,末端位置不是不变吗?"
"你观察得对。J4只影响末端姿态(绕Z的旋转),对位置完全没有贡献。工作空间分析本质上只需要J1~J3三个关节。J4参与采样纯属浪费,但为了让读者理解完整4-DOF模型,我保留它并在代码里标注。"
阿芳点头:"也就是说,采样逻辑要跟物理结构匹配——位置工作空间看前3个关节就够了。"
构型约定:本篇采用4-DOF码垛机械臂(R-R-R-R构型)。标准DH参数:连杆1(基座回转)d₁=0.45 m;连杆2(大臂俯仰)a₂=0.55 m, α₂=-90°;连杆3(小臂俯仰)a₃=0.45 m;连杆4(末端翻转)d₄=0.15 m。J4不影响末端位置,工作空间分析降维至3-DOF。技术栈限定numpy + matplotlib。
⚠️ 声明:本篇全部代码为课程离线仿真,运行在普通PC上,用于工作空间分析验证。代码中不包含任何真实机器人控制器实时运算或安全逻辑。不可直接用于产线设备。动力学参数(额定扭矩、负载等)与本篇纯运动学分析无关,留待后续动力学篇章使用。
2. 痛点分析
课程模块 本篇对应的工程痛点
第2章 位姿描述与坐标变换 → 2.3 工作空间分析 蒙特卡洛采样若不限关节角范围,生成的工作空间包含不可达区域
第3章 连杆参数及运动学求解 → 3.4 工作空间边界 关节限位是工作空间形状的硬约束,必须在采样阶段嵌入
第6章 轨迹规划与控制 → 6.1 路径可行性 基于未限位的伪工作空间做轨迹规划会导致路径点不可达
本篇代码解决 L2 离线仿真校核层。输入:关节限位
"[lower, upper]"、连杆DH参数、采样数N。处理:①策略A:在合法区间内直接均匀采样(零剔除);②策略B:全域
"[-π, π]"采样后拒绝超限样本(统计剔除率);③合法样本送正运动学;④聚合末端坐标生成工作空间点云。输出:合法工作空间3D点云 + 4张分析图表 + 分关节违规统计。
不覆盖:逆运动学求解、动力学、实时控制、碰撞检测。
3. 核心逻辑讲解
物理直觉
机械臂的工作空间由连杆长度和关节限位共同决定。蒙特卡洛法的本质是:在关节空间里随机取点,通过正运动学映射到末端位置。如果采样区间大于合法限位,超限样本算出来的点物理上不可达——就像用全中国的人口分布估算一个小区的人口。
两种正确采样策略:
- 策略A(区间内直接采样):对每个关节i从
"[lower_i, upper_i]"独立抽均匀随机数。所有样本天生合法,剔除率=0%,效率最高。适合纯工作空间可视化。
- 策略B(全域采样+拒绝):从
"[-π, π]"全域撒点,逐个检查限位,超限丢弃。能统计"如果不加限位会浪费多少样本",适合教学演示伪工作空间危害。
本篇两种都实现,对比展示。
J4对位置无贡献的物理原因
J4的DH参数:
"θ₄"自由变化,
"d₄=0.15",
"a₄=0",
"α₄=0"。变换矩阵为
"Rz(θ₄)·Tz(0.15)"。绕Z旋转不改变Z方向平移,且无X/Y偏移。因此J4只给末端附加一个绕Z的旋转(姿态变化),位置坐标
"[x, y, z]"完全不受θ₄影响。工作空间分析只需J1~J3。
业务逻辑 → 代码映射
输入:关节限位、DH参数、采样数N
↓
【策略A:区间内直接采样】
for each joint i: θ_i ~ Uniform(lower_i, upper_i)
所有样本天生合法 → 直接送正运动学
剔除率 = 0%
↓
【策略B:全域采样+拒绝】
for each sample: θ_i ~ Uniform(-π, π) for all i
check_limits(θ): 任一θ_i超出[lower_i, upper_i] → 丢弃
记录violated_indices统计各关节违规次数
合法样本送正运动学
↓
【正运动学】
标准DH连乘:T = ∏ Rz(θ_i)·Tz(d_i)·Tx(a_i)·Rx(α_i)
提取 p = T[:3,3]
↓
【输出】
策略A点云(主结果)+ 策略B点云(对比)+ 分关节违规统计
关键公式文字翻译
蒙特卡洛均匀采样(策略A):对每个关节i,
"θ_i = lower_i + rand() × (upper_i - lower_i)"。
"rand()"是
"[0,1)"均匀随机数。各关节独立。
拒绝采样(策略B):从提议分布(全域均匀)抽取候选样本,检查是否满足约束条件(全部关节在限位内)。满足则接受,否则拒绝并重抽。接受概率 = 各关节合法区间占比的乘积。
正运动学变换链:逐连杆执行
"Rz(θ)·Tz(d)·Tx(a)·Rx(α)"。本臂具体参数:连杆1只有d₁=0.45(基座高度);连杆2有a₂=0.55和α₂=-90°(大臂水平投影+垂直翻转);连杆3有a₃=0.45(小臂延伸);连杆4有d₄=0.15(末端沿自身Z偏移)。
4. OOP代码实现
技术栈
限定:numpy, matplotlib。不引入pybullet、roboticstoolbox等任何第三方重型库。全部自实现。
项目结构树
palletizing_workspace/
├── run_workspace_sampling.py
├── requirements.txt
├── robot/
│ ├── __init__.py
│ ├── models.py
│ ├── dh_kinematics.py
│ ├── workspace_sampler.py
│ └── visualize.py
├── tests/
│ ├── __init__.py
│ └── test_workspace.py
├── results/
└── output/
核心源码
"robot/models.py" — 数据模型
"""
数据模型:关节限位、DH参数、机器人配置
全部参数内置,自包含可复现
DH参数完整对应表(标准DH):
连杆1 (J1基座回转): θ₁变量, d₁=0.45m, a₁=0, α₁=0
连杆2 (J2大臂俯仰): θ₂变量, d₂=0, a₂=0.55m, α₂=-90°
连杆3 (J3小臂俯仰): θ₃变量, d₃=0, a₃=0.45m, α₃=0
连杆4 (J4末端翻转): θ₄变量, d₄=0.15m, a₄=0, α₄=0
"""
from dataclasses import dataclass, field
from typing import List, Tuple
import math
@dataclass
class JointLimits:
"""
单个关节的限位定义
lower: 下限位 (rad)
upper: 上限位 (rad)
name: 关节名称
"""
lower: float
upper: float
name: str = ""
def __post_init__(self):
if self.lower >= self.upper:
raise ValueError(
f"关节{self.name}下限位({self.lower})必须<上限位({self.upper})")
if not (math.isfinite(self.lower) and math.isfinite(self.upper)):
raise TypeError(f"关节{self.name}限位必须为有限数值")
@dataclass
class RobotConfig:
"""
机器人完整配置
"""
joint_limits: List[JointLimits] = field(default_factory=list)
dh_params: List[Tuple[float, float, float, float]] = field(
default_factory=list)
def __post_init__(self):
if not self.joint_limits:
# 4-DOF码垛臂限位(弧度制)
self.joint_limits = [
JointLimits(math.radians(-165), math.radians(165),
"J1_基座回转"),
JointLimits(math.radians(-20), math.radians(85),
"J2_大臂俯仰"),
JointLimits(math.radians(-90), math.radians(60),
"J3_小臂俯仰"),
JointLimits(math.radians(-180), math.radians(180),
"J4_末端翻转"),
]
if not self.dh_params:
# 标准DH参数:(θ_offset, d, a, α)
# 连杆1: 基座高0.45m
# 连杆2: 大臂长0.55m, 扭角-90°使Z轴转向垂直
# 连杆3: 小臂长0.45m
# 连杆4: 末端偏移0.15m
self.dh_params = [
(0.0, 0.45, 0.0, 0.0),
(0.0, 0.0, 0.55, -math.pi / 2),
(0.0, 0.0, 0.45, 0.0),
(0.0, 0.15, 0.0, 0.0),
]
"robot/dh_kinematics.py" — 正运动学
"""
标准DH正运动学:4-DOF码垛机械臂
变换顺序:Rz(θ) · Tz(d) · Tx(a) · Rx(α)
J4仅影响末端姿态,对位置无贡献
"""
import numpy as np
import math
from robot.models import RobotConfig
class DHKinematics:
def __init__(self, config: RobotConfig):
self.config = config
def _rot_z(self, theta):
"""绕Z轴旋转矩阵"""
c, s = math.cos(theta), math.sin(theta)
return np.array([
[c, -s, 0, 0],
[s, c, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
], dtype=float)
def _trans_z(self, d):
"""沿Z轴平移矩阵"""
M = np.eye(4)
M[2, 3] = d
return M
def _trans_x(self, a):
"""沿X轴平移矩阵"""
M = np.eye(4)
M[0, 3] = a
return M
def _rot_x(self, alpha):
"""绕X轴旋转矩阵"""
c, s = math.cos(alpha), math.sin(alpha)
return np.array([
[1, 0, 0, 0],
[0, c, -s, 0],
[0, s, c, 0],
[0, 0, 0, 1]
], dtype=float)
def forward_kinematics(self, joint_angles: list):
"""
正运动学:输入4个关节角(rad),输出末端4×4齐次变换矩阵
"""
if len(joint_angles) != 4:
raise ValueError("需要4个关节角")
T_total = np.eye(4)
for i, (theta_offset, d, a, alpha) in enumerate(
self.config.dh_params):
theta = joint_angles[i] + theta_offset
T_i = (
self._rot_z(theta)
@ self._trans_z(d)
@ self._trans_x(a)
@ self._rot_x(alpha)
)
T_total = T_total @ T_i
return T_total
def get_end_effector_position(self, joint_angles: list):
"""
返回末端执行器位置 [x, y, z] (m)
注意:J4(θ₄)对位置无贡献,仅影响姿态
"""
T = self.forward_kinematics(joint_angles)
return T[:3, 3].copy()
"robot/workspace_sampler.py" — 工作空间采样器
"""
工作空间蒙特卡洛采样器
策略A:在合法区间内直接均匀采样(零剔除)
策略B:全域[-π,π]采样+拒绝(统计剔除率+分关节违规)
使用np.random.default_rng替代全局seed
"""
import numpy as np
from robot.models import RobotConfig
from robot.dh_kinematics import DHKinematics
class WorkspaceSampler:
def __init__(self, config: RobotConfig,
kinematics: DHKinematics):
self.config = config
self.kinematics = kinematics
def check_limits(self, joint_angles: list):
"""
检查一组关节角是否全部在限位内
返回 (is_valid: bool, violated_indices: list)
"""
violated = []
for i, (angle, limit) in enumerate(
zip(joint_angles, self.config.joint_limits)):
if not (limit.lower <= angle <= limit.upper):
violated.append(i)
return len(violated) == 0, violated
def sample_within_limits(self, n_samples=5000, seed=42):
"""
策略A:在每个关节的合法区间内直接均匀采样
所有样本天生合法,剔除率=0%
使用独立Generator,不污染全局随机状态
"""
rng = np.random.default_rng(seed)
valid_angles = []
valid_positions = []
for _ in range(n_samples):
candidate = []
for limit in self.config.joint_limits:
angle = rng.uniform(limit.lower, limit.upper)
candidate.append(angle)
# 理论上全部合法,仍做断言检查
is_valid, violated = self.check_limits(candidate)
if not is_valid:
raise RuntimeError(
f"策略A生成了超限样本!关节{violated}")
pos = self.kinematics.get_end_effector_position(candidate)
valid_angles.append(candidate)
valid_positions.append(pos)
return {
'valid_angles': np.array(valid_angles),
'valid_positions': np.array(valid_positions),
'rejected_count': 0,
'total_attempts': n_samples,
'rejection_rate': 0.0,
'n_valid': n_samples,
'per_joint_violations': np.zeros(4, dtype=int),
}
def sample_with_rejection(self, n_samples=5000,
max_attempts=None, seed=42):
"""
策略B:全域[-π,π]采样+拒绝超限样本
统计剔除率和各关节违规次数
"""
if max_attempts is None:
max_attempts = n_samples * 100
rng = np.random.default_rng(seed)
valid_angles = []
valid_positions = []
rejected_count = 0
total_attempts = 0
per_joint_violations = np.zeros(4, dtype=int)
while (len(valid_angles) < n_samples and
total_attempts < max_attempts):
total_attempts += 1
candidate = []
for _ in range(4):
candidate.append(rng.uniform(-np.pi, np.pi))
is_valid, violated = self.check_limits(candidate)
if not is_valid:
rejected_count += 1
for vi in violated:
per_joint_violations[vi] += 1
continue
pos = self.kinematics.get_end_effector_position(candidate)
valid_angles.append(candidate)
valid_positions.append(pos)
rejection_rate = rejected_count / total_attempts if total_attempts > 0 else 0
return {
'valid_angles': np.array(valid_angles),
'valid_positions': np.array(valid_positions),
'rejected_count': rejected_count,
'total_attempts': total_attempts,
'rejection_rate': rejection_rate,
'n_valid': len(valid_angles),
'per_joint_violations': per_joint_violations,
}
"robot/visualize.py" — 可视化
"""
工作空间采样可视化:4张图表
① 策略A合法工作空间3D点云
② XY平面投影对比(策略A vs 策略B)
③ XZ平面投影对比
④ 分关节违规次数条形图(支撑"J2贡献最大"结论)
"""
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans',
'Noto Sans CJK SC']
plt.rcParams['axes.unicode_minus'] = False
class WorkspaceVisualizer:
def __init__(self, results_dir="results"):
self.results_dir = results_dir
import os
os.makedirs(results_dir, exist_ok=True)
def plot_3d_workspace(self, positions):
"""① 合法工作空间3D点云"""
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
ax.scatter(positions[:, 0], positions[:, 1], positions[:, 2],
c=positions[:, 2], cmap='viridis', s=3, alpha=0.6)
ax.set_xlabel('X (m)')
ax.set_ylabel('Y (m)')
ax.set_zlabel('Z (m)')
ax.set_title('策略A:限位区间内直接采样工作空间3D点云')
plt.tight_layout()
path = f"{self.results_dir}/01_3d_workspace.png"
plt.savefig(path, dpi=150)
plt.close()
return path
def plot_xy_projection_comparison(self, pos_a, pos_b):
"""② XY平面投影对比"""
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
axes[0].scatter(pos_a[:, 0], pos_a[:, 1],
s=2, c='blue', alpha=0.5)
axes[0].set_xlabel('X (m)')
axes[0].set_ylabel('Y (m)')
axes[0].set_title('策略A(区间内采样)XY投影')
axes[0].axis('equal')
axes[0].grid(True, alpha=0.3)
axes[1].scatter(pos_b[:, 0], pos_b[:, 1],
s=2, c='red', alpha=0.3)
axes[1].set_xlabel('X (m)')
axes[1].set_ylabel('Y (m)')
axes[1].set_title('策略B(全域+拒绝)XY投影')
axes[1].axis('equal')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
path = f"{self.results_dir}/02_xy_projection_compare.png"
plt.savefig(path, dpi=150)
plt.close()
return path
def plot_xz_projection_comparison(self, pos_a, pos_b):
"""③ XZ平面投影对比"""
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
axes[0].scatter(pos_a[:, 0], pos_a[:, 2],
s=2, c='blue', alpha=0.5)
axes[0].set_xlabel('X (m)')
axes[0].set_ylabel('Z (m)')
axes[0].set_title('策略A(区间内采样)XZ投影')
axes[0].grid(True, alpha=0.3)
axes[1].scatter(pos_b[:, 0], pos_b[:, 2],
s=2, c='red', alpha=0.3)
axes[1].set_xlabel('X (m)')
axes[1].set_ylabel('Z (m)')
axes[1].set_title('策略B(全域+拒绝)XZ投影')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
path = f"{self.results_dir}/03_xz_projection_compare.png"
plt.savefig(path, dpi=150)
plt.close()
return path
def plot_per_joint_violations(self, per_joint_violations,
joint_names):
"""④ 分关节违规次数条形图"""
fig, ax = plt.subplots(figsize=(8, 5))
indices = np.arange(len(joint_names))
bars = ax.bar(indices, per_joint_violations,
color=['steelblue', 'red', 'orange', 'green'],
alpha=0.8)
# 在柱顶标注数值
for bar, val in zip(bars, per_joint_violations):
if val > 0:
ax.text(bar.get_x() + bar.get_width() / 2,
bar.get_height() + max(per_joint_violations) * 0.02,
str(val), ha='center', va='bottom', fontsize=9)
ax.set_xticks(indices)
ax.set_xticklabels(joint_names)
ax.set_ylabel('违规次数')
ax.set_title('策略B中各关节导致样本被拒的次数')
ax.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
path = f"{self.results_dir}/04_per_joint_violations.png"
plt.savefig(path, dpi=150)
plt.close()
return path
5. 可视化要求
序号 图表文件 服务结论
①
"01_3d_workspace.png" 策略A合法工作空间3D点云,可读出实际可达区域形状
②
"02_xy_projection_compare.png" XY投影对比,可读出两种策略生成的点云一致
③
"03_xz_projection_compare.png" XZ投影对比,可读出Z方向受J2/J3限位约束的形状
④
"04_per_joint_violations.png" 分关节违规次数,可读出J2(限位最窄)违规次数最多
6. README使用说明 + 核心知识点卡片
快速开始
pip install -r requirements.txt
python -m pytest tests/test_workspace.py -v # 3/3 测试通过
python run_workspace_sampling.py # 运行工作空间采样
数据格式说明
# 修改关节限位:编辑 RobotConfig.joint_limits
# 修改连杆参数:编辑 RobotConfig.dh_params
# 修改采样数:编辑 run_workspace_sampling.py 中的 n_samples
参数 修改位置 说明
关节限位
"JointLimits(lower, upper)" 弧度制
连杆长度
"dh_params" 中
"a"/
"d" 值 米
采样数量
"n_samples=" 整数
随机种子
"seed=" 整数
📌 知识点卡片
工作空间与关节限位(对应课本第2章2.3节、第3章3.4节)
- 蒙特卡洛法两种策略:策略A在合法区间内直接采样(零剔除,效率高);策略B全域撒点+拒绝(可统计浪费)
- J4不影响位置:末端翻转轴与J3共线,工作空间分析降维至3-DOF
- 分关节违规统计:限位最窄的关节贡献最多拒绝次数
- 工程铁律:工作空间采样=先筛后算;J4姿态自由度不计入位置工作空间
7. 总结与工程师思考
工业落地难处
一、限位参数标称偏差。 本篇使用标称限位值,但实际机械挡块位置可能有±1°~2°偏差。基于不精确限位生成的工作空间仍可能包含不可达点。
二、关节耦合干涉。 本篇假设各关节独立限位。实际码垛臂在J2和J3同时到达极端角度时可能发生机械干涉,单纯逐关节检查无法捕获。
三、动力学约束未纳入。 本篇纯运动学分析。重载时关节扭矩接近额定值(如J2额定450 N·m),即使角度在限位内也可能因扭矩超限而不可达。完整工作空间需叠加动力学约束。
工程师心得
阿芳看着
"04_per_joint_violations.png"上J2那根高耸的红色柱子:"全域采样时J2违规了三万多
8. 写作与风格限制
本篇全部代码为课程离线仿真,运行在普通PC上,用于工作空间分析验证。代码中不包含任何真实机器人控制器实时运算或安全逻辑。 完全去营销,中立客观。拒绝论文式生硬开头,保持故事叙事感。全部物理量标注完整单位。
9. 文末附录
环境配置
pip install numpy>=1.24.0 matplotlib>=3.7.0
"requirements.txt":
numpy>=1.24.0
matplotlib>=3.7.0
运行说明
cd palletizing_workspace
pip install -r requirements.txt
python -m pytest tests/ -v
python run_workspace_sampling.py
参数修改指南
想改什么 改哪里 示例
关节限位
"JointLimits(lower, upper)" -20°→-30°
连杆长度
"dh_params" 中a/d值 0.55→0.60
采样数量
"n_samples=" 5000→10000
随机种子
"seed=" 42→123
输出文件清单
文件 含义
"results/01_3d_workspace.png" 策略A合法工作空间3D点云
"results/02_xy_projection_compare.png" XY投影对比(策略A vs B)
"results/03_xz_projection_compare.png" XZ投影对比(策略A vs B)
"results/04_per_joint_violations.png" 分关节违规次数统计
"output/workspace_points.csv" 合法末端位置数据
仿真结果摘要
统计项 数值 数据来源
策略A合法样本数 5000 区间内直接采样
策略A剔除率 0% 设计使然
策略B目标合法样本 5000 全域+拒绝
策略B总尝试次数 ~46000 运行统计
策略B剔除率 ~89.1% 1 - 0.917×0.292×0.417×1.0
J2违规占比 ~78% 分关节统计
测试通过 3/3 pytest
正文完。以下为
"run_workspace_sampling.py" 完整主入口 +
"tests/test_workspace.py" 完整测试文件。
"run_workspace_sampling.py"
"""
码垛仿真:关节限位约束下的工作空间采样 主入口
运行:python run_workspace_sampling.py
"""
import numpy as np
import os
import csv
import math
from robot.models import RobotConfig
from robot.dh_kinematics import DHKinematics
from robot.workspace_sampler import WorkspaceSampler
from robot.visualize import WorkspaceVisualizer
def main():
print("=" * 60)
print("码垛仿真:关节限位约束下的工作空间采样")
print("=" * 60)
# 1. 初始化
config = RobotConfig()
kinematics = DHKinematics(config)
sampler = WorkspaceSampler(config, kinematics)
viz = WorkspaceVisualizer(results_dir="results")
n_samples = 5000
seed = 42
# 2. 策略A:区间内直接采样
print(f"\n--- 策略A:合法区间内直接采样({n_samples}样本) ---")
result_a = sampler.sample_within_limits(
n_samples=n_samples, seed=seed)
print(f" 合法样本数: {result_a['n_valid']}")
print(f" 剔除率: {result_a['rejection_rate'] * 100:.1f}% "
f"(设计使然,区间内采样天生合法)")
pos_a = result_a['valid_positions']
print(f" 工作空间范围:")
print(f" X: [{pos_a[:, 0].min():.3f}, "
f"{pos_a[:, 0].max():.3f}] m")
print(f" Y: [{pos_a[:, 1].min():.3f}, "
f"{pos_a[:, 1].max():.3f}] m")
print(f" Z: [{pos_a[:, 2].min():.3f}, "
f"{pos_a[:, 2].max():.3f}] m")
# 3. 策略B:全域采样+拒绝
print(f"\n--- 策略B:全域[-π,π]采样+拒绝(目标{n_samples}合法样本) ---")
result_b = sampler.sample_with_rejection(
n_samples=n_samples, seed=seed)
print(f" 合法样本数: {result_b['n_valid']}")
print(f" 总尝试次数: {result_b['total_attempts']}")
print(f" 剔除样本数: {result_b['rejected_count']}")
print(f" 剔除率: {result_b['rejection_rate'] * 100:.1f}%")
print(f" 分关节违规次数:")
for i, lim in enumerate(config.joint_limits):
print(f" {lim.name}: "
f"{result_b['per_joint_violations'][i]}")
pos_b = result_b['valid_positions']
# 4. 可视化
print("\n--- 生成可视化图表 ---")
viz.plot_3d_workspace(pos_a)
viz.plot_xy_projection_comparison(pos_a, pos_b)
viz.plot_xz_projection_comparison(pos_a, pos_b)
joint_names = [lim.name for lim in config.joint_limits]
viz.plot_per_joint_violations(
result_b['per_joint_violations'], joint_names)
print(" 完成! 图表已保存至 results/ 目录")
# 5. 保存CSV
os.makedirs("output", exist_ok=True)
csv_path = "output/workspace_points.csv"
with open(csv_path, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['x_m', 'y_m', 'z_m'])
for i in range(min(5000, len(pos_a))):
writer.writerow([
f"{pos_a[i, 0]:.6f}",
f"{pos_a[i, 1]:.6f}",
f"{pos_a[i, 2]:.6f}"
])
print(f" 数据已保存: {csv_path}")
print("=" * 60)
print("⚠️ 本代码为课程离线仿真,不可直接用于产线设备")
print("=" * 60)
if __name__ == "__main__":
main()
"tests/test_workspace.py"
"""
工作空间采样单元测试
运行:python -m pytest tests/test_workspace.py -v
"""
import numpy as np
import math
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from robot.models import RobotConfig, JointLimits
from robot.dh_kinematics import DHKinematics
from robot.workspace_sampler import WorkspaceSample
利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!