《Python机器人基础原理》第十八篇搬运工作站——给定关节状态基于动力学术解全套关节力矩1. 实际工业应用场景周五下午某汽车零部件厂的发动机缸体搬运工作站。一台3-DOF搬运机械臂基座偏航肘部俯仰垂直升降负责将缸体从加工中心取出并放置到AGV托盘上。机器人构型大臂长800 mm小臂长600 mm第三轴为z轴升降行程±300 mm。关节硬件限位θ₁ ±170°、θ₂ ±140°、d₃ ±0.3 m。连杆质量大臂8.5 kg质心距基座0.35 m小臂5.2 kg质心距肘部0.28 m末端液压夹具0.8 kg 缸体45.0 kg 45.8 kg质心距肘部0.60 m即小臂末端。关节额定扭矩基座偏航轴θ₁额定80 N·m肘部俯仰轴θ₂额定300 N·m。z轴额定轴向推力800 N。新来的调试工程师小王接手这台设备要验证一段新轨迹的力矩是否超限。他手头有每段轨迹的关节角度、角速度、角加速度数据——从示教器里导出的CSV每10 ms一个点。老张这段轨迹第二轴最大力矩到多少小王问。老张正在对面工位拧伺服驱动器的CAN线你用控制器自带的前馈算一下看峰值。控制器里的前馈模型用的是出厂默认参数大臂质心0.35 m实际夹具改过一次质心偏了大概3 cm。我想离线算一下给定每一组(q, q˙, q¨)直接求出全套关节力矩跟控制器里跑的对比。离线算你手头有动力学库吗没有。我就用numpy从头写。基座偏航轴θ₁绕竖直轴旋转重力对它的力矩恒为零——这个从模型里去掉。肘部俯仰轴θ₂才扛重力。给定状态直接算 τ M(q)q¨ C(q,q˙)q˙ G(q)。不需要积分不需要轨迹生成——输入就是状态输出就是力矩。那挺好。你把缸体负载也加上45 kg呢。还有算完跟伺服驱动器的实际电流曲线对比一下差太多说明参数该标定了。第三轴z升降的轴向力别忘了那可是449 N的死重。小王打开编辑器先跑一组典型工况θ₁30°, θ₂-45°, θ̇₁0.8 rad/s, θ̇₂-0.5 rad/s, θ̈₁2.0 rad/s², θ̈₂1.5 rad/s²。看看各轴力矩多少。本篇解决的问题给定任意一组关节角度、角速度、角加速度基于拉格朗日动力学方程直接求解全套关节力矩。基座偏航轴重力矩恒为零竖直轴肘部俯仰轴计算完整惯性科氏重力项z轴输出轴向支撑力。不生成轨迹不积分微分方程不做控制闭环。纯粹是给状态→算力矩的离线校核工具。⚠️ 声明本篇全部代码为课程离线仿真运行在普通PC上用于给定关节状态下动力学力矩的批量计算与校核。不可直接用于真实机器人控制器、伺服驱动器或搬运产线。真实工业现场需结合在线参数辨识、关节扭矩传感器反馈与安全监控。代码作者不对直接使用本代码于真实设备造成的任何后果负责。2. 痛点分析课程模块 本篇对应的工程痛点第4章 机器人动力学建模 → 4.2 连杆坐标系与D-H参数 连杆质量、质心位置是力矩计算的基础输入参数不准直接导致力矩偏差第4章 → 4.4 关节空间动力学方程 τ M(q)q¨ C(q,q˙)q˙ G(q) 给定关节状态直接求解力矩验证前馈模型输出是否准确第5章 机器人关节驱动与控制 → 5.3 前馈补偿 控制器前馈力矩 τ_ff M·q¨des C·q˙des G本篇离线复现该计算第6章 轨迹规划与控制 → 6.2 轨迹评价 评估给定轨迹的力矩峰值是否超出关节硬件限制本篇代码解决 L2 离线动力学校核层。输入关节状态数组(q, q˙, q¨) 机器人参数。输出各旋转关节的τ_M、τ_C、τ_G、τ_total以及z轴轴向力F_z。代码不是设备实时控制器不做伺服闭环不做轨迹规划。它做的事情是给定状态直接算力矩告诉工程师这段轨迹的力矩是否安全控制器前馈跟理论值差多少。不覆盖轨迹生成、实时控制、摩擦力建模、6轴完整动力学。3. 核心逻辑讲解物理直觉你推过超市购物车吗空车和满载推起来的感觉完全不一样——不只是重转弯时还拽手。机械臂也一样。给定某一瞬间机器人的姿态角度、运动快慢速度、加减速加速度你可以直接算出每个关节需要出多少力力矩。不用管之前怎么动的、之后要去哪——这一瞬间惯性、科氏力、重力各占多少加起来就是总力矩。但这里有个关键区分基座偏航轴θ₁绕竖直轴旋转重力方向也是竖直的力臂为零所以重力对θ₁的力矩恒为零。 只有肘部俯仰轴θ₂的轴是水平的才需要对抗重力。这是从第十四篇一路遗留到本篇的建模级错误——之前几篇把平面2连杆的公式原样搬过来重力项里带着cosθ₁物理上不成立。本篇彻底修正。业务逻辑 → 代码映射输入机器人参数DH、质量、质心 关节状态数组(q, q˙, q¨)↓步骤1对每一组状态计算惯性矩阵 M(q)2×2前两旋转轴↓步骤2计算科氏力/离心力向量 C(q,q˙)·q˙↓步骤3计算重力向量 G(q)仅τ_G2τ_G10↓步骤4τ_rot M·q¨ C·q˙ G↓步骤5计算z轴轴向力 F_z (m₁m₂m₃)·g↓步骤6汇总所有状态点的力矩统计峰值与硬件限位对比↓步骤7输出图表关键公式文字翻译惯性项 τ_M M(q)·q¨M是2×2矩阵。M₁₁ m₁·com₁² m₂·(L₁²com₂²2L₁·com₂·cosθ₂) m₃·(L₁²com₃²2L₁·com₃·cosθ₂)。注意m₃的力臂用的是com₃距肘部距离不是硬编码L₂。M₁₂耦合项同理。科氏力项 τ_C C(q,q˙)·q˙h m₂·L₁·com₂ m₃·L₁·com₃。com₃直接影响科氏耦合强度。重力项 τ_Gτ_G1 0基座偏航竖直轴。τ_G2 -g·(m₂·com₂ m₃·com₃)·cos(θ₁θ₂)。只有俯仰轴扛重力。z轴轴向力 F_zF_z (m₁m₂m₃)·g 附加惯性项。本篇简化为静载荷加减速分量。单位力矩 N·m角度 rad角速度 rad/s角加速度 rad/s²力 N。4. OOP代码实现技术栈严格限定numpy, scipy, matplotlib。不引入pybullet、roboticstoolbox等任何第三方机器人库。项目结构树robot_torque_solver/├── run_torque_solver.py├── requirements.txt├── dynamics/│ ├── __init__.py│ ├── models.py│ ├── trajectory.py│ ├── dynamics_solver.py│ └── visualize.py├── tests/│ ├── __init__.py│ └── test_torque_solver.py└── results/核心源码dynamics/models.py — dataclass数据模型3-DOF搬运机械臂数据模型连杆参数、质量、质心位置、关节限位、重力常量构型说明基座偏航(θ1, 竖直轴) 肘部俯仰(θ2, 水平轴) z升降(d3)物理声明- θ1绕竖直轴旋转重力对其力矩恒为零- θ2为水平俯仰轴承受全部重力矩- d3为垂直平移输出轴向力而非力矩质心坐标定义全局统一- com₁: 大臂质心距基座关节沿连杆方向的距离 (m)- com₂: 小臂质心距肘部关节沿连杆方向的距离 (m)- com₃: 末端负载质心距肘部关节沿连杆方向的距离 (m)from dataclasses import dataclass, fieldfrom typing import List, Tuple, Optionalimport numpy as npdataclassclass JointLimit:单关节硬件限位min_val: floatmax_val: floatjoint_name: str torque_limit: float 0.0 # 额定扭矩 (N·m)0表示未指定def __post_init__(self):if self.min_val self.max_val:raise ValueError(fJointLimit: min_val({self.min_val})必须严格小于fmax_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)dataclassclass JointLimitSet:3-DOF搬运机械臂关节限位集合limits: List[JointLimit]def __post_init__(self):if len(self.limits) ! 3:raise ValueError(f需要3个关节限位, 实际{len(self.limits)}个)classmethoddef default_3dof_transfer(cls):默认3-DOF搬运臂限位θ1: ±170° (基座偏航, 额定80 N·m)θ2: ±140° (肘部俯仰, 额定300 N·m)d3: ±300 mmpi 3.141592653589793return cls(limits[JointLimit(-170 * pi / 180, 170 * pi / 180, joint1_base_yaw, 80.0),JointLimit(-140 * pi / 180, 140 * pi / 180, joint2_elbow_pitch, 300.0),JointLimit(-0.300, 0.300, joint3_z_lift, 0.0),])def check_all_with_indices(self, joint_values: List[float]) - Tuple[bool, List[int]]:if len(joint_values) ! 3:raise ValueError(f需要3个关节值, 实际{len(joint_values)}个)all_pass Trueviolation_indices []for i, (val, limit) in enumerate(zip(joint_values, self.limits)):if not limit.contains(val):all_pass Falseviolation_indices.append(i)return all_pass, violation_indicesdef get_torque_limits(self) - List[float]:返回各关节额定扭矩列表return [lim.torque_limit for lim in self.limits]dataclassclass LinkMassProps:连杆质量与质心属性mass: 质量 (kg)com_x: 质心沿连杆方向距近端关节的距离 (m)com_y, com_z: 垂直于连杆方向的质心偏移 (m)本篇质点模型设为0link_name: 连杆标识max_com_x: 质心允许的最大x值连杆长度上限mass: floatcom_x: floatcom_y: floatcom_z: floatlink_name: str max_com_x: Optional[float] Nonedef __post_init__(self):if self.mass 0:raise ValueError(fLinkMassProps: 质量不能为负, 实际{self.mass})for name, val in [(com_x, self.com_x), (com_y, self.com_y),(com_z, self.com_z)]:if not np.isfinite(val):raise ValueError(fLinkMassProps.{name}必须为有限数值)if self.max_com_x is not None and self.com_x self.max_com_x:raise ValueError(fLinkMassProps({self.link_name}): 质心x{self.com_x}m f超出连杆长度上限{self.max_com_x}m)dataclassclass MassPropertySet:完整连杆质量属性集合link_props: List[LinkMassProps]link_lengths: List[float] field(default_factorylambda: [0.0, 0.0, 0.0])def __post_init__(self):if len(self.link_props) ! 3:raise ValueError(f需要3个连杆质量属性, 实际{len(self.link_props)}个)for i, (prop, length) in enumerate(zip(self.link_props, self.link_lengths)):if length 0 and prop.com_x length:raise ValueError(f连杆{i}({prop.link_name}): 质心{prop.com_x}m超出长度{length}m)classmethoddef default_transfer_arm(cls):3-DOF搬运机械臂质量参数大臂: 8.5 kg, 质心距基座0.35 m (L10.80m)小臂: 5.2 kg, 质心距肘部0.28 m (L20.60m)末端: 液压夹具0.8 kg 缸体45.0 kg 45.8 kg, 质心距肘部0.60mlink_lengths [0.80, 0.60, 0.0]props [LinkMassProps(8.5, 0.35, 0.0, 0.0, link1_upper_arm, link_lengths[0]),LinkMassProps(5.2, 0.28, 0.0, 0.0, link2_forearm, link_lengths[1]),LinkMassProps(45.8, 0.60, 0.0, 0.0, link3_end_effector, None),]return cls(link_propsprops, link_lengthslink_lengths)def copy_with_modified_com(self, link_index: int, new_com_x: float):返回修改后的质心属性副本new_props []for i, prop in enumerate(self.link_props):if i link_index:new_props.append(LinkMassProps(prop.mass, new_com_x, prop.com_y, prop.com_z,prop.link_name, prop.max_com_x))else:new_props.append(LinkMassProps(prop.mass, prop.com_x, prop.com_y, prop.com_z,prop.link_name, prop.max_com_x))return MassPropertySet(link_propsnew_props, link_lengthsself.link_lengths)def total_mass(self) - float:计算总质量return sum(prop.mass for prop in self.link_props)dataclassclass DHParam:标准D-H参数theta: floatd: floata: floatalpha: floatdef __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(fDHParam.{name}必须为有限数值)dataclassclass RobotParams:3-DOF搬运机械臂完整参数大臂800mm, 小臂600mmlinks: List[DHParam]joint_names: List[str] field(default_factorylambda: [joint1_base_yaw, joint2_elbow_pitch, joint3_z_lift])g: float 9.81classmethoddef default_transfer_arm(cls):links [DHParam(0.0, 0.0, 0.800, 0.0),DHParam(0.0, 0.0, 0.600, 0.0),DHParam(0.0, 0.0, 0.0, 0.0),]return cls(linkslinks)dynamics/trajectory.py — 关节状态序列生成器关节状态序列生成器从CSV或内置数组加载(q, q˙, q¨)序列本篇不生成轨迹只消费外部给定的状态数据CSV格式time, q1, q2, d3, qd1, qd2, dd3, qdd1, qdd2, ddd3import numpy as npfrom dynamics.models import JointLimitSetclass JointStateSequence:关节状态序列存储多组(q, q˙, q¨)用于批量力矩计算def __init__(self, times, q_array, q_dot_array, q_ddot_array,limits: JointLimitSet None):times: 时间戳数组 (s)q_array: 形状(n, 3) 关节角度 [θ1, θ2, d3]q_dot_array: 形状(n, 3) 关节速度 [θ̇1, θ̇2, ḋ3]q_ddot_array: 形状(n, 3) 关节加速度 [θ̈1, θ̈2, d̈3]limits: 可选用于校验角度是否超限位self.times np.array(times, dtypefloat)self.q_array np.array(q_array, dtypefloat)self.q_dot_array np.array(q_dot_array, dtypefloat)self.q_ddot_array np.array(q_ddot_array, dtypefloat)self.limits limitsn len(times)if self.q_array.shape ! (n, 3):raise ValueError(fq_array形状应为({n}, 3), 实际{self.q_array.shape})if self.q_dot_array.shape ! (n, 3):raise ValueError(fq_dot_array形状应为({n}, 3))if self.q_ddot_array.shape ! (n, 3):raise ValueError(fq_ddot_array形状应为({n}, 3))# 有限值检查for name, arr in [(q_array, self.q_array),(q_dot_array, self.q_dot_array),(q_ddot_array, self.q_ddot_array)]:if not np.all(np.isfinite(arr)):raise ValueError(f{name}包含非有限值(NaN或Inf))# 限位校验仅校验旋转关节的角度if limits is not None:for i, t in enumerate(times):rot_angles [self.q_array[i, 0], self.q_array[i, 1], self.q_array[i, 2]]all_pass, violations limits.check_all_with_indices(rot_angles)if not all_pass:raise ValueError(f时刻t{t:.3f}s: 关节{vi}超限 f(值{rot_angles[vi]:.3f}, f限位[{limits.limits[vi].min_val:.3f}, f{limits.limits[vi].max_val:.3f}])for vi in violations)classmethoddef from_csv(cls, filepath, limits: JointLimitSet None):从CSV文件加载状态序列data np.loadtxt(filepath, delimiter,, skiprows1)if data.shape[1] 10:raise ValueError(fCSV需要至少10列数据, 实际{data.shape[1]}列)times data[:, 0]q_array data[:, 1:4]q_dot_array data[:, 4:7]q_ddot_array data[:, 7:10]return cls(times, q_array, q_dot_array, q_ddot_array, limits)classmethoddef synthetic_test_sequence(cls, n_points50, limits: JointLimitSet None):生成合成测试序列模拟典型搬运轨迹的中间段使用五次多项式插值生成平滑状态序列times np.linspace(0, 1.0, n_points)# 模拟一段平滑运动θ₁从0→60°, θ₂从0→-45°, d₃从0→0.2mpi 3.141592653589793theta1_end 60 * pi / 180theta2_end -45 * pi / 180d3_end 0.20q_array np.zeros((n_points, 3))q_dot_array np.zeros((n_points, 3))q_ddot_array np.zeros((n_points, 3))for i, t in enumerate(times):# 五次多项式插值平滑 s 10t³ - 15t⁴ 6t⁵# t∈[0,1]时有效times上限就是1.0不需要else分支s 10*t**3 - 15*t**4 6*t**5s_dot 30*t**2 - 60*t**3 30*t**4s_ddot 60*t - 180*t**2 120*t**3q_array[i, 0] s * theta1_endq_array[i, 1] s * theta2_endq_array[i, 2] s * d3_endq_dot_array[i, 0] s_dot * theta1_endq_dot_array[i, 1] s_dot * theta2_endq_dot_array[i, 2] s_dot * d3_endq_ddot_array[i, 0] s_ddot * theta1_endq_ddot_array[i, 1] s_ddot * theta2_endq_ddot_array[i, 2] s_ddot * d3_endreturn cls(times, q_array, q_dot_array, q_ddot_array, limits)def __len__(self):return len(self.times)dynamics/dynamics_solver.py — 动力学求解器核心业务类动力学求解器给定关节状态(q, q˙, q¨)计算全套关节力矩τ M(q)q¨ C(q,q˙)q˙ G(q)3-DOF搬运臂基座偏航(θ1, 竖直轴) 肘部俯仰(θ2, 水平轴) z升降(d3)物理声明- θ1绕竖直轴旋转重力对其力矩恒为零 → τ_G1 0- θ2为水平俯仰轴承受全部重力矩- d3为垂直平移输出轴向力F_z而非旋转力矩- 末端负载质心com₃统一用于所有含m₃的力臂项不硬编码L2注意每次修改质量/质心参数后必须创建新的DynamicsSolver实例import numpy as npfrom dynamics.models import RobotParams, MassPropertySetclass DynamicsSolver:基于拉格朗日方程的2旋转1平移搬运臂动力学求解器给定关节状态直接计算全套力矩和轴向力classmethoddef from_mass_props(cls, params: RobotParams, mass_props: MassPropertySet):工厂方法从参数创建求解器推荐用于参数扫描return cls(params, mass_props)def __init__(self, params: RobotParams, mass_props: MassPropertySet):self.params paramsself.mass_props mass_propsself.g params.gself.L1 params.links[0].aself.L2 params.links[1].aself.m1 mass_props.link_props[0].massself.m2 mass_props.link_props[1].massself.m3 mass_props.link_props[2].mass # 末端负载self.com1 mass_props.link_props[0].com_xself.com2 mass_props.link_props[1].com_xself.com3 mass_props.link_props[2].com_x # 末端负载距肘部距离# 科氏力耦合系数 h m₂·L₁·com₂ m₃·L₁·com₃# com₃统一用于力臂计算不再硬编码L2self.h self.m2 * self.L1 * self.com2 self.m3 * self.L1 * self.com3# 总质量用于z轴轴向力self.total_mass self.m1 self.m2 self.m3def inertia_matrix(self, theta2):计算2×2惯性矩阵 M(q)前两旋转轴m₃的力臂统一使用com₃c2 np.cos(theta2)# M11 m₁·com₁² m₂·(L₁²com₂²2L₁·com₂·cosθ₂)# m₃·(L₁²com₃²2L₁·com₃·cosθ₂)M11 (self.m1 * self.com1**2 self.m2 * (self.L1**2 self.com2**2 2*self.L1*self.com2*c2) self.m3 * (self.L1**2 self.com3**2 2*self.L1*self.com3*c2))# M12 m₂·(com₂²L₁·com₂·cosθ₂) m₃·(com₃²L₁·com₃·cosθ₂)M12 (self.m2 * (self.com2**2 self.L1*self.com2*c2) self.m3 * (self.com3**2 self.L1*self.com3*c2))# M22 m₂·com₂² m₃·com₃²M22 self.m2 * self.com2**2 self.m3 * self.com3**2return np.array([[M11, M12],[M12, M22]], dtypefloat)def coriolis_vector(self, theta2, theta1_dot, theta2_dot):计算科氏力/离心力向量h中使用com₃com₃变化直接影响科氏力矩s2 np.sin(theta2)tau_C1 -self.h * s2 * (2 * theta1_dot * theta2_dot theta2_dot**2)tau_C2 self.h * s2 * theta1_dot**2return np.array([tau_C1, tau_C2], dtypefloat)def gravity_vector(self, theta1, theta2):计算重力项向量θ1绕竖直轴旋转 → τ_G1 0重力对竖直轴无力矩τ_G2 -g·(m₂·com₂ m₃·com₃)·cos(θ₁θ₂)# τ_G1 0 基座偏航轴竖直轴重力力矩恒为零tau_G1 0.0c12 np.cos(theta1 theta2)# τ_G2: 小臂质心 末端负载质心均使用com参数tau_G2 -self.g * (self.m2 * self.com2 self.m3 * self.com3) * c12return np.array([tau_G1, tau_G2], dtypefloat)def axial_force(self, d3_ddot0.0):计算z轴轴向支撑力 (N)F_z 总重力 m_total·d̈₃升降加速度分量F_gravity self.total_mass * self.gF_inertia self.total_mass * d3_ddotreturn F_gravity F_inertiadef compute_all_terms(self, q, q_dot, q_ddot):计算完整动力学方程的各项分解返回(tau_M, tau_C, tau_G, tau_total)tau_total为3元素数组第三轴力矩恒为0z平移输出轴向力if len(q) 2 or len(q_dot) 2 or len(q_ddot) 2:raise ValueError(需要至少2个旋转关节的角度、速度、加速度)theta1 q[0]theta2 q[1]theta1_dot q_dot[0]theta2_dot q_dot[1]theta1_ddot q_ddot[0]theta2_ddot q_ddot[1]M self.inertia_matrix(theta2)tau_M_rot M np.array([theta1_ddot, theta2_ddot])tau_C_rot self.coriolis_vector(theta2, theta1_dot, theta2_dot)tau_G_rot self.gravity_vector(theta1, theta2)tau_total_rot tau_M_rot tau_C_rot tau_G_rot# 扩展为3元素第三轴z平移力矩恒为0tau_M np.array([tau_M_rot[0], tau_M_rot[1], 0.0])tau_C np.array([tau_C_rot[0], tau_C_rot[1], 0.0])tau_G np.array([tau_G_rot[0], tau_G_rot[1], 0.0])tau_total np.array([tau_total_rot[0], tau_total_rot[1], 0.0])return tau_M, tau_C, tau_G, tau_totaldef compute_sequence(self, state_sequence):对状态序列批量计算力矩返回字典包含各力矩历史数组和z轴轴向力n len(state_sequence)tau_M_hist np.zeros((n, 3))tau_C_hist np.zeros((n, 3))tau_G_hist np.zeros((n, 3))tau_total_hist np.zeros((n, 3))F_z_hist np.zeros(n)for i in range(n):tau_M, tau_C, tau_G, tau_total self.compute_all_terms(state_sequence.q_array[i],state_sequence.q_dot_array[i],state_sequence.q_ddot_array[i])tau_M_hist[i, :] tau_Mtau_C_hist[i, :] tau_Ctau_G_hist[i, :] tau_Gtau_total_hist[i, :] tau_total# z轴轴向力d3_ddot state_sequence.q_ddot_array[i, 2]F_z_hist[i] self.axial_force(d3_ddot)peaks {tau_M_peak: np.max(np.abs(tau_M_hist), axis0),tau_C_peak: np.max(np.abs(tau_C_hist), axis0),tau_G_peak: np.max(np.abs(tau_G_hist), axis0),tau_total_peak: np.max(np.abs(tau_total_hist), axis0),F_z_peak: np.max(np.abs(F_z_hist)),}return {peaks: peaks,tau_M_hist: tau_M_hist,tau_C_hist: tau_C_hist,tau_G_hist: tau_G_hist,tau_total_hist: tau_total_hist,F_z_hist: F_z_hist,}dynamics/visualize.py — 可视化模块搬运工作站关节力矩计算仿真结果可视化4张核心图表图表②的硬件限位从JointLimitSet动态传入不再硬编码假数import numpy as npimport matplotlib.pyplot as pltplt.rcParams[font.sans-serif] [SimHei, DejaVu Sans]plt.rcParams[axes.unicode_minus] Falseclass Visualizer:def __init__(self, results_dirresults):self.results_dir results_dirimport osos.makedirs(results_dir, exist_okTrue)def plot_torque_breakdown(self, times, tau_M_hist, tau_C_hist, tau_G_hist, tau_total_hist):① 各力矩分量随时间变化Joint1和Joint2fig, axes plt.subplots(2, 1, figsize(10, 7), sharexTrue)joint_names [Joint1 (Base Yaw, Vertical Axis),Joint2 (Elbow Pitch, Horizontal Axis)]for j_idx, ax in enumerate(axes):ax.plot(times, tau_M_hist[:, j_idx利用AI解决实际问题如果你觉得这个工具好用欢迎关注长安牧笛