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

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐