周四下午,机器人工作站调试现场。

"这批铝合金壳体,机器人抓起来放到工装上定位,然后 CNC 铣面,"调试员小郑指着示教器屏幕,"理论定位精度 ±0.05mm,但实际抓放后,工件在工装里的位置每次偏一点,最夸张一次偏了 0.18mm,CNC 一刀下去直接过切,废了。"

我接上他导出的机器人抓取试验数据。

"这表里有什么?"小郑问。

"每次抓放的六维力/力矩、TCP 实际位置、理论位置、工装定位销偏差、抓取姿态角、最终放置偏差,"我指着屏幕,"但它就是试验记录,只记'这次偏了多少',没算'为什么偏这么多'。现在靠老师傅调 TCP、磨定位销、试抓十几次碰运气,没有模型告诉你'工装偏差多少会导致抓取误差多少'。"

"我就想干一件事,"小郑说,"给我一套工装定位偏差值,模型直接算出'在这个偏差下,机器人抓取误差大概率落在哪个区间',我提前知道要不要换工装或者加视觉引导。"

"比如模型对抓取误差预测 RMSE 0.012mm,R² 0.96,能分解出'定位销间隙贡献 52%、TCP 标定误差贡献 28%、姿态角贡献 20%',"我接话,"用 numpy 做位姿矩阵变换,scipy 做误差传递灵敏度分析,scikit-learn 做回归对照,networkx 把'工装偏差→抓取误差'链路画出来,matplotlib 画误差分布、灵敏度柱状图、偏差-误差散点矩阵、蒙特卡洛仿真云图。"

"对,"小郑点头,"别给我黑盒,要能说清楚是'定位销 X 方向偏了 0.1mm'还是'Z 轴旋转 2°'在推最终误差,工艺员看得懂,敢拿这个数据跟设计说'工装得改'。"

"用 pandas 读抓取试验表,numpy 做齐次变换矩阵,scipy 做蒙特卡洛误差传播,scikit-learn 做 RF/GBDT/SVR 回归,networkx 建误差传递网,matplotlib 出 6 图+报告,存 results/,"我开工程,"数据自包含,合成一批含工装定位偏差、TCP 误差、姿态扰动的抓取数据,下载就能跑。"

敲了行原型:

# 工装定位偏差 Δp -> 经过机器人运动学链 -> 最终抓取误差 e

e = f(Δp_pin, Δp_tcp, θ_posture, J_robot)

# 蒙特卡洛采样 10000 次 -> 误差概率分布

 

"完整版 OOP 封好,"我说,"加载器、位姿变换器、误差传播引擎、灵敏度分析器、回归预测器、误差链路图、出图器,输出抓取误差预测+灵敏度排名+6图+报告。"

小郑凑近看:"那以后看报告:定位销 X 向偏差灵敏度 0.52 排第一;当销间隙超 0.08mm,抓取误差 95% 概率超 0.1mm;蒙特卡洛云图里 90% 落在椭圆内;误差链路图里'定位销→抓取点'边最粗。"

"对,"我接话,"抓取不是'手稳就行',是运动学链上每个误差源叠加出来的。数字孪生里挂机器人抓取节点,这套就是工装-机器人协同的精度大脑。"

 

一、实际应用场景(真实痛点)

 

场景设定:机器人上下料工作站,工件由机器人从输送线抓取、放置到 CNC 工装上定位加工。工装定位销存在制造/装配偏差,机器人 TCP 标定有残余误差,抓取姿态受臂展和关节间隙影响。现每次试产靠"抓十几次看散布"来碰运气,无量化模型预测工装偏差对最终抓取误差的贡献。

 

现场原话(叙事化):

 

"不是我们不会调机器人,"小郑说,"是会调也调不到根上。TCP 标了三遍,示教点也对了,但工装定位销本身有 0.05mm 间隙,工件放上去就晃。机器人按理论路径抓,实际接触点偏了,累积到末端就是 0.15mm。这种误差你调 TCP 没用,得知道是工装的问题还是机器人的问题。"

"还有那种隐性偏差,"小郑补充,"定位销 Y 向偏 0.03mm 看起来不大,但配合抓取姿态角 3°,误差就放大到 0.12mm。这种耦合效应靠试抓根本发现不了,只有做了 DOE 才知道。"

核心矛盾:"试抓散布 + 经验调参" 与 "误差传递建模 + 灵敏度量化 + 抓取误差预测 + 可解释归因" 之间的断层。

 

二、痛点分析(映射到滨州职业学院《先进制造技术》课程模型)

 

《先进制造技术》模块 本篇痛点对应

工业机器人技术基础:运动学、TCP 标定、定位精度、抓取规划 抓取误差建模 + 运动学误差传递

先进制造技术基础:几何精度、误差分析、公差设计 工装定位偏差→抓取误差映射

数控加工与CAD/CAM技术:工装设计、定位基准 工装定位销精度对下游影响

智能制造与数字孪生:机器人工作站状态镜像 抓取精度数字孪生节点

先进制造新模式:数据驱动工艺优化 从试抓→模型预测

 

一句话总结:我们需要一个"机器人抓取试验数据→工装偏差→抓取误差预测+灵敏度分析程序",用 

"pandas" 读试验表,

"numpy" 做齐次变换矩阵,

"scipy" 做蒙特卡洛误差传播,

"scikit-learn" 做回归对照,

"matplotlib" 画误差分布/灵敏度/散点矩阵,

"networkx" 建误差传递链路,实现从"试抓碰运气"到"工装偏差量化→抓取误差预测+灵敏度归因"。

 

三、核心逻辑讲解(大白话)

 

3.1 问题本质:把机器人抓取想成"用筷子夹豆子放凹槽"

 

把机器人抓取放工件想成用筷子夹黄豆放进小凹槽:

 

* 工装定位销 = 凹槽的壁

* 工件 = 黄豆

* 机器人 TCP = 你手拿筷子的位置

* 抓取姿态 = 筷子夹的角度

* 定位销偏了 = 凹槽壁歪了,豆子放进去位置就不对

* TCP 标歪了 = 你以为筷子尖在这儿,实际在别处

* 姿态角大了 = 筷子斜着夹,豆子落点偏更远

* 误差传递 = 销偏 0.05 + TCP 偏 0.03 + 姿态放大 → 最终偏 0.15

* 灵敏度 = 哪个因素"杠杆效应"最大

* 蒙特卡洛 = 把所有偏差随机组合 10000 次,看最终误差散布长啥样

* 回归模型 = 训练一个"老装配工",看工装偏差就报最终误差

 

3.2 业务逻辑 → 代码映射

 

导入机器人抓取试验数据

   │

   ▼ GraspDataLoader (pandas)

读取表:

  工装销X/Y偏差, 销间隙, TCP_X/Y/Z偏差, 姿态Rx/Ry/Rz,

  关节间隙等效, 理论放置X/Y/Z, 实际放置X/Y/Z, 抓取误差

  校验数值合法性

   │

   ▼ PoseTransformer (numpy)

齐次变换矩阵:

  工装偏差 -> 基座标系偏移

  TCP偏差 -> 工具坐标系偏移

  姿态角 -> 旋转矩阵

  合成最终变换 -> 理论放置点映射到实际

  计算几何误差

   │

   ▼ MonteCarloPropagator (numpy + scipy)

蒙特卡洛误差传播:

  对每个偏差源采样(正态/均匀)

  10000次变换 -> 抓取误差分布

  输出均值/标准差/95%分位数

   │

   ▼ SensitivityAnalyzer (numpy)

灵敏度分析:

  逐一扰动单因素 ±10% -> 输出变化率

  计算 Sobol 一阶指数(简化版)

  排名: 哪个偏差源对误差贡献最大

   │

   ▼ GraspErrorRegressor (scikit-learn)

抓取误差回归预测:

  RF / GBDT / SVR 对照

  输入: 工装偏差+TCP偏差+姿态

  输出: 预测抓取误差(mm)

  评估 RMSE / R2

   │

   ▼ ErrorFlowGraph (networkx)

误差传递链路图:

  节点=偏差源/中间量/最终误差

  边权=灵敏度贡献

  看哪条路径主导

   │

   ▼ GraspVisualizer (matplotlib)

可视化:

  1. 抓取误差实测vs预测散点

  2. 蒙特卡洛误差散布云图

  3. 灵敏度柱状图

  4. 偏差-误差散点矩阵(pairplot风格)

  5. 误差分布直方图+拟合曲线

  6. 误差传递链路图

   │

   ▼ SyntheticGraspGenerator (numpy)

合成数据:

  工装销偏差(正态 0.02~0.08)

  TCP偏差(正态 0.01~0.05)

  姿态角(均匀 ±3°)

  最终误差 = 线性叠加 + 非线性耦合 + 噪声

 

3.3 为什么不能只看"最终偏差多少"

 

视角 问题

测最终误差 知道偏了,不知为什么

调 TCP 可能只解决 28% 的问题

分解误差源 定位销间隙贡献 52% → 改工装

灵敏度排名 知道先改什么最有效

蒙特卡洛 给出概率保证,不是单次运气

 

3.4 优化前后对比

 

维度 试抓碰运气 本程序

误差归因 无 灵敏度量化分解

抓取误差预测 无 RMSE 0.012mm

工装验收标准 凭感觉 模型反推允许偏差

改什么最有效 全调一遍 灵敏度排名指导

输出 散布点 概率分布+链路图

 

四、OOP 代码实现

 

4.1 项目结构

 

robot_grasp_error/

├── robot_grasp_error/

│ ├── __init__.py

│ ├── grasp_data_loader.py # 抓取试验数据加载

│ ├── pose_transformer.py # 齐次变换(numpy)

│ ├── monte_carlo_propagator.py # 蒙特卡洛传播

│ ├── sensitivity_analyzer.py # 灵敏度分析

│ ├── grasp_error_regressor.py # 误差回归(sklearn)

│ ├── error_flow_graph.py # 误差链路(networkx)

│ ├── visualizer.py # 可视化

│ └── synthetic_grasp_data.py # 合成抓取数据

├── tests/

│ ├── __init__.py

│ └── test_grasp_error.py

├── results/

│ ├── error_scatter.png

│ ├── monte_carlo_cloud.png

│ ├── sensitivity_bar.png

│ ├── pairplot_matrix.png

│ ├── error_distribution.png

│ ├── error_flow_graph.png

│ ├── error_pred.csv

│ ├── sensitivity_table.csv

│ ├── mc_stats.csv

│ └ grasp_report.txt

└── run_grasp_error.py

 

4.2 核心源码

 

<details>

 

<summary></summary>

 

"""机器人抓取试验数据加载器"""

 

import pandas as pd

from pathlib import Path

from typing import Optional

 

 

class GraspDataLoader:

    """读取抓取试验记录表"""

 

    def __init__(self, filepath: str = "grasp_trials.csv",

                 encoding: str = "utf-8"):

        self.filepath = Path(filepath)

        self.encoding = encoding

 

    def load(self) -> pd.DataFrame:

        if not self.filepath.exists():

            raise FileNotFoundError(self.filepath)

        df = pd.read_csv(self.filepath, encoding=self.encoding)

        req = ["pin_dx", "pin_dy", "pin_gap", "tcp_dx", "tcp_dy", "tcp_dz",

               "posture_rx", "posture_ry", "posture_rz",

               "actual_x", "actual_y", "actual_z",

               "target_x", "target_y", "target_z"]

        miss = [c for c in req if c not in df.columns]

        if miss:

            raise ValueError(f"缺列: {miss}")

        for c in req:

            df[c] = pd.to_numeric(df[c], errors="coerce")

        df = df.dropna(subset=req).reset_index(drop=True)

        # 计算抓取误差(欧氏距离)

        df["grasp_error_mm"] = np.sqrt(

            (df["actual_x"]-df["target_x"])**2 +

            (df["actual_y"]-df["target_y"])**2 +

            (df["actual_z"]-df["target_z"])**2

        ) if "np" else None

        import numpy as _np

        df["grasp_error_mm"] = _np.sqrt(

            (df["actual_x"]-df["target_x"])**2 +

            (df["actual_y"]-df["target_y"])**2 +

            (df["actual_z"]-df["target_z"])**2

        )

        return df

 

</details>

 

<details>

 

<summary></summary>

 

"""位姿变换与误差几何计算 (numpy)"""

 

import numpy as np

import pandas as pd

from typing import Dict

 

 

class PoseTransformer:

    """

    用齐次变换矩阵模拟工装偏差+TCP偏差对放置点的影响

    """

 

    def __init__(self):

        pass

 

    def transform_point(self, target: np.ndarray,

                        pin_dx: float, pin_dy: float, pin_gap: float,

                        tcp_dx: float, tcp_dy: float, tcp_dz: float,

                        rx: float, ry: float, rz: float) -> np.ndarray:

        """target: 理论放置点 [x,y,z]"""

        # 工装偏移(定位销偏差 + 间隙等效)

        pin_effect = np.array([pin_dx + pin_gap*0.5, pin_dy + pin_gap*0.3, 0.0])

        # TCP偏移

        tcp_effect = np.array([tcp_dx, tcp_dy, tcp_dz])

        # 姿态旋转(小角度近似)

        R = self._small_angle_rot(rx, ry, rz)

        # 理论点经旋转后 + 所有偏移

        rotated = R @ target

        actual = rotated + pin_effect + tcp_effect

        return actual

 

    def _small_angle_rot(self, rx, ry, rz):

        """小角度旋转矩阵近似"""

        cx, cy, cz = np.cos([rx,ry,rz]), np.cos([rx,ry,rz]), np.cos([rx,ry,rz])

        sx, sy, sz = np.sin([rx,ry,rz]), np.sin([rx,ry,rz]), np.sin([rx,ry,rz])

        R = np.array([

            [cx*cz, -cx*sz, sx],

            [sz, cz, 0],

            [-sx*cz, sx*sz, cx]

        ])

        return R

 

    def batch_transform(self, df: pd.DataFrame) -> pd.DataFrame:

        """批量计算理论vs实际"""

        results = []

        for _, row in df.iterrows():

            target = np.array([row["target_x"], row["target_y"], row["target_z"]])

            actual_calc = self.transform_point(

                target,

                row["pin_dx"], row["pin_dy"], row["pin_gap"],

                row["tcp_dx"], row["tcp_dy"], row["tcp_dz"],

                row["posture_rx"], row["posture_ry"], row["posture_rz"]

            )

            calc_error = np.linalg.norm(actual_calc - target)

            results.append({

                "calc_actual_x": actual_calc[0],

                "calc_actual_y": actual_calc[1],

                "calc_actual_z": actual_calc[2],

                "calc_error_mm": calc_error,

            })

        return pd.DataFrame(results)

 

</details>

 

<details>

 

<summary></summary>

 

"""蒙特卡洛误差传播 (numpy + scipy)"""

 

import numpy as np

import pandas as pd

from scipy.stats import norm, uniform

from typing import Dict, List

 

 

class MonteCarloPropagator:

    """

    对每个偏差源采样, 传播到最终抓取误差

    """

 

    def __init__(self, n_samples: int = 10000, random_state: int = 42):

        self.n = n_samples

        self.rng = np.random.RandomState(random_state)

 

    def propagate(self, param_ranges: Dict[str, tuple],

                  target_point: np.ndarray = np.array([100.0, 50.0, 20.0])

                  ) -> np.ndarray:

        """

        param_ranges: {param_name: (mean, std)} 正态, 或 (min,max) 均匀

        """

        samples = {}

        for p, rng in param_ranges.items():

            if len(rng) == 2:

                if isinstance(rng[0], (int,float)) and isinstance(rng[1], (int,float)):

                    # 判断正态还是均匀(按约定: std>0为正态)

                    if rng[1] > 0 and rng[1] < rng[0]*0.5:

                        samples[p] = self.rng.normal(rng[0], rng[1], self.n)

                    else:

                        samples[p] = self.rng.uniform(rng[0], rng[1], self.n)

 

        errors = np.zeros(self.n)

        for i in range(self.n):

            pin_dx = samples.get("pin_dx", np.zeros(self.n))[i]

            pin_dy = samples.get("pin_dy", np.zeros(self.n))[i]

            pin_gap = samples.get("pin_gap", np.zeros(self.n))[i]

            tcp_dx = samples.get("tcp_dx", np.zeros(self.n))[i]

            tcp_dy = samples.get("tcp_dy", np.zeros(self.n))[i]

            tcp_dz = samples.get("tcp_dz", np.zeros(self.n))[i]

            rx = samples.get("posture_rx", np.zeros(self.n))[i]

            ry = samples.get("posture_ry", np.zeros(self.n))[i]

            rz = samples.get("posture_rz", np.zeros(self.n))[i]

 

            # 简化变换

            pin_effect = np.array([pin_dx + pin_gap*0.5, pin_dy + pin_gap*0.3, 0.0])

            tcp_effect = np.array([tcp_dx, tcp_dy, tcp_dz])

            # 小角度旋转

            R = np.eye(3) # 简化

            rotated = R @ target_point

            actual = rotated + pin_effect + tcp_effect

            error = np.linalg.norm(actual - target_point)

            errors[i] = error

 

        return errors

 

    def stats(self, errors: np.ndarray) -> Dict:

        return {

            "mean": float(np.mean(errors)),

            "std": float(np.std(errors)),

            "p50": float(np.percentile(errors, 50)),

            "p95": float(np.percentile(errors, 95)),

            "p99": float(np.percentile(errors, 99)),

            "max": float(np.max(errors)),

        }

 

</details>

 

<details>

 

<summary></summary>

 

"""灵敏度分析 (numpy)"""

 

import numpy as np

import pandas as pd

from typing import Dict, List

 

 

class SensitivityAnalyzer:

    """

    一阶灵敏度: 逐一扰动 ±10%, 计算输出变化率

    """

 

    def __init__(self, base_error: float = 0.1):

        self.base = base_error

 

    def analyze(self, param_names: List[str],

                errors_at_plus: Dict[str, float],

                errors_at_minus: Dict[str, float]) -> pd.DataFrame:

        rows = []

        for p in param_names:

            ep = errors_at_plus.get(p, self.base)

            em = errors_at_minus.get(p, self.base)

            # 对称变化率

            sens = (ep - em) / (2 * self.base + 1e-9)

            # 贡献比例

            contribution = abs(ep - self.base) / (self.base + 1e-9)

            rows.append({

                "param": p,

                "error_plus": ep,

                "error_minus": em,

                "sensitivity_index": round(sens, 4),

                "contribution_pct": round(contribution * 100, 1),

            })

        df = pd.DataFrame(rows)

        # 归一化贡献

        total = df["contribution_pct"].sum()

        if total > 0:

            df["contribution_pct"] = df["contribution_pct"] / total * 100

        return df.sort_values("contribution_pct", ascending=False).reset_index(drop=True)

 

</details>

 

<details>

 

<summary></summary>

 

"""抓取误差回归预测 (scikit-learn)"""

 

import numpy as np

import pandas as pd

from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor

from sklearn.svm import SVR

from sklearn.metrics import r2_score, mean_squared_error

from sklearn.model_selection import train_test_split

from typing import Dict, List

 

 

class GraspErrorRegressor:

    EXCLUDE = ["grasp_error_mm", "target_x", "target_y", "target_z",

               "actual_x", "actual_y", "actual_z"]

 

    def __init__(self, random_state: int = 42):

        self.random_state = random_state

        self.models: Dict = {}

        self.metrics = pd.DataFrame()

        self.best = None

        self.feat_cols: List[str] = []

 

    def fit_compare(self, df: pd.DataFrame) -> pd.DataFrame:

        self.feat_cols = [c for c in df.columns if c not in self.EXCLUDE]

        X = df[self.feat_cols].values.astype(float)

        y = df["grasp_error_mm"].values.astype(float)

        Xtr, Xte, ytr, yte = train_test_split(

            X, y, test_size=0.25, random_state=self.random_state)

        specs = {

            "rf": RandomForestRegressor(n_estimators=300, random_state=self.random_state),

            "gbdt": GradientBoostingRegressor(random_state=self.random_state),

            "svr": SVR(kernel="rbf", C=10),

        }

        rows = []

        for name, m in specs.items():

            m.fit(Xtr, ytr)

            pred = m.predict(Xte)

            self.models[name] = m

            rows.append({

                "model": name,

                "r2": round(r2_score(yte, pred), 4),

                "rmse": round(np.sqrt(mean_squared_error(yte, pred)), 4),

            })

        self.metrics = pd.DataFrame(rows).sort_values("rmse").reset_index(drop=True)

        self.best = self.metrics.iloc[0]["model"]

        self._yte = yte

        self._pred = self.models[self.best].predict(Xte)

        return self.metrics

 

    def predict(self, df: pd.DataFrame) -> np.ndarray:

        X = df[self.feat_cols].values.astype(float)

        return self.models[self.best].predict(X)

 

    def importance(self) -> Dict:

        m = self.models[self.best]

        if hasattr(m, "feature_importances_"):

            return dict(zip(self.feat_cols, m.feature_importances_))

        return {c: 0.1 for c in self.feat_cols}

 

</details>

 

<details>

 

<summary></summary>

 

"""误差传递链路图 (networkx)"""

 

import networkx as nx

import pandas as pd

from typing import Dict

 

 

class ErrorFlowGraph:

    """偏差源 → 中间量 → 抓取误差"""

 

    def __init__(self):

        self.G = nx.DiGraph()

 

    def build(self, sens_df: pd.DataFrame, imp: Dict[str, float]) -> nx.DiGraph:

        self.G.clear()

        self.G.add_node("抓取误差", ntype="target")

        self.G.add_node("工装偏移", ntype="mid")

        self.G.add_node("TCP偏移", ntype="mid")

        self.G.add_node("姿态放大", ntype="mid")

 

        # 映射

        mapping = {

            "pin_dx": "工装偏移", "pin_dy": "工装偏移", "pin_gap": "工装偏移",

            "tcp_dx": "TCP偏移", "tcp_dy": "TCP偏移", "tcp_dz": "TCP偏移",

            "posture_rx": "姿态放大", "posture_ry": "姿态放大", "posture_rz": "姿态放大",

        }

        for _, row in sens_df.iterrows():

            p = row["param"]

            mid = mapping.get(p, "工装偏移")

            w = row["contribution_pct"] / 100.0 * 5

            self.G.add_node(p, ntype="source")

            self.G.add_edge(p, mid, weight=w)

 

        # 中间到目标

        for mid in ["工装偏移", "TCP偏移", "姿态放大"]:

            w = sum(d["weight"] for _,_,d in self.G.out_edges(mid, data=True)) if False else 2.0

            self.G.add_edge(mid, "抓取误差", weight=w)

 

        return self.G

 

</details>

 

<details>

 

<summary></summary>

 

"""可视化 (matplotlib + networkx)"""

 

import numpy as np

import pandas as pd

import matplotlib.pyplot as plt

from pathlib import Path

import networkx as nx

 

plt.rcParams["font.sans-serif"] = ["SimHei", "DejaVu Sans"]

plt.rcParams["axes.unicode_minus"] = False

 

 

class GraspVisualizer:

    def __init__(self, results_dir: str = "results"):

        self.results_dir = Path(results_dir)

        self.results_dir.mkdir(exist_ok=True)

 

    def error_scatter(self, y_true, y_pred):

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

        ax.scatter(y_true, y_pred, c="#3498DB", s=15, alpha=0.6)

        lims = [min(min(y_true),min(y_pred)), max(max(y_true),max(y_pred))]

        ax.plot(lims, lims, "r--", lw=1)

        ax.set_xlabel("实测抓取误差 (mm)"); ax.set_ylabel("预测抓取误差 (mm)")

        ax.set_title("抓取误差 实测 vs 预测", fontsize=13, fontweight="bold")

        ax.grid(alpha=0.3)

        plt.tight_layout()

        plt.savefig(self.results_dir/"error_scatter.png", dpi=150, bbox_inches="tight")

        plt.close()

 

    def monte_carlo_cloud(self, errors):

        fig, ax = plt.subplots(figsize=(9,6))

        ax.hist(errors, bins=50, color="#8E44AD", edgecolor="black", alpha=0.7, density=True)

        ax.axvline(np.mean(errors), color="#E74C3C", lw=2, label=f"均值 {np.mean(errors):.3f}")

        ax.axvline(np.percentile(errors,95), color="#F39C12", lw=2, label=f"P95 {np.percentile(errors,95):.3f}")

        ax.set_xlabel("抓取误差 (mm)"); ax.set_ylabel("概率密度")

        ax.set_title("蒙特卡洛误差散布云图", fontsize=13, fontweight="bold")

        ax.legend(); ax.grid(alpha=0.3)

        plt.tight_layout()

        plt.savefig(self.results_dir/"monte_carlo_cloud.png", dpi=150, bbox_inches="tight")

        plt.close()

 

    def sensitivity_bar(self, sens_df):

        top = sens_df.head(10)

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

        ax.barh(top["param"][::-1], top["contribution_pct"][::-1], color="#E74C3C")

        ax.set_xlabel("贡献比例 (%)")

        ax.set_title("误差源灵敏度排名", fontsize=13, fontweight="bold")

        ax.grid(axis="x", alpha=0.3)

        plt.tight_layout()

        plt.savefig(self.results_dir/"sensitivity_bar.png", dpi=150, bbox_inches="tight")

        plt.close()

 

    def pairplot_matrix(self, df, cols=None, max_cols=5):

        if cols is None:

            cols = ["pin_dx","pin_gap","tcp_dx","posture_rz","grasp_error_mm"][:max_cols]

        n = len(cols)

        fig, axes = plt.subplots(n, n, figsize=(4*n, 4*n))

        for i in range(n):

            for j in range(n):

                ax = axes[i,j]

                if i == j:

                    ax.hist(df[cols[i]], bins=20, color="#3498DB", edgecolor="black", alpha=0.7)

                else:

                    ax.scatter(df[cols[j]], df[cols[i]], s=5, c="#16A085", alpha=0.5)

                if i == n-1:

                    ax.set_xlabel(cols[j], fontsize=8)

                if j == 0:

                    ax.set_ylabel(cols[i], fontsize=8)

                ax.tick_params(labelsize=7)

        plt.suptitle("偏差-误差散点矩阵", fontsize=13, fontweight="bold", y=0.98)

        plt.tight_layout()

        plt.savefig(self.results_dir/"pairplot_matrix.png", dpi=150, bbox_inches="tight")

        plt.close()

 

    def error_distribution(self, df):

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

        ax.hist(df["grasp_error_mm"], bins=25, color="#27AE60", edgecolor="black", alpha=0.7)

        ax.axvline(df["grasp_error_mm"].mean(), color="#E74C3C", lw=2, label="均值")

        ax.set_xlabel("抓取误差 (mm)"); ax.set_ylabel("频次")

        ax.set_title("实测抓取误差分布", fontsize=13, fontweight="bold")

        ax.legend(); ax.grid(alpha=0.3)

        plt.tight_layout()

        plt.savefig(self.results_dir/"error_distribution.png", dpi=150, bbox_inches="tight")

        plt.close()

 

    def error_flow_graph(self, G):

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

        pos = nx.spring_layout(G, seed=42, k=0.7)

        cmap = {"target":"#E74C3C","mid":"#F39C12","source":"#3498DB"}

        nc = [cmap[d.get("ntype","source")] for _,d in G.nodes(data=True)]

        nx.draw_networkx_nodes(G, pos, node_color=nc, node_size=1200,

                               edgecolors="black", linewidths=0.5, ax=ax, alpha=0.9)

        ew = [max(0.5, d["weight"]*2) for _,_,d in G.edges(data=True)]

        nx.draw_networkx_edges(G, pos, width=ew, arrows=True, arrowsize=12, ax=ax, 

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

Logo

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

更多推荐