python的机器人基础原理工业场景模拟第一百一十六篇:机械臂动力学-控制联合仿真,把动力学力矩输出接入PD控制器完成闭环。
《Python机器人基础原理》第一百一十六篇:机械臂动力学-控制联合仿真——把动力学力矩输出接入PD控制器完成闭环
声明:本篇全部为课程离线仿真代码,不能直接用于真实机器人控制器,规避工程误导风险。
1. 实际工业应用场景
周五上午,焊接工作站。小赵盯着示波器上那条扭动的关节1电流曲线,眉头拧成了结。昨天刚把PD参数从Kp=200调到Kp=350,末端跟踪圆弧的误差确实从8 mm压到了4 mm,但关节1在路径上半段有明显的"抖动-补偿"循环,电机发热严重。
老郑端着咖啡走过来,看了一眼屏幕:"你把重力忘了。竖直平面内,关节1扛着整条臂+负载的重力矩,峰值快30 N·m。你那350的P乘以位置误差,最多输出十几牛米——不够补重力的,所以控制器一直在'追重力'。"
小赵愣了一下:"所以PD在跟重力拔河?"
"对。PD只管'位置差→出力',它不知道重力在那儿蹲着。你要么加大P(但会抖),要么把重力前馈加上——让动力学模型先算出一个开环重力矩,PD只管补偿剩下的跟踪误差。这就是动力学-控制联合仿真的意义:让控制器'知道'物理规律,而不是纯靠反馈硬扛。"
构型约定:竖直平面内2R机械臂。连杆参数:L1 a₁=0.50 m / m₁=6.5 kg / I₁=0.45 kg·m² / com₁=0.25 m;L2 a₂=0.40 m / m₂=4.0 kg / I₂=0.20 kg·m² / com₂=0.20 m。末端跟踪任务:圆心(0.45, 0.30) m、半径0.15 m、角度-40°~+40°的圆弧,21航点,总时长4.0 s。经代码验证:r∈[0.600, 0.689] m,q₂∈[-97°, -81°](elbow_down),远离奇异区(r=0.90 m,q₂=0°)。PD参数:Kp=[300, 200] N·m/rad,Kd=[50, 35] N·m·s/rad。对比方案:①纯PD(无重力补偿);②PD+重力前馈G(q_des);③PD+逆动力学前馈(M·qdd_des + C·qd_des + G(q_des))。力矩限幅±120/±90 N·m。RK4步长0.001 s,仿真5.0 s。技术栈:numpy + scipy(interpolate) + matplotlib。
⚠️ 声明:本篇全部代码为课程离线仿真,运行在普通PC上,用于动力学-控制联合验证。代码中不包含任何真实机器人控制器实时运算或安全逻辑。不可直接用于产线设备。
2. 痛点分析
课程模块 本篇对应的工程痛点
第4章 机器人动力学建模 → 刚体动力学 竖直平面内重力矩不可忽略,纯PD与重力"拔河"导致抖动和发热
第5章 机器人关节驱动与控制 → 前馈控制 反馈控制单打独斗不够,需要动力学前馈分担主要物理负载
第6章 轨迹规划与控制 → 轨迹跟踪 跟踪精度取决于"前馈精度+反馈补偿"的合力,而非单一环节
本篇代码解决 L2 离线仿真校核层。输入:2R连杆参数、笛卡尔圆弧航点、PD增益、前馈模式选择。处理:①笛卡尔路径三次样条→期望关节角/速度/加速度(IK+样条直接求值,不做二次降级);②动力学模型实时计算M、C、G;③控制器按选定模式组合输出力矩,tau_ff直接记录;④RK4积分。输出:4张联合仿真对比图表 + 误差/力矩报告。
不覆盖:摩擦力模型、关节柔性、电机电气动态、实时操作系统调度。
3. 核心逻辑讲解
物理直觉
想象你用手端着一杯水画圆弧。你的肌肉不仅要出力让手"走到正确位置"(这对应PD的反馈力),还要持续出力对抗水的重力(这对应重力前馈)。如果你只靠"眼睛看偏差再纠正"(纯PD),你会不自觉地晃——因为你的肌肉一直在"发现偏差→纠正→又偏差"的循环里。
动力学前馈就是让控制器"提前知道"需要出多少力来对抗重力和加速度,PD只需要处理剩下的小偏差。这就像你画弧之前就知道手要倾斜多少来平衡水的重力,画的时候只需要微调。
业务逻辑 → 代码映射
输入:理想笛卡尔航点 + 连杆参数 + PD增益 + 前馈模式
↓
【步骤1:轨迹生成】
笛卡尔圆弧 → 三次样条 → p_des(t), v_des(t), a_des(t)
IK → q_des(t),样条直接求值 qd_des(t), qdd_des(t)(不降级为线性插值)
↓
【步骤2:仿真循环(对每个时刻t)】
① 读取当前 q(t), qd(t)
② 通过样条对象直接求值:q_des, qd_des, qdd_des(平滑连续)
③ 动力学计算:
M = mass_matrix(q)
C = coriolis(q, qd)
G = gravity(q)
④ 控制器输出(三种模式):
模式A(纯PD):τ = Kp*(q_des-q) + Kd*(qd_des-qd)
模式B(+重力前馈):τ += G(q_des)
模式C(+逆动力学前馈):τ += M(q_des)·qdd_des + C(q_des,qd_des)·qd_des + G(q_des)
⑤ 直接记录返回的 tau_ff(不是用两闭环力矩做差分!)
⑥ RK4积分得到下一步 q, qd
↓
【步骤3:指标汇总】
各模式末端误差RMSE、各关节力矩曲线、前馈力矩曲线
↓
【步骤4:4张图表】
关键公式文字翻译
关节驱动力方程:M(q)·qdd + C(q,qd)·qd + G(q) = τ。左边是"物理世界要的力",右边是"电机给的力"。
纯PD:τ = Kp·(q_des - q) + Kd·(qd_des - qd)。完全不管M、C、G——相当于蒙着眼睛靠手感调位置。
重力前馈:τ = Kp·e_q + Kd·e_qd + G(q_des)。G(q_des)是开环的重力预估,在期望位形求值,不依赖当前实际q——所以即使q还没到位,重力补偿已经在那儿了。
逆动力学前馈:τ = M(q_des)·qdd_des + C(q_des,qd_des)·qd_des + G(q_des) + Kp·e_q + Kd·e_qd。前馈项在期望轨迹上求值(即"逆动力学"),精确预估了沿理想轨迹运动所需的力矩。误差动态满足 M·ë + Kd·ė + Kp·e ≈ 0(当模型精确时),是一个解耦的线性二阶系统。实际中模型参数有误差,所以跟踪精度受限于模型精度,不可能"任意精确"。
4. OOP代码实现
技术栈
限定:numpy, scipy(interpolate仅用于轨迹插值), matplotlib。不引入pybullet、roboticstoolbox等任何第三方重型库。
项目结构树
dynamics_control_sim/
├── run_sim.py # 主入口
├── requirements.txt
├── robot/
│ ├── __init__.py
│ ├── models.py # dataclass数据模型
│ ├── dynamics.py # 2R闭式动力学(M, C, G)
│ ├── kinematics.py # 正/逆运动学
│ ├── trajectory.py # 笛卡尔路径 + IK + 样条直接求值
│ ├── controller.py # PD / PD+重力 / 逆动力学前馈 三种模式
│ ├── simulator.py # RK4仿真器(支持多模式对比,直接记录tau_ff)
│ ├── metrics.py # 误差/力矩指标
│ └── visualize.py # 4张图表
├── tests/
│ ├── __init__.py
│ └── test_sim.py # 3类测试
├── results/ # 4张图表
└── output/ # 指标报告txt
核心源码
"robot/models.py"
"""
Data models for 2R dynamics-control co-simulation.
"""
from dataclasses import dataclass, field
import numpy as np
@dataclass
class LinkInertia:
name: str
mass: float # kg
length: float # m
inertia: float # kg·m^2
com_offset: float # m
def __post_init__(self):
if self.mass <= 0:
raise ValueError(f"{self.name}: mass must be positive")
if self.length <= 0:
raise ValueError(f"{self.name}: length must be positive")
if self.inertia <= 0:
raise ValueError(f"{self.name}: inertia must be positive")
if not 0 < self.com_offset <= self.length:
raise ValueError(f"{self.name}: com_offset must be in (0, length]")
@dataclass
class PDGains:
Kp: np.ndarray
Kd: np.ndarray
def __post_init__(self):
# Validate BEFORE reshape — ensures wrong shapes raise ValueError
kp = np.asarray(self.Kp, dtype=float)
kd = np.asarray(self.Kd, dtype=float)
if kp.shape != (2,):
raise ValueError(f"Kp must be length-2, got shape {kp.shape}")
if kd.shape != (2,):
raise ValueError(f"Kd must be length-2, got shape {kd.shape}")
self.Kp = kp
self.Kd = kd
@dataclass
class TorqueLimits:
tau_max: np.ndarray
def __post_init__(self):
self.tau_max = np.asarray(self.tau_max, dtype=float).reshape(2,)
if self.tau_max.shape != (2,) or np.any(self.tau_max <= 0):
raise ValueError("tau_max must be length-2 positive array")
@dataclass
class SimConfig:
links: list = field(default_factory=list)
gravity: float = 9.81
dt: float = 0.001
t_sim: float = 5.0
def __post_init__(self):
if not self.links:
self.links = [
LinkInertia("L1", 6.5, 0.50, 0.45, 0.25),
LinkInertia("L2", 4.0, 0.40, 0.20, 0.20),
]
"robot/dynamics.py"
"""
2R planar rigid-body dynamics (vertical plane).
Closed-form M, C, G — no symbolic dependency, pure numpy.
"""
import numpy as np
import math
from robot.models import SimConfig
class Dynamics2R:
def __init__(self, config: SimConfig):
self.cfg = config
self.links = config.links
self.g = config.gravity
self.m1 = self.links[0].mass
self.m2 = self.links[1].mass
self.l1 = self.links[0].length
self.l2 = self.links[1].length
self.r1 = self.links[0].com_offset
self.r2 = self.links[1].com_offset
self.I1 = self.links[0].inertia
self.I2 = self.links[1].inertia
def mass_matrix(self, q):
"""M(q) — symmetric positive definite."""
q2 = q[1]
c2 = math.cos(q2)
M11 = (self.I1 + self.I2 + self.m1*self.r1**2
+ self.m2*(self.l1**2 + self.r2**2 + 2*self.l1*self.r2*c2))
M12 = self.I2 + self.m2*self.r2*(self.r2 + self.l1*c2)
M22 = self.I2 + self.m2*self.r2**2
return np.array([[M11, M12], [M12, M22]], dtype=float)
def coriolis_vector(self, q, qd):
"""C(q, qd)·qd — centrifugal & Coriolis terms."""
q2 = q[1]
s2 = math.sin(q2)
h = self.m2 * self.l1 * self.r2 * s2
C1 = h * qd[1] * (2.0*qd[0] + qd[1])
C2 = -h * qd[0]**2
return np.array([[C1], [C2]], dtype=float)
def gravity_vector(self, q):
"""G(q) — gravity torques in vertical plane."""
q1 = q[0]
q2 = q[1]
c1 = math.cos(q1)
c12 = math.cos(q1 + q2)
G1 = self.m1*self.g*self.r1*c1 + self.m2*self.g*(self.l1*c1 + self.r2*c12)
G2 = self.m2*self.g*self.r2*c12
return np.array([[G1], [G2]], dtype=float)
def inverse_dynamics(self, q, qd, qdd):
"""
Compute required torque: τ = M·qdd + C·qd + G.
Used for full inverse dynamics feedforward at desired trajectory.
"""
M = self.mass_matrix(q)
Cqd = self.coriolis_vector(q, qd)
G = self.gravity_vector(q)
tau = M @ qdd.reshape(2, 1) + Cqd + G
return tau
"robot/kinematics.py"
"""
2R planar forward/inverse kinematics (flange center, no TCP offset).
"""
import numpy as np
import math
class Kinematics2R:
def __init__(self, l1=0.50, l2=0.40):
self.l1 = l1
self.l2 = l2
def forward(self, q):
q1, q2 = q[0], q[1]
x = self.l1 * math.cos(q1) + self.l2 * math.cos(q1 + q2)
y = self.l1 * math.sin(q1) + self.l2 * math.sin(q1 + q2)
return np.array([x, y], dtype=float)
def inverse(self, x, y, elbow_down=True):
r2 = x**2 + y**2
r = math.sqrt(r2)
r_min, r_max = abs(self.l1 - self.l2), self.l1 + self.l2
if r < r_min - 1e-6 or r > r_max + 1e-6:
raise ValueError(f"Point ({x:.4f}, {y:.4f}) outside workspace "
f"[{r_min:.3f}, {r_max:.3f}] m")
cos_q2 = (r2 - self.l1**2 - self.l2**2) / (2.0 * self.l1 * self.l2)
cos_q2 = max(-1.0, min(1.0, cos_q2))
q2 = math.acos(cos_q2)
if elbow_down:
q2 = -q2
alpha = math.atan2(y, x)
beta = math.atan2(self.l2 * math.sin(abs(q2)),
self.l1 + self.l2 * math.cos(abs(q2)))
q1 = alpha - beta if elbow_down else alpha + beta
return np.array([q1, q2], dtype=float)
"robot/trajectory.py"
"""
Cartesian arc trajectory → joint space via IK + CubicSpline direct evaluation.
Key design: spline objects are stored and evaluated directly (no linear interpolation
degradation). IK failures print a warning instead of silent fallback.
"""
import numpy as np
import math
from scipy.interpolate import CubicSpline
from robot.kinematics import Kinematics2R
class JointTrajectoryGenerator:
def __init__(self, kinematics: Kinematics2R, num_samples=500,
elbow_down=True):
self.kin = kinematics
self.num_samples = num_samples
self.elbow_down = elbow_down
def generate_from_arc(self, center_x, center_y, radius, angle_min_deg,
angle_max_deg, t_total=4.0, num_waypoints=21):
"""Create arc waypoints, then IK + joint splines."""
angles = np.linspace(angle_min_deg * np.pi / 180,
angle_max_deg * np.pi / 180, num_waypoints)
t_wp = np.linspace(0.0, t_total, num_waypoints)
waypoints_xy = np.zeros((num_waypoints, 2))
for i, theta in enumerate(angles):
waypoints_xy[i, 0] = center_x + radius * np.cos(theta)
waypoints_xy[i, 1] = center_y + radius * math.sin(theta)
# Cartesian spline
spl_x = CubicSpline(t_wp, waypoints_xy[:, 0], bc_type='natural')
spl_y = CubicSpline(t_wp, waypoints_xy[:, 1], bc_type='natural')
# Sample densely for joint trajectory
t_dense = np.linspace(0.0, t_total, self.num_samples)
p_dense = np.column_stack([spl_x(t_dense), spl_y(t_dense)])
# IK for each dense sample (with warning on failure)
q_dense = np.zeros((self.num_samples, 2))
for i, p in enumerate(p_dense):
try:
q_dense[i] = self.kin.inverse(p[0], p[1],
elbow_down=self.elbow_down)
except ValueError as e:
print(f" ⚠ IK failed at sample {i}, t={t_dense[i]:.3f}s: {e}")
q_dense[i] = q_dense[i-1] if i > 0 else np.zeros(2)
# Joint splines — these are the authoritative references
spl_q1 = CubicSpline(t_dense, q_dense[:, 0], bc_type='natural')
spl_q2 = CubicSpline(t_dense, q_dense[:, 1], bc_type='natural')
return t_dense, spl_q1, spl_q2
"robot/controller.py"
"""
Three control modes:
Mode 0: Pure PD (no feedforward)
Mode 1: PD + gravity feedforward G(q_des)
Mode 2: PD + inverse dynamics feedforward (M·qdd_des + C·qd_des + G(q_des))
Returns tau_pd and tau_ff separately for direct logging (no differential proxy).
"""
import numpy as np
from robot.models import PDGains, TorqueLimits
from robot.dynamics import Dynamics2R
class DynamicsController:
def __init__(self, gains: PDGains, torque_limits: TorqueLimits,
dynamics: Dynamics2R, mode=0):
self.Kp = gains.Kp
self.Kd = gains.Kd
self.tau_max = torque_limits.tau_max.reshape(2, 1)
self.dyn = dynamics
self.mode = mode # 0, 1, or 2
def compute_torque(self, q, qd, q_des, qd_des, qdd_des):
q = np.asarray(q).reshape(2, 1)
qd = np.asarray(qd).reshape(2, 1)
q_des = np.asarray(q_des).reshape(2, 1)
qd_des = np.asarray(qd_des).reshape(2, 1)
qdd_des = np.asarray(qdd_des).reshape(2, 1)
# PD feedback
e_q = q_des - q
e_qd = qd_des - qd
tau_pd = (self.Kp.reshape(2, 1) * e_q
+ self.Kd.reshape(2, 1) * e_qd)
tau_ff = np.zeros((2, 1))
if self.mode >= 1:
# Gravity feedforward at desired posture (open-loop)
G = self.dyn.gravity_vector(q_des.flatten())
tau_ff += G
if self.mode >= 2:
# Inverse dynamics feedforward at desired trajectory
M = self.dyn.mass_matrix(q_des.flatten())
Cqd = self.dyn.coriolis_vector(q_des.flatten(), qd_des.flatten())
tau_ff += M @ qdd_des + Cqd
tau_raw = tau_pd + tau_ff
tau_clipped = np.clip(tau_raw, -self.tau_max, self.tau_max)
return tau_clipped, tau_pd, tau_ff
"robot/simulator.py"
"""
RK4 simulator. Uses spline objects directly for smooth reference evaluation.
Records tau_ff directly from controller (no differential proxy).
Initial state set to IK(t=0) to eliminate transient contamination.
"""
import numpy as np
from robot.dynamics import Dynamics2R
from robot.controller import DynamicsController
from robot.models import SimConfig
class DynamicsControlSimulator:
def __init__(self, dynamics: Dynamics2R, controller: DynamicsController,
t_grid, spl_q1, spl_q2, config: SimConfig):
self.dyn = dynamics
self.ctrl = controller
self.cfg = config
self.dt = config.dt
self.n_steps = int(config.t_sim / config.dt)
self.t_grid = t_grid
self.spl_q1 = spl_q1
self.spl_q2 = spl_q2
def _get_ref(self, t):
"""Direct spline evaluation — smooth, no linear interpolation."""
t_clip = max(self.t_grid[0], min(t, self.t_grid[-1]))
q1 = float(self.spl_q1(t_clip))
q2 = float(self.spl_q2(t_clip))
qd1 = float(self.spl_q1(t_clip, 1))
qd2 = float(self.spl_q2(t_clip, 1))
qdd1 = float(self.spl_q1(t_clip, 2))
qdd2 = float(self.spl_q2(t_clip, 2))
return (np.array([q1, q2]), np.array([qd1, qd2]),
np.array([qdd1, qdd2]))
def _derivatives(self, x, t):
q = x[0:2]
qd = x[2:4]
q_des, qd_des, qdd_des = self._get_ref(t)
tau, _, _ = self.ctrl.compute_torque(q, qd, q_des, qd_des, qdd_des)
M = self.dyn.mass_matrix(q)
Cqd = self.dyn.coriolis_vector(q, qd)
G = self.dyn.gravity_vector(q)
qdd = np.linalg.solve(M, tau - Cqd - G)
return np.concatenate([qd, qdd.flatten()])
def run(self, q_start=None, q_dot_start=None):
if q_start is None:
q_start = np.zeros(2)
if q_dot_start is None:
q_dot_start = np.zeros(2)
x = np.concatenate([q_start, q_dot_start])
x_hist = np.zeros((self.n_steps + 1, 4))
x_hist[0] = x
# Log PD and FF torques separately (direct from controller)
tau_pd_hist = np.zeros((self.n_steps + 1, 2))
tau_ff_hist = np.zeros((self.n_steps + 1, 2))
t_arr = np.arange(self.n_steps + 1) * self.dt
dt = self.dt
for i in range(self.n_steps):
t = t_arr[i]
xi = x_hist[i]
k1 = self._derivatives(xi, t)
k2 = self._derivatives(xi + 0.5*dt*k1, t + 0.5*dt)
k3 = self._derivatives(xi + 0.5*dt*k2, t + 0.5*dt)
k4 = self._derivatives(xi + dt*k3, t + dt)
# Average control quantities for logging (RK4-weighted)
_, pd1, ff1 = self.ctrl.compute_torque(
xi[0:2], xi[2:4], *self._get_ref(t))
_, pd2, ff2 = self.ctrl.compute_torque(
xi[0:2] + 0.5*dt*k1[0:2], xi[2:4] + 0.5*dt*k1[2:4],
*self._get_ref(t + 0.5*dt))
_, pd3, ff3 = self.ctrl.compute_torque(
xi[0:2] + 0.5*dt*k2[0:2], xi[2:4] + 0.5*dt*k2[2:4],
*self._get_ref(t + 0.5*dt))
_, pd4, ff4 = self.ctrl.compute_torque(
xi[0:2] + dt*k3[0:2], xi[2:4] + dt*k3[2:4],
*self._get_ref(t + dt))
tau_pd_hist[i+1] = (pd1 + 2*pd2 + 2*pd3 + pd4).flatten() / 6.0
tau_ff_hist[i+1] = (ff1 + 2*ff2 + 2*ff3 + ff4).flatten() / 6.0
x_next = xi + (dt/6.0)*(k1 + 2*k2 + 2*k3 + k4)
x_hist[i+1] = x_next
return t_arr, x_hist, tau_pd_hist, tau_ff_hist
"robot/metrics.py"
"""
Metrics: end-effector error, torque statistics.
Error defined via original Cartesian spline (not FK(q_des_interp)).
"""
import numpy as np
from scipy.interpolate import CubicSpline
from robot.kinematics import Kinematics2R
def compute_end_error_cartesian(t, x_hist, spl_x, spl_y, kinematics: Kinematics2R):
"""
Compute Cartesian end-effector tracking error using original
Cartesian spline as p_des (not FK of interpolated joint angles).
"""
n = len(t)
e_norm = np.zeros(n)
for i in range(n):
q_actual = x_hist[i, 0:2]
p_actual = kinematics.forward(q_actual)
# Original Cartesian path
t_clip = max(spl_x.x[0], min(t[i], spl_x.x[-1]))
p_des = np.array([float(spl_x(t_clip)), float(spl_y(t_clip))])
e_norm[i] = np.linalg.norm(p_actual - p_des)
return e_norm
def compute_rmse(arr):
return np.sqrt(np.mean(arr**2))
"robot/visualize.py"
"""
4 charts for dynamics-control co-simulation comparison.
Chart ④ plots tau_ff directly (not a differential proxy).
"""
import os
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 Visualizer:
def __init__(self, results_dir="results"):
self.results_dir = results_dir
os.makedirs(results_dir, exist_ok=True)
def plot_end_error_comparison(self, t, e_pure, e_grav, e_full):
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(t, e_pure * 1000, 'r-', linewidth=1.0, label='Pure PD')
ax.plot(t, e_grav * 1000, 'g-', linewidth=1.0, label='PD + Gravity FF')
ax.plot(t, e_full * 1000, 'b-', linewidth=1.0, label='PD + Inv. Dyn. FF')
ax.set_xlabel('Time (s)')
ax.set_ylabel('End-Effector Error (mm)')
ax.set_title('End-Effector Tracking Error: 3 Control Modes')
ax.legend(fontsize=8)
ax.grid(True, alpha=0.3)
path = os.path.join(self.results_dir, "01_end_error_comparison.png")
plt.tight_layout()
plt.savefig(path, dpi=150)
plt.close()
return path
def plot_joint1_torque(self, t, tau_pd_pure, tau_pd_grav, tau_pd_full,
tau_ff_grav, tau_ff_full):
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(t, tau_pd_pure[:, 0], 'r-', linewidth=0.8, label='Pure PD: τ_PD')
ax.plot(t, tau_pd_grav[:, 0], 'g-', linewidth=0.8, label='+Grav FF: τ_PD')
ax.plot(t, (tau_pd_full[:, 0] + tau_ff_full[:, 0]), 'b-', linewidth=0.8,
label='Inv. Dyn. FF: τ_total')
ax.plot(t, tau_ff_grav[:, 0], 'm--', linewidth=0.6, label='Grav FF only')
ax.plot(t, tau_ff_full[:, 0], 'c--', linewidth=0.6, label='Inv. Dyn. FF total')
ax.set_xlabel('Time (s)')
ax.set_ylabel('Torque (N·m)')
ax.set_title('Joint 1 Torque Decomposition')
ax.legend(fontsize=7)
ax.grid(True, alpha=0.3)
path = os.path.join(self.results_dir, "02_joint1_torque.png")
plt.tight_layout()
plt.savefig(path, dpi=150)
plt.close()
return path
def plot_joint2_torque(self, t, tau_pd_pure, tau_pd_grav, tau_pd_full,
tau_ff_grav, tau_ff_full):
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(t, tau_pd_pure[:, 1], 'r-'
利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)