news 2026/10/23 23:40:26

python的机器人基础原理工业场景模拟第八十九篇:码垛仿真,限定机械臂关节角度上下限,在采样生成工作空间时直接剔除超出限位的关节样本。

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
python的机器人基础原理工业场景模拟第八十九篇:码垛仿真,限定机械臂关节角度上下限,在采样生成工作空间时直接剔除超出限位的关节样本。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 23:28:51

SpringBoot整合Shiro+Redis实现用户认证授权与分布式会话方案

SpringBootShiroRedis 整合实现用户认证授权&#xff0c;这套组合在中小型项目里实在太常见了。我自己接过好几个这类需求&#xff0c;真要说起来&#xff0c;踩过的坑不比写过的代码少。如果你正准备给项目加一套登录鉴权&#xff0c;又不想直接上Spring Security那一大坨配置…

作者头像 李华
网站建设 2026/10/23 23:26:53

Java反射实现单据号到ID的动态映射机制

1. 项目概述&#xff1a;为什么单据号和ID之间需要“反射式”映射在实际业务系统开发中&#xff0c;我几乎每天都会遇到这样一个看似简单却暗藏玄机的问题&#xff1a;用户在界面上输入一个单据号&#xff08;比如“PO20240517001”、“INV-2024-Q3-8821”&#xff09;&#xf…

作者头像 李华
网站建设 2026/10/23 23:07:57

连接跟踪与流量统计:从conntrack到iptables的运维实战

连接跟踪流量统计&#xff0c;这是Linux网络运维里一个特别容易被忽视&#xff0c;但关键时刻能救命的角落。很多人搞iptables、搞防火墙规则精通得很&#xff0c;一说到“你线上每秒多少新建连接&#xff1f;连接表占用多少&#xff1f;到底哪个IP在持续往外发包&#xff1f;”…

作者头像 李华
网站建设 2026/10/23 23:04:50

MATLAB多目标跟踪工程实践:检测框、ID与轨迹对齐方法

简介&#xff1a;本资源是一份面向计算机视觉初学者与MATLAB实践者的多目标跟踪入门级教学包&#xff0c;聚焦视频监控、智能交通等典型场景下的目标检测、状态估计与轨迹关联核心问题。压缩包共3个文件&#xff08;1个txt说明文档、1个avi测试视频、1个m主运行脚本&#xff09…

作者头像 李华
网站建设 2026/10/23 23:04:08

把长文档塞进AI大脑,到底该用多高的分辨率?

你有没有用手机扫描过一份几十页的合同&#xff0c;然后想让AI帮你读一遍找出某个关键条款&#xff1f;这时候你会遇到一个很尴尬的选择&#xff1a;扫描分辨率调低一点&#xff0c;文件小、上传快&#xff0c;但AI可能认错字&#xff0c;把"不得转让"看成"不得…

作者头像 李华