news 2026/10/14 15:44:22

python的机器人基础原理工业场景模拟第二十一篇:点位搬运任务,采用三次多项式完成关节空间插值,生成关节角度随时间变化序列。

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
python的机器人基础原理工业场景模拟第二十一篇:点位搬运任务,采用三次多项式完成关节空间插值,生成关节角度随时间变化序列。

《Python机器人基础原理》第二十一篇:机械臂仿真——点位搬运任务,采用三次多项式完成关节空间插值,生成关节角度随时间变化序列

1. 实际工业应用场景

周五上午,某3C电子装配车间的上下料工位。一台3-DOF垂直关节机械臂(基座偏航+肘部俯仰+末端旋转)正在执行PCB板从进料皮带到测试工位的搬运任务。机器人构型:大臂长600 mm,小臂长450 mm,末端旋转轴位于小臂末端。连杆质量:大臂5.2 kg(质心距基座0.28 m),小臂3.1 kg(质心距肘部0.20 m),末端夹具1.5 kg(质心距肘部0.12 m,质心到旋转轴线横向偏置0.02 m)。关节额定扭矩:基座偏航轴60 N·m,肘部俯仰轴180 N·m,关节最大角速度限制:基座偏航2.5 rad/s、肘部俯仰2.0 rad/s、末端旋转3.0 rad/s;最大角加速度限制:基座偏航5.0 rad/s²、肘部俯仰4.0 rad/s²、末端旋转6.0 rad/s²。

调试工程师小李把示教器递给旁边的新人小赵:"你试试让机器人从进料位走到测试位。起点:θ₁=0°、θ₂=-30°、θ₃=0°;终点:θ₁=60°、θ₂=-80°、θ₃=90°。走关节插值模式。"

小赵在示教器上设了起点和终点,选了"线性插值",点下执行。机器人动起来了——但走到一半突然抖了一下,末端夹具上的PCB板差点飞出去。

小李一把按住急停:"停。你用的线性插值,角度变化率是常数,意味着每个关节在起终点瞬间加速度无穷大——速度从零瞬间跳到某个值再瞬间归零,电机得承受理论上的冲击脉冲。那一下抖动就是冲击导致的。"

小赵愣了一下:"那应该用什么?"

"三次多项式。给定起点和终点的角度、角速度(通常设为零),多项式自动保证位置和速度连续——也就是C¹连续。但注意,三次多项式的加速度在起终点是不连续的:从静止瞬间跳到一个非零值,加加速度(jerk)在起点是无穷大。这比线性插值好得多——至少速度不再有阶跃——但如果你看加速度曲线,起点有个跳变。这恰好说明了三次的局限:它只能保证C¹,不能保证C²。下一篇我们上五次多项式,把起终点加速度也约束为零。"

小赵在草稿纸上列式子:"三次多项式θ(t) = a₀ + a₁t + a₂t² + a₃t³,四个约束解出四个系数。起终点速度为零的话,a₁=0,a₂=3Δq/T²,a₃=-2Δq/T³。速度峰值是1.5|Δq|/T,出现在t=T/2。"

"对。咱们取T=2.0 s。Joint2的Δq = -50° = -0.873 rad,峰值速度=1.5×0.873/2.0 ≈ 0.65 rad/s,峰值加速度=6×0.873/4.0 ≈ 1.31 rad/s²。都在限位以内——速度限2.0,加速度限4.0,余量很足。"

"好。那我写个离线仿真,T=2.0 s,用三次多项式生成θ₁、θ₂、θ₃各自的时间序列,画四张图验证。"

本篇解决的问题:给定一组关节空间点位(起点和终点的角度),采用三次多项式(Cubic Polynomial)完成关节空间插值,生成关节角度、角速度、角加速度随时间变化的连续序列。用于点位搬运任务的离线轨迹验证,确保位置和速度连续(C¹),校验是否超出物理限位。

⚠️ 声明:本篇全部代码为课程离线仿真,运行在普通PC上,用于点位搬运任务的关节空间轨迹生成与可视化。不可直接用于真实机器人控制器、伺服驱动器或装配产线设备。真实工业现场需结合运动控制卡、实时插补、软硬限位保护。代码作者不对直接使用本代码于真实设备造成的任何后果负责。

2. 痛点分析

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

第6章 轨迹规划与控制 → 6.1 关节空间轨迹规划 点位搬运需要位置和速度连续,线性插值导致速度阶跃→理论加速度无穷大→机械冲击

第6章 轨迹规划与控制 → 6.2 多项式插值 三次多项式提供C¹连续(位置、速度),满足点位运动基本平滑性需求;加速度在起终点有阶跃(C¹但不C²)

第5章 机器人关节驱动与控制 → 5.1 伺服驱动基础 电机有最大转速和最大加速度限制,轨迹需离线校核是否超限

本篇代码解决 L2 离线轨迹规划层。输入:起点/终点关节角度、运动时间T=2.0 s、采样点数。输出:各关节的θ(t)、θ̇(t)、θ̈(t)时间序列。代码不是设备实时控制器,不做伺服闭环。它做的事情是:给定起点终点,用三次多项式生成C¹连续轨迹,校验连续性和物理可行性。

不覆盖:笛卡尔空间直线/圆弧插补、五次多项式/梯形速度规划、B样条、实时插补、多轴联动、6轴完整轨迹。五次多项式(C²连续)留待下一篇。

3. 核心逻辑讲解

物理直觉

你开车从A点停到B点。线性插值就像一脚油门踩到底然后急刹——速度瞬间从零跳到满速再瞬间归零,车会猛冲再猛停。三次多项式的做法是:缓缓加速到某个速度再缓缓减速到零,速度曲线是平滑的S形。但注意——加速度从起步瞬间就不是零(你一踩油门就有加速度),在停车瞬间也跳回零。所以三次多项式保证速度不断裂(C¹),但加速度在起终点有跳变(不是C²)。想要加速度也连续,得上五次多项式。

业务逻辑 → 代码映射

输入:起点角度q_start、终点角度q_end、运动时间T=2.0 s、采样点数N=100

↓

步骤1:对每个关节,建立三次多项式 θ(t) = a₀ + a₁·t + a₂·t² + a₃·t³

↓

步骤2:代入约束条件解出系数(线性方程组通解,支持非零起终点速度):

- θ(0) = q_start → a₀ = q_start

- θ(T) = q_end → a₀ + a₁·T + a₂·T² + a₃·T³ = q_end

- θ'(0) = v_start → a₁ = v_start

- θ'(T) = v_end → a₁ + 2·a₂·T + 3·a₃·T² = v_end

↓

步骤3(起终点速度为零的特例):

a₀ = q_start, a₁ = 0, a₂ = 3Δq/T², a₃ = -2Δq/T³

↓

步骤4:在N个采样点上计算 θ(t), θ'(t) = 2a₂t + 3a₃t², θ''(t) = 2a₂ + 6a₃t

↓

步骤5:解析计算峰值(非采样近似):

速度峰值 = 1.5|Δq|/T(t=T/2)

加速度极值 = max(|2a₂|, |2a₂+6a₃T|)(边界值)

↓

步骤6:校验位置/速度/加速度三重限位,输出4张图表

关键公式文字翻译

三次多项式:θ(t) = a₀ + a₁·t + a₂·t² + a₃·t³。a₀是起点角度,a₁是起点速度,a₂和a₃由终点约束决定。

系数求解(通解):建立线性方程组求解a₂、a₃,支持任意起终点速度。本篇特例(起终点速度为零):a₀=q_start,a₁=0,a₂=3Δq/T²,a₃=-2Δq/T³。

速度:θ̇(t) = 2a₂t + 3a₃t²。起终点为零,中间在t=T/2达到峰值1.5|Δq|/T。

加速度:θ̈(t) = 2a₂ + 6a₃t。从2a₂线性变化到2a₂+6a₃T。在t=0处加速度跳变到2a₂(非零),在t=T处跳变回零——这就是C¹但不C²的数学表现。

单位:角度 rad,角速度 rad/s,角加速度 rad/s²,时间 s。

4. OOP代码实现

技术栈

严格限定:numpy, scipy, matplotlib。不引入pybullet、roboticstoolbox等任何第三方机器人库。

项目结构树

point_to_point_sim/

├── run_point_to_point_sim.py

├── requirements.txt

├── trajectory/

│ ├── __init__.py

│ ├── models.py

│ ├── cubic_interpolator.py

│ └── visualize.py

├── tests/

│ ├── __init__.py

│ └── test_point_to_point_sim.py

└── results/

核心源码

"trajectory/models.py" — dataclass数据模型

"""

3-DOF垂直关节机械臂数据模型

点位搬运仿真:三次多项式关节空间插值

构型说明:基座偏航(θ1, 竖直轴) + 肘部俯仰(θ2, 水平轴) + 末端旋转(θ3)

物理声明:

- 本篇仅做轨迹规划,不涉及动力学计算

- 关节限位用于校验插值结果是否越界

- 速度/加速度限制用于校验轨迹可行性

"""

from dataclasses import dataclass, field

from typing import List, Tuple, Optional

import numpy as np

@dataclass

class JointWaypoint:

"""

单个关节的单个路点

angle: 关节角度 (rad)

velocity: 关节角速度 (rad/s),设为零表示停靠

"""

angle: float

velocity: float = 0.0

joint_name: str = ""

def __post_init__(self):

if not np.isfinite(self.angle):

raise ValueError(f"JointWaypoint: 角度必须为有限数值")

if not np.isfinite(self.velocity):

raise ValueError(f"JointWaypoint: 速度必须为有限数值")

@dataclass

class JointLimit:

"""单关节硬件限位"""

min_val: float

max_val: float

joint_name: str = ""

max_velocity: float = 0.0 # 最大角速度 (rad/s)

max_acceleration: float = 0.0 # 最大角加速度 (rad/s²)

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:

return (self.min_val - 1e-10) <= value <= (self.max_val + 1e-10)

def velocity_ok(self, vel: float) -> bool:

if self.max_velocity <= 0:

return True

return abs(vel) <= self.max_velocity + 1e-10

def acceleration_ok(self, accel: float) -> bool:

if self.max_acceleration <= 0:

return True

return abs(accel) <= self.max_acceleration + 1e-10

@dataclass

class JointLimitSet:

"""3-DOF机械臂关节限位集合"""

limits: List[JointLimit]

def __post_init__(self):

if len(self.limits) != 3:

raise ValueError(f"需要3个关节限位, 实际{len(self.limits)}个")

@classmethod

def default_3dof_arm(cls):

"""

默认3-DOF机械臂限位

θ1: ±170°, 最大速度2.5 rad/s, 最大加速度5.0 rad/s²

θ2: ±140°, 最大速度2.0 rad/s, 最大加速度4.0 rad/s²

θ3: ±180°, 最大速度3.0 rad/s, 最大加速度6.0 rad/s²

"""

pi = 3.141592653589793

return cls(limits=[

JointLimit(-170 * pi / 180, 170 * pi / 180, 'joint1_base_yaw',

2.5, 5.0),

JointLimit(-140 * pi / 180, 140 * pi / 180, 'joint2_elbow_pitch',

2.0, 4.0),

JointLimit(-180 * pi / 180, 180 * pi / 180, 'joint3_end_rotate',

3.0, 6.0),

])

def check_all_positions(self, joint_values: List[float]) -> Tuple[bool, List[int]]:

if len(joint_values) != 3:

raise ValueError(f"需要3个关节值, 实际{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

def check_all_velocities(self, velocities: List[float]) -> Tuple[bool, List[int]]:

if len(velocities) != 3:

raise ValueError(f"需要3个速度值, 实际{len(velocities)}个")

all_pass = True

violation_indices = []

for i, (vel, limit) in enumerate(zip(velocities, self.limits)):

if not limit.velocity_ok(vel):

all_pass = False

violation_indices.append(i)

return all_pass, violation_indices

def check_all_accelerations(self, accelerations: List[float]) -> Tuple[bool, List[int]]:

if len(accelerations) != 3:

raise ValueError(f"需要3个加速度值, 实际{len(accelerations)}个")

all_pass = True

violation_indices = []

for i, (accel, limit) in enumerate(zip(accelerations, self.limits)):

if not limit.acceleration_ok(accel):

all_pass = False

violation_indices.append(i)

return all_pass, violation_indices

@dataclass

class RobotParams:

"""

3-DOF机械臂参数(本篇仅用于轨迹规划,不涉及动力学)

"""

joint_names: List[str] = field(default_factory=lambda: [

'joint1_base_yaw', 'joint2_elbow_pitch', 'joint3_end_rotate'

])

link_lengths: List[float] = field(default_factory=lambda: [0.600, 0.450, 0.0])

@classmethod

def default_3dof_arm(cls):

return cls()

"trajectory/cubic_interpolator.py" — 三次多项式插值器

"""

三次多项式关节空间轨迹插值器

θ(t) = a₀ + a₁·t + a₂·t² + a₃·t³

约束:θ(0)=q_start, θ(T)=q_end, θ'(0)=v_start, θ'(T)=v_end

连续性说明:三次多项式保证C¹连续(位置和速度连续),

但加速度在段边界有阶跃(不是C²)。起终点加速度非零跳变。

"""

import numpy as np

from trajectory.models import JointWaypoint, JointLimitSet

class CubicPolynomialSegment:

"""

单个关节的单个三次多项式段

系数: θ(t) = a₀ + a₁·t + a₂·t² + a₃·t³

"""

def __init__(self, q_start: float, q_end: float, duration: float,

qd_start: float = 0.0, qd_end: float = 0.0):

if duration <= 0:

raise ValueError(f"运动时间必须大于零, 实际{duration}")

if not np.isfinite(q_start) or not np.isfinite(q_end):

raise ValueError("起点/终点角度必须为有限数值")

if not np.isfinite(qd_start) or not np.isfinite(qd_end):

raise ValueError("起点/终点速度必须为有限数值")

self.q_start = q_start

self.q_end = q_end

self.duration = duration

self.qd_start = qd_start

self.qd_end = qd_end

# 求解系数(线性方程组通解,支持非零起终点速度)

# θ(0) = a₀ = q_start

# θ'(0) = a₁ = qd_start

# θ(T) = a₀ + a₁·T + a₂·T² + a₃·T³ = q_end

# θ'(T) = a₁ + 2·a₂·T + 3·a₃·T² = qd_end

self.a0 = q_start

self.a1 = qd_start

T = duration

mat = np.array([[T**2, T**3],

[2*T, 3*T**2]], dtype=float)

rhs = np.array([q_end - self.a0 - self.a1 * T,

qd_end - self.a1], dtype=float)

try:

coeffs = np.linalg.solve(mat, rhs)

except np.linalg.LinAlgError:

raise ValueError(f"三次多项式系数矩阵奇异, duration={duration}")

self.a2 = coeffs[0]

self.a3 = coeffs[1]

def evaluate(self, t: float) -> Tuple[float, float, float]:

"""计算 t 时刻的角度、角速度、角加速度"""

if t < -1e-12 or t > self.duration + 1e-12:

raise ValueError(f"时间t={t}超出范围[0, {self.duration}]")

t = max(0.0, min(t, self.duration))

theta = self.a0 + self.a1 * t + self.a2 * t**2 + self.a3 * t**3

theta_dot = self.a1 + 2 * self.a2 * t + 3 * self.a3 * t**2

theta_ddot = 2 * self.a2 + 6 * self.a3 * t

return theta, theta_dot, theta_ddot

def max_velocity(self) -> float:

"""

计算该段的最大绝对速度(解析解)

速度函数是二次函数 θ'(t) = a₁ + 2a₂t + 3a₃t²

极值点出现在 t = -a₂/(3a₃)(如果在[0,T]内)或边界

特例(零起终点速度):峰值 = 1.5|Δq|/T,出现在 t=T/2

"""

candidates = [0.0, self.duration]

if abs(self.a3) > 1e-12:

t_extreme = -self.a2 / (3 * self.a3)

if 0 < t_extreme < self.duration:

candidates.append(t_extreme)

max_v = 0.0

for t in candidates:

_, v, _ = self.evaluate(t)

max_v = max(max_v, abs(v))

return max_v

def max_acceleration(self) -> float:

"""

计算该段的最大绝对加速度(解析解)

加速度是线性的 θ''(t) = 2a₂ + 6a₃t,极值在边界

"""

_, _, a_start = self.evaluate(0.0)

_, _, a_end = self.evaluate(self.duration)

return max(abs(a_start), abs(a_end))

def peak_velocity_formula(self) -> float:

"""

零起终点速度时的解析峰值速度:1.5|Δq|/T

用于校验max_velocity()计算结果

"""

if abs(self.qd_start) < 1e-12 and abs(self.qd_end) < 1e-12:

delta_q = self.q_end - self.q_start

return 1.5 * abs(delta_q) / self.duration

return self.max_velocity()

class CubicJointTrajectory:

"""

多关节三次多项式轨迹生成器

对每个关节独立生成三次多项式插值序列

构造时校验起终点路点是否在位置限位内(尽早失败)

"""

def __init__(self, start_waypoints: List[JointWaypoint],

end_waypoints: List[JointWaypoint],

duration: float, n_points: int = 100,

limits: JointLimitSet = None):

if len(start_waypoints) != 3 or len(end_waypoints) != 3:

raise ValueError("需要3个关节的起点和终点路点")

self.duration = duration

self.n_points = n_points

self.times = np.linspace(0, duration, n_points)

self.limits = limits

# 构造时尽早校验起终点位置限位

if limits is not None:

start_angles = [wp.angle for wp in start_waypoints]

end_angles = [wp.angle for wp in end_waypoints]

start_ok, start_violations = limits.check_all_positions(start_angles)

if not start_ok:

err_msgs = []

for vi in start_violations:

err_msgs.append(

f"起点关节'{limits.limits[vi].joint_name}' "

f"角度={start_angles[vi]:.3f}超出限位"

f"[{limits.limits[vi].min_val:.3f}, "

f"{limits.limits[vi].max_val:.3f}]"

)

raise ValueError(";".join(err_msgs))

end_ok, end_violations = limits.check_all_positions(end_angles)

if not end_ok:

err_msgs = []

for vi in end_violations:

err_msgs.append(

f"终点关节'{limits.limits[vi].joint_name}' "

f"角度={end_angles[vi]:.3f}超出限位"

f"[{limits.limits[vi].min_val:.3f}, "

f"{limits.limits[vi].max_val:.3f}]"

)

raise ValueError(";".join(err_msgs))

self.segments = []

for i in range(3):

seg = CubicPolynomialSegment(

q_start=start_waypoints[i].angle,

q_end=end_waypoints[i].angle,

duration=duration,

qd_start=start_waypoints[i].velocity,

qd_end=end_waypoints[i].velocity

)

self.segments.append(seg)

# 批量计算

self.q_array = np.zeros((n_points, 3))

self.q_dot_array = np.zeros((n_points, 3))

self.q_ddot_array = np.zeros((n_points, 3))

for i, t in enumerate(self.times):

for j in range(3):

q, qd, qdd = self.segments[j].evaluate(t)

self.q_array[i, j] = q

self.q_dot_array[i, j] = qd

self.q_ddot_array[i, j] = qdd

def validate_limits(self, limits: JointLimitSet) -> Tuple[bool, List[str]]:

"""

校验整条轨迹是否满足限位、速度限、加速度限

返回 (all_pass, messages)

"""

all_pass = True

messages = []

for i in range(self.n_points):

t = self.times[i]

q_vals = self.q_array[i, :].tolist()

qd_vals = self.q_dot_array[i, :].tolist()

qdd_vals = self.q_ddot_array[i, :].tolist()

# 位置校验

pos_ok, pos_violations = limits.check_all_positions(q_vals)

if not pos_ok:

all_pass = False

for vi in pos_violations:

messages.append(

f"t={t:.3f}s: 关节'{limits.limits[vi].joint_name}' "

f"角度={q_vals[vi]:.3f}超出限位"

f"[{limits.limits[vi].min_val:.3f}, "

f"{limits.limits[vi].max_val:.3f}]"

)

# 速度校验

vel_ok, vel_violations = limits.check_all_velocities(qd_vals)

if not vel_ok:

all_pass = False

for vi in vel_violations:

messages.append(

f"t={t:.3f}s: 关节'{limits.limits[vi].joint_name}' "

f"角速度={qd_vals[vi]:.3f}超出限制"

f"{limits.limits[vi].max_velocity:.3f}"

)

# 加速度校验

acc_ok, acc_violations = limits.check_all_accelerations(qdd_vals)

if not acc_ok:

all_pass = False

for vi in acc_violations:

messages.append(

f"t={t:.3f}s: 关节'{limits.limits[vi].joint_name}' "

f"角加速度={qdd_vals[vi]:.3f}超出限制"

f"{limits.limits[vi].max_acceleration:.3f}"

)

return all_pass, messages

def get_peak_values(self) -> dict:

"""返回各关节的峰值角度、速度、加速度"""

return {

'q_peak': np.max(np.abs(self.q_array), axis=0),

'qd_peak': np.max(np.abs(self.q_dot_array), axis=0),

'qdd_peak': np.max(np.abs(self.q_ddot_array), axis=0),

}

@classmethod

def from_angle_pairs(cls, q_start: List[float], q_end: List[float],

duration: float, n_points: int = 100,

limits: JointLimitSet = None):

"""

便捷工厂方法:直接从角度列表创建轨迹

"""

if len(q_start) != 3 or len(q_end) != 3:

raise ValueError("需要3个关节的起点和终点角度")

start_waypoints = [

JointWaypoint(q_start[0], 0.0, 'joint1_base_yaw'),

JointWaypoint(q_start[1], 0.0, 'joint2_elbow_pitch'),

JointWaypoint(q_start[2], 0.0, 'joint3_end_rotate'),

]

end_waypoints = [

JointWaypoint(q_end[0], 0.0, 'joint1_base_yaw'),

JointWaypoint(q_end[1], 0.0, 'joint2_elbow_pitch'),

JointWaypoint(q_end[2], 0.0, 'joint3_end_rotate'),

]

return cls(start_waypoints, end_waypoints, duration, n_points, limits)

"trajectory/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_joint_angles(self, times, q_array):

"""

① 关节角度曲线:验证从起点平滑走到终点

"""

fig, axes = plt.subplots(3, 1, figsize=(10, 9), sharex=True)

joint_names = ['Joint1 (Base Yaw)', 'Joint2 (Elbow Pitch)', 'Joint3 (End Rotate)']

colors = ['steelblue', 'red', 'green']

for j_idx, (ax, name, color) in enumerate(zip(axes, joint_names, colors)):

ax.plot(times, q_array[:, j_idx] * 180 / np.pi, '-',

color=color, linewidth=1.5)

ax.set_ylabel(f'{name}\nAngle (deg)')

ax.set_title(f'{name.split("(")[0].strip()} — Angle Profile')

ax.grid(True, alpha=0.3)

axes[2].set_xlabel('Time (s)')

plt.tight_layout()

path = f"{self.results_dir}/01_joint_angles.png"

plt.savefig(path, dpi=150)

plt.close()

return path

def plot_joint_velocities(self, times, q_dot_array):

"""

② 关节角速度曲线:验证起终点为零、中间平滑(C¹连续)

"""

fig, axes = plt.subplots(3, 1, figsize=(10, 9), sharex=True)

joint_names = ['Joint1 (Base Yaw)', 'Joint2 (Elbow Pitch)', 'Joint3 (End Rotate)']

colors = ['steelblue', 'red', 'green']

for j_idx, (ax, name, color) in enumerate(zip(axes, joint_names, colors)):

ax.plot(times, q_dot_array[:, j_idx], '-',

color=color, linewidth=1.5)

ax.axhline(y=0, color='black', linestyle='--', linewidth=0.8, alpha=0.5)

ax.set_ylabel(f'{name}\nVelocity (rad/s)')

ax.set_title(f'{name.split("(")[0].strip()} — Velocity Profile (C¹ Continuous)')

ax.grid(True, alpha=0.3)

axes[2].set_xlabel('Time (s)')

plt.tight_layout()

path = f"{self.results_dir}/02_joint_velocities.png"

plt.savefig(path, dpi=150)

plt.close()

return path

def plot_joint_accelerations(self, times, q_ddot_array):

"""

③ 关节角加速度曲线:验证线性变化,注意起终点有阶跃(C¹但不C²)

"""

fig, axes = plt.subplots(3, 1, figsize=(10, 9), sharex=True)

joint_names = ['Joint1 (Base Yaw)', 'Joint2 (Elbow Pitch)', 'Joint3 (End Rotate)']

colors = ['steelblue', 'red', 'green']

for j_idx, (ax, name, color) in enumerate(zip(axes, joint_names, colors)):

ax.plot(times, q_ddot_array[:, j_idx], '-',

color=color, linewidth=1.5)

ax.axhline(y=0, color='black', linestyle='--', linewidth=0.8, alpha=0.5)

# 标注起点加速度跳变

ax.axvline(x=0, color='orange', linestyle=':', linewidth=1.0, alpha=0.7)

ax.axvline(x=times[-1], color='orange', linestyle=':', linewidth=1.0, alpha=0.7)

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

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

8700客户端操作文档:从环境基线到业务闭环的交付链路解析

简介&#xff1a;这份资源是海康威视iVMS-8700客户端的操作说明文档&#xff0c;面向智能楼宇安防运维人员、视频监控系统集成与实施工程师&#xff0c;以及需要快速上手该平台的初、中级操作者。内容围绕iVMS-8700基于SOA架构的模块化设计展开&#xff0c;重点讲解电视墙操作平…

作者头像 李华
网站建设 2026/10/14 15:44:02

Oracle生产实战避坑指南:建表规范、DML安全与查询性能三原则

1. 这不是教科书&#xff0c;是我在产线踩了三年坑后整理的Oracle实操手记“Oracle基本用法全教程&#xff08;一&#xff09;”——看到这个标题&#xff0c;你可能下意识想点开又划走&#xff1a;又是那种从CREATE TABLE开始、讲完SELECT就收工的“基础入门”&#xff1f;我懂…

作者头像 李华
网站建设 2026/10/14 15:43:47

OpenCV图像处理实操:从读图到边缘检测的完整指南

OpenCV这个词在计算机视觉圈子里出现频率有多高&#xff0c;不用我多说。无论你是要做图像分类、目标检测&#xff0c;还是只想写个脚本批量处理照片&#xff0c;几乎都绕不开这套库。今天这篇文章不聊复杂的深度学习&#xff0c;就踏踏实实把OpenCV的图像基本操作捋一遍&#…

作者头像 李华
网站建设 2026/10/14 15:43:30

蓝鲸CMDB API详解:create_process_instance 创建进程实例接口原理与实战

后端企业应用运维 【免费下载链接】bk-cmdb 蓝鲸智云配置平台(BlueKing CMDB) 项目地址&#xff1a; https://gitcode.com/gh_mirrors/bk/bk-cmdb 点击查看 免费下载 本篇围绕 bk-cmdb 配置中心&#xff08;CC&#xff09;的进程实例创建接口 create_process_instance 展开&am…

作者头像 李华