《Python机器人基础原理》第一百三十五篇:重复定位仿真——多次执行同一目标点位闭环运动,统计末端点位离散误差

 

声明:本篇全部为课程离线仿真代码,不能直接用于真实机器人控制器,规避工程误导风险。

 

1. 实际工业应用场景

 

周一早班,白车身点焊工位。2R平面搬运机械臂要把焊枪重复送到同一个目标点——基座坐标系下的(0.60 m, 0.30 m),准备执行连续30次点焊。每次循环:从上一轮结束的实际状态出发,经过0.8 s的5阶多项式点到点运动,到位后保持2.0 s,然后进入下一轮。

 

"第3次和第27次的落点肉眼就能看出偏差。"质检员把测量数据递过来,"30次到位位置画出来是个小椭圆,长轴1.8 mm,短轴0.9 mm。工艺要求重复定位精度±0.5 mm,超差了。"

 

调试工程师小刘翻出参数:"2R平面构型,L1=0.55 m / L2=0.45 m,连杆质量8.0 kg / 5.0 kg,转动惯量0.60 kg·m² / 0.18 kg·m²,质心0.28 m / 0.22 m,末端负载2.0 kg。PD用Kp=[600, 450] N·m/rad、Kd=[60, 45] N·m·s/rad。到位保持阶段期望速度为零,纯靠PD+重力前馈把关节锁在目标角。跑30次链式重复定位仿真,注入关节静摩擦死区和负载质量摄动,看末端点位的离散分布到底有多大。"

 

⚠️ 声明:本代码仅用于课程离线仿真,运行在普通PC上。不包含任何真实机器人控制器的实时通信栈或硬件驱动。不可直接用于产线设备。

 

2. 痛点分析

 

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

第4章 机器人动力学建模 → 重力/摩擦力 到位保持阶段,静摩擦死区导致微小PD输出无法克服库仑摩擦,产生微幅粘滑振动

第5章 机器人关节驱动与控制 → 伺服闭环 链式重复中每次初始状态不同(上一轮残余振动),控制器无积分项,微小稳态误差逐次累积

第6章 轨迹规划与控制 → 点到点运动 运动段结束后进入保持段,瞬态收敛过程会消耗部分保持时间,影响有效定位窗口

 

本篇代码解决 L2 离线仿真校核层。输入:2R连杆参数、目标点位、PD增益、重复次数、散布源参数。处理:①链式循环,每轮从上一轮结束状态出发;②5阶多项式点到点轨迹运动0.8 s;③到位后零速保持2.0 s;④注入静摩擦死区与负载质量摄动;⑤RK4物理积分;⑥按ISO 9283统计相对均值的离散误差。输出:4张分析图表 + 单元测试。

 

不覆盖:编码器量化误差、减速器背隙、温漂、视觉引导闭环。散布源为离线仿真中人为注入的简化模型,不代表真实物理的全部不确定性。

 

3. 核心逻辑讲解

 

物理直觉

 

重复定位精度衡量的是"同一个指令,执行多次,末端实际落在哪里"。它不是一次准不准的问题,而是多次之间散不散的问题。真实机器人的散布来自多个源头:关节静摩擦死区让微小驱动力不足以克服库仑摩擦,末端只能在死区内微振;负载质量随焊丝消耗缓慢变化,重力前馈的参数与实际不再一致;上一轮结束时的残余振动成为下一轮的初始扰动。这些散布源叠加在一起,让每次的最终落点形成一个小范围的散布椭圆。本篇在确定性动力学模型中有意注入这两类散布源——关节静摩擦死区和负载质量每轮随机摄动,让仿真能够复现这一物理现象。

 

业务逻辑 → 代码映射

 

输入:连杆参数 + 目标点位 + PD增益 + 重复次数 + 散布源参数

  ↓

【步骤1:生成点到点轨迹】

  5阶多项式插值 → 0.8 s运动段(起止速度/加速度为零)

  → 2.0 s保持段(q_des恒定,qd_des=0)

  → 物理时间轴以 dt_phys=0.1 ms 均匀铺开

  ↓

【步骤2:链式重复执行N次】

  第1轮从目标点附近随机初始位形出发

  第i轮(i>1)从上轮结束的实际状态出发

  → 每轮注入静摩擦死区(库仑+粘性)

  → 每轮负载质量在标称值附近随机摄动

  → RK4仿真 → 记录全程误差时程 + 保持段末端实际位置

  ↓

【步骤3:离散误差统计(ISO 9283口径)】

  计算样本均值 p̄

  → 每轮相对均值的偏差 di = ||pi - p̄||

  → 重复定位精度 RP = mean(di) + 3σ(di)

  → 同时计算绝对定位精度(相对目标点)作为对照

  ↓

【步骤4:可视化】

  4张图:散布散点+3σ椭圆、相对均值误差分布、X/Y箱线图、收敛叠加

 

关键公式文字翻译

 

5阶多项式轨迹:q(t) = a0 + a1·t + a2·t² + a3·t³ + a4·t⁴ + a5·t⁵。6个系数由6个边界条件确定:起止位置、起止速度(0)、起止加速度(0)。这样生成的轨迹在运动段内速度连续、加速度连续,不会产生阶跃冲击。

 

ISO 9283 重复定位精度:RP = (1/N)·Σ‖p_i − p̄‖ + 3·std(‖p_i − p̄‖)。它衡量的是各次落点相对样本均值的离散程度,与系统性偏差无关。系统性偏差属于绝对定位精度(accuracy)的范畴,两者口径不同,本篇同时给出。

 

静摩擦死区模型:当|qd| < v_db时,摩擦力为库仑摩擦·sign(qd)乘以一个平滑过渡系数;否则为库仑摩擦+粘性摩擦。死区让极小速度下的驱动力被摩擦完全抵消,末端在目标附近产生微幅不可控振荡。

 

4. OOP代码实现

 

技术栈

 

限定:numpy, matplotlib。不引入pybullet、roboticstoolbox等第三方重型库。

 

项目结构树

 

repeatability_sim/

├── run_sim.py # 主入口(重复定位仿真)

├── requirements.txt

├── robot/

│ ├── __init__.py

│ ├── models.py # dataclass数据模型

│ ├── inverse_kinematics.py # 解析逆解

│ ├── trajectory.py # 5阶多项式点到点轨迹

│ ├── dynamics.py # 2R闭式动力学(含摩擦)

│ ├── controller.py # PD+重力前馈

│ ├── simulator.py # RK4仿真器(记录历史)

│ └── visualize.py # 4张图表

├── tests/

│ ├── __init__.py

│ └── test_integration.py # 3类11个测试

├── results/ # 4张图表

└── README.md

 

核心源码

 

"robot/models.py"

 

"""

Data models for repeatability simulation.

# OFFLINE SIMULATION ONLY — not for production controllers.

"""

 

import numpy as np

from dataclasses import dataclass

 

 

@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 Payload:

    """

    末端负载。

    质心默认位于末端执行器末端(距关节2距离 = l2),

    即负载力臂为 l2,与连杆2末端重合。

    """

    mass: float = 0.0 # kg

    def __post_init__(self):

        if self.mass < 0:

            raise ValueError("Payload mass cannot be negative")

 

 

@dataclass

class PDGains:

    """PD增益。"""

    Kp: np.ndarray

    Kd: np.ndarray

 

    def __post_init__(self):

        self.Kp = np.asarray(self.Kp, dtype=float)

        self.Kd = np.asarray(self.Kd, dtype=float)

        if self.Kp.shape != (2,):

            raise ValueError(f"Kp must be length-2, got {self.Kp.shape}")

        if self.Kd.shape != (2,):

            raise ValueError(f"Kd must be length-2, got {self.Kd.shape}")

        if np.any(self.Kp <= 0) or np.any(self.Kd < 0):

            raise ValueError("Kp must be positive, Kd non-negative")

 

 

@dataclass

class SimConfig:

    """

    仿真全局配置。

    T_move: 运动段时间 (s)

    T_hold: 保持段时间 (s)

    tau_limit_j1 / tau_limit_j2: 关节力矩限幅 (N·m)

    """

    links: list

    T_move: float = 0.8

    T_hold: float = 2.0

    gravity: float = 9.81

    dt_phys: float = 0.0001

    tau_limit_j1: float = 100.0

    tau_limit_j2: float = 80.0

 

    def __post_init__(self):

        if not self.links or len(self.links) < 2:

            raise ValueError("SimConfig.links must contain at least 2 links")

        if self.T_move <= 0 or self.T_hold <= 0:

            raise ValueError("T_move and T_hold must be positive")

        if self.dt_phys <= 0:

            raise ValueError("SimConfig.dt_phys must be positive")

        if self.tau_limit_j1 <= 0 or self.tau_limit_j2 <= 0:

            raise ValueError("tau_limit must be positive")

 

"robot/inverse_kinematics.py"

 

"""

解析逆运动学:平面2R机械臂。

# OFFLINE SIMULATION ONLY — not for production controllers.

"""

 

import numpy as np

import math

from robot.models import SimConfig

 

 

class InverseKinematics2R:

    """平面2R解析逆解,多解按与上一拍最近分支筛选。"""

    def __init__(self, config: SimConfig):

        if len(config.links) < 2:

            raise ValueError("InverseKinematics2R requires at least 2 links")

        self.l1 = config.links[0].length

        self.l2 = config.links[1].length

 

    def solve_single(self, x, y, q_target=None):

        """返回最优逆解。不可达时抛出ValueError。"""

        x, y = float(x), float(y)

        r2 = x * x + y * y

        r = math.sqrt(r2)

 

        max_reach = self.l1 + self.l2

        min_reach = abs(self.l1 - self.l2)

        if r > max_reach + 1e-9 or r < min_reach - 1e-9:

            raise ValueError(

                f"Position ({x:.4f}, {y:.4f}) unreachable: "

                f"r={r:.4f} outside [{min_reach}, {max_reach}]")

 

        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_a = math.acos(cos_q2)

        q2_b = -q2_a

 

        solutions = []

        for q2 in [q2_a, q2_b]:

            phi = math.atan2(self.l2 * math.sin(q2),

                             self.l1 + self.l2 * math.cos(q2))

            q1 = math.atan2(y, x) - phi

            solutions.append(np.array([q1, q2]))

 

        if q_target is not None:

            q_target = np.asarray(q_target, dtype=float)

            solutions.sort(key=lambda q: np.linalg.norm(q - q_target))

 

        return solutions[0].copy()

 

"robot/trajectory.py"

 

"""

5阶多项式点到点轨迹。

# OFFLINE SIMULATION ONLY — not for production controllers.

"""

 

import numpy as np

 

 

class QuinticJointTrajectory:

    """

    5阶多项式关节轨迹。

    边界条件:起止位置给定,起止速度=0,起止加速度=0。

    运动段:t ∈ [0, T_move],5阶多项式平滑过渡。

    t >= T_move 时返回目标值(保持段由 Simulator 根据 T_hold 处理)。

    """

    def __init__(self, q_start, q_target, T_move):

        self.q_start = np.asarray(q_start, dtype=float)

        self.q_target = np.asarray(q_target, dtype=float)

        self.T_move = float(T_move)

 

        if self.T_move <= 0:

            raise ValueError(f"T_move must be positive, got {self.T_move}")

 

        delta = self.q_target - self.q_start

        T = self.T_move

        T2 = T * T

        T3 = T2 * T

        T4 = T3 * T

        T5 = T4 * T

 

        self.a0 = self.q_start.copy()

        self.a1 = np.zeros(2)

        self.a2 = np.zeros(2)

        self.a3 = 10.0 * delta / T3

        self.a4 = -15.0 * delta / T4

        self.a5 = 6.0 * delta / T5

 

    def get_at(self, t):

        """返回t时刻的关节角、速度、加速度。"""

        if t <= 0:

            return self.q_start.copy(), np.zeros(2), np.zeros(2)

 

        if t >= self.T_move:

            return self.q_target.copy(), np.zeros(2), np.zeros(2)

 

        t2 = t * t

        t3 = t2 * t

        t4 = t3 * t

        t5 = t4 * t

 

        q = (self.a0 + self.a1 * t + self.a2 * t2

             + self.a3 * t3 + self.a4 * t4 + self.a5 * t5)

        qd = (self.a1 + 2.0 * self.a2 * t

              + 3.0 * self.a3 * t2 + 4.0 * self.a4 * t3

              + 5.0 * self.a5 * t4)

        qdd = (2.0 * self.a2 + 6.0 * self.a3 * t

               + 12.0 * self.a4 * t2 + 20.0 * self.a5 * t3)

 

        return q, qd, qdd

 

"robot/dynamics.py"

 

"""

2R planar rigid-body dynamics with payload and friction.

# OFFLINE SIMULATION ONLY — not for production controllers.

"""

 

import warnings

import numpy as np

import math

from robot.models import SimConfig, Payload

 

 

class Dynamics2R:

    def __init__(self, config: SimConfig):

        self.cfg = config

        self.links = config.links

        self.g = config.gravity

        self.payload = Payload(0.0)

 

        # 静摩擦死区参数(关节空间)

        self.coulomb_j1 = 2.5 # N·m

        self.coulomb_j2 = 1.5 # N·m

        self.viscous_j1 = 0.8 # N·m·s/rad

        self.viscous_j2 = 0.5 # N·m·s/rad

        self.deadband_vel = 1e-4 # rad/s,速度低于此值进入死区平滑

 

        self.l1 = self.links[0].length

        self.m1 = self.links[0].mass

        self.r1 = self.links[0].com_offset

        self.I1 = self.links[0].inertia

 

        self.l2 = self.links[1].length

        self.m2 = self.links[1].mass

        self.r2 = self.links[1].com_offset

        self.I2 = self.links[1].inertia

 

    def set_payload(self, p: Payload):

        self.payload = p

 

    def forward_kinematics(self, q):

        """参数化正运动学。"""

        x = self.l1 * math.cos(q[0]) + self.l2 * math.cos(q[0] + q[1])

        y = self.l1 * math.sin(q[0]) + self.l2 * math.sin(q[0] + q[1])

        return np.array([x, y], dtype=float)

 

    def mass_matrix(self, q):

        """2R质量矩阵M(q)。"""

        q2 = q[1]

        c2 = math.cos(q2)

        mp = self.payload.mass

        M11 = (self.I1 + self.I2 + mp * self.l2**2

               + self.m1 * self.r1**2

               + self.m2 * (self.l1**2 + self.r2**2

                            + 2 * self.l1 * self.r2 * c2)

               + mp * (self.l1**2 + self.l2**2

                       + 2 * self.l1 * self.l2 * c2))

        M12 = (self.I2

               + self.m2 * self.r2 * (self.r2 + self.l1 * c2)

               + mp * self.l2 * (self.l2 + self.l1 * c2))

        M22 = self.I2 + mp * self.l2**2 + self.m2 * self.r2**2

        return np.array([[M11, M12], [M12, M22]], dtype=float)

 

    def coriolis_vector(self, q, qd):

        """科氏力/离心力向量。"""

        q2 = q[1]

        s2 = math.sin(q2)

        mp = self.payload.mass

        h = (self.m2 * self.l1 * self.r2

             + mp * self.l1 * self.l2) * 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)。"""

        q1, q2 = q[0], q[1]

        c1 = math.cos(q1)

        c12 = math.cos(q1 + q2)

        mp = self.payload.mass

        G1 = (self.m1 * self.g * self.r1 * c1

              + self.m2 * self.g * (self.l1 * c1 + self.r2 * c12)

              + mp * self.g * (self.l1 * c1 + self.l2 * c12))

        G2 = (self.m2 * self.g * self.r2 * c12

              + mp * self.g * self.l2 * c12)

        return np.array([G1, G2], dtype=float)

 

    def friction_torque(self, qd):

        """

        关节静摩擦+粘性摩擦模型。

        速度低于 deadband_vel 时,库仑项按速度比例平滑过渡,

        避免符号函数带来的数值不连续。

        """

        tau_f = np.zeros(2)

        for i, qdi in enumerate(qd):

            fc = self.coulomb_j1 if i == 0 else self.coulomb_j2

            fv = self.viscous_j1 if i == 0 else self.viscous_j2

 

            if abs(qdi) < self.deadband_vel:

                # 死区内平滑过渡,避免sign不连续

                scale = qdi / self.deadband_vel

                tau_f[i] = (fc * scale + fv * qdi)

            else:

                tau_f[i] = fc * np.sign(qdi) + fv * qdi

        return tau_f

 

    def forward_dynamics(self, q, qd, tau):

        """

        正向动力学:τ - C(q,qd) - G(q) - τ_friction = M(q)·qdd

        """

        M = self.mass_matrix(q)

        Cqd = self.coriolis_vector(q, qd)

        G = self.gravity_vector(q)

        tau_f = self.friction_torque(qd)

        rhs = tau - Cqd - G - tau_f

 

        det = M[0, 0] * M[1, 1] - M[0, 1] * M[1, 0]

        if abs(det) < 1e-12:

            warnings.warn(f"Mass matrix near-singular, det={det:.2e}",

                          RuntimeWarning)

            return np.zeros(2)

        inv_M = np.array([[M[1, 1], -M[0, 1]],

                           [-M[1, 0], M[0, 0]]]) / det

        return inv_M @ rhs

 

"robot/controller.py"

 

"""

PD控制器 + 重力前馈。

# OFFLINE SIMULATION ONLY — not for production controllers.

"""

 

import numpy as np

from robot.models import PDGains

 

 

class PDController:

    def __init__(self, gains: PDGains, gravity_compensation=True):

        self.Kp = gains.Kp

        self.Kd = gains.Kd

        self.gravity_compensation = gravity_compensation

 

    def compute_torque(self, q, qd, q_des, qd_des, gravity_torque=None):

        """

        PD + 重力前馈。

        重力前馈使用期望位形下的重力向量 G(q_des)。

        """

        e = q_des - q

        ed = qd_des - qd

        tau_pd = self.Kp * e + self.Kd * ed

 

        if self.gravity_compensation and gravity_torque is not None:

            tau = tau_pd + gravity_torque

        else:

            tau = tau_pd

 

        return tau

 

"robot/simulator.py"

 

"""

RK4仿真器:重复定位仿真(记录全程历史)。

# OFFLINE SIMULATION ONLY — not for production controllers.

"""

 

import numpy as np

from robot.dynamics import Dynamics2R

from robot.controller import PDController

from robot.models import SimConfig, Payload

 

 

class Simulator:

    def __init__(self, dynamics: Dynamics2R, controller: PDController,

                 trajectory, config: SimConfig):

        self.dyn = dynamics

        self.ctrl = controller

        self.traj = trajectory

        self.config = config

 

        # 总仿真时长 = 运动段 + 保持段,均从 SimConfig 读取

        self.t_total = config.T_move + config.T_hold

        self.n_phys_steps = int(round(self.t_total / config.dt_phys))

 

    def run(self, q_start, payload_mass=2.0):

        """

        单次仿真运行,返回最终状态 + 全程历史。

        """

        self.dyn.set_payload(Payload(payload_mass))

 

        # 状态向量:[q1, q2, qd1, qd2]

        x = np.concatenate([np.asarray(q_start, dtype=float), np.zeros(2)])

 

        # 预分配历史数组

        t_arr = np.arange(self.n_phys_steps) * self.config.dt_phys

        pos_errors_mm = np.zeros(self.n_phys_steps)

        q_hist = np.zeros((self.n_phys_steps, 2))

 

        for i in range(self.n_phys_steps):

            t = t_arr[i]

 

            # 期望轨迹

            q_des, qd_des, _ = self.traj.get_at(t)

 

            # 记录期望与实际的正运动学误差

            p_act = self.dyn.forward_kinematics(x[:2])

            p_des = self.dyn.forward_kinematics(q_des)

            pos_errors_mm[i] = np.linalg.norm(p_act - p_des) * 1000.0

            q_hist[i] = x[:2]

 

            # 控制力矩

            grav_ff = self.dyn.gravity_vector(q_des)

            tau = self.ctrl.compute_torque(

                x[:2], x[2:4], q_des, qd_des, gravity_torque=grav_ff)

 

            # 按关节分级力矩限幅

            tau[0] = np.clip(tau[0],

                              -self.config.tau_limit_j1,

                               self.config.tau_limit_j1)

            tau[1] = np.clip(tau[1],

                              -self.config.tau_limit_j2,

                               self.config.tau_limit_j2)

 

            # RK4积分

            def deriv(xx, tau_fixed=tau):

                qdd = self.dyn.forward_dynamics(

                    xx[:2], xx[2:4], tau_fixed)

                return np.concatenate([xx[2:4], qdd])

 

            k1 = deriv(x)

            k2 = deriv(x + 0.5 * self.config.dt_phys * k1)

            k3 = deriv(x + 0.5 * self.config.dt_phys * k2)

            k4 = deriv(x + self.config.dt_phys * k3)

            x = x + (self.config.dt_phys / 6.0) * (k1 + 2*k2 + 2*k3 + k4)

 

        # 返回最终状态 + 历史

        p_final = self.dyn.forward_kinematics(x[:2])

        return {

            'q_final': x[:2].copy(),

            'qd_final': x[2:4].copy(),

            'p_final': p_final,

            't_hist': t_arr,

            'pos_errors_mm': pos_errors_mm,

            'q_hist': q_hist,

        }

 

"robot/visualize.py"

 

"""

4张重复定位仿真可视化图表。

# OFFLINE SIMULATION ONLY — not for production controllers.

"""

 

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.rd = results_dir

        os.makedirs(results_dir, exist_ok=True)

 

    def plot_endpoint_scatter(self, endpoints, target, stats):

        """图1:末端点位散布散点图,叠加3σ椭圆(ISO 9283口径)。"""

        fig, ax = plt.subplots(figsize=(7, 7))

 

        x = endpoints[:, 0]

        y = endpoints[:, 1]

 

        # 散点

        ax.scatter(x, y, c='steelblue', s=40, alpha=0.7,

                   edgecolors='k', linewidths=0.5, label='实际落点')

 

        # 目标点

        ax.scatter(target[0], target[1], c='red', s=120,

                   marker='*', edgecolors='k', linewidths=1.5,

                   label='目标点', zorder=5)

 

        # 样本均值

        ax.scatter(stats['mean_x'], stats['mean_y'], c='orange', s=80,

                   marker='s', edgecolors='k', linewidths=1.0,

                   label='样本均值', zorder=5)

 

        # 3σ椭圆(相对均值)

        eigvals, eigvecs = np.linalg.eigh(stats['cov'])

        order = eigvals.argsort()[::-1]

        eigvals = eigvals[order]

        eigvecs = eigvecs[:, order]

 

        theta = np.linspace(0, 2*np.pi, 100)

        ell = 3.0 * np.sqrt(eigvals[:, None]) * np.array([np.cos(theta), np.sin(theta)])

        ell = eigvecs @ ell

        ell[0, :] += stats['mean_x']

        ell[1, :] += stats['mean_y']

        ax.plot(ell[0, :], ell[1, :], 'r--', linewidth=1.5,

                label='3σ 置信椭圆')

 

        ax.set_xlabel('X (m)')

        ax.set_ylabel('Y (m)')

        ax.set_title(f'重复定位末端点位散布 (N={len(endpoints)})')

        ax.legend(fontsize=8)

        ax.set_aspect('equal', adjustable='box')

        ax.grid(True, alpha=0.3)

        plt.tight_layout()

 

        path = os.path.join(self.rd, "01_endpoint_scatter.png")

        plt.savefig(path, dpi=150)

        plt.close()

        return path

 

    def plot_relative_error_histogram(self, rel_errors_mm):

        """图2:相对样本均值的误差分布直方图。"""

        fig, ax = plt.subplots(figsize=(10, 5))

 

        ax.hist(rel_errors_mm, bins=min(15, len(rel_errors_mm)),

                color='steelblue', edgecolor='white', alpha=0.8)

 

        mean_rel = np.mean(rel_errors_mm)

        std_rel = np.std(rel_errors_mm)

        rp = mean_rel + 3 * std_rel

 

        ax.axvline(mean_rel, color='red', linestyle='-',

                   linewidth=1.5, label=f'均值={mean_rel:.3f} mm')

        ax.axvline(rp, color='orange', linestyle='--',

                   linewidth=1.5, label=f'RP(均值+3σ)={rp:.3f} mm')

 

        ax.set_xlabel('相对均值的末端位置误差 (mm)')

        ax.set_ylabel('频次')

        ax.set_title('重复定位误差分布直方图(ISO 9283口径)')

        ax.legend(fontsize=8)

        ax.grid(True, alpha=0.3)

        plt.tight_layout()

 

        path = os.path.join(self.rd, "02_relative_error_histogram.png")

        plt.savefig(path, dpi=150)

        plt.close()

        return path

 

    def plot_boxplot_xy(self, endpoints, target):

        """图3:X/Y方向相对均值误差箱线图。"""

        fig, ax = plt.subplots(figsize=(8, 5))

 

        mean_pos = np.mean(endpoints, axis=0)

        dx = (endpoints[:, 0] - mean_pos[0]) * 1000.0 # mm

        dy = (endpoints[:, 1] - mean_pos[1]) * 1000.0 # mm

 

        bp = ax.boxplot([dx, dy], patch_artist=True,

                        labels=['X方向误差 (mm)', 'Y方向误差 (mm)'])

        bp['boxes'][0].set_facecolor('lightblue')

        bp['boxes'][1].set_facecolor('lightgreen')

 

        ax.axhline(0, c

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

Logo

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

更多推荐