2. 硫化仪数据读取与特征点自动提取

为什么不能直接“看曲线读点”

硫化仪导出的原始文件通常是两列(时间 min、扭矩 dN·m)或多列(含温度、上下模扭矩)的 CSV/TXT,采样间隔常见 0.5~2 s,单条曲线几百到上千点。人工在图上找 t10、t90 既慢又不可复现,尤其当曲线存在启动抖动、扭矩漂移或中途掉点时,肉眼判断会引入 0.2~0.5 min 的偏差,直接影响后续阿伦尼乌斯拟合的可靠性。

本案例的核心是把“读点”变成一套可复现的数值规则:先做曲线清洗,再按标准定义定位特征点,最后用物理合理性条件筛出异常曲线。关键约定如下(全文与代码保持一致):

  • 扭矩单位统一为 dN·m,时间单位 min;
  • MH 取曲线后段稳定平台的中位数,ML 取升温结束后的起始平台中位数,而不是全局最大/最小值——避免尖峰毛刺被误判;
  • t10、t90 定义为扭矩达到 ML + 0.10×(MH−ML) 与 ML + 0.90×(MH−ML) 的时刻,用线性插值求交点;
  • 时间原点为合模加压时刻(导出文件首行时间通常为 0)。
扭矩-时间曲线上的特征点定位规则 时间 t / min 扭矩 M / dN·m ML MH ML+0.10ΔM ML+0.90ΔM t10 t90 后段平台中位数 → MH 起始平台 → ML 异常曲线判定条件 ① ΔM = MH−ML < 0.8  曲线过平,无硫化平台 ② t90 ≤ t10 或 t90 > 60  交点顺序/量程异常 ③ 平台段斜率 > 0.02  未达正硫化,持续爬升 ④ 缺失点 > 5% 或时间倒序  文件损坏/拼接错误 命中任一 → 标记 flag 并保留原值

数据清洗与特征点提取的实现

下面脚本用 pandas 完成批量读取,核心是三个函数:clean_curve 负责去重、排序、剔除明显毛刺;find_plateaus 用分位数确定 ML/MH;extract_features 做插值求 t10/t90 并执行异常判定。示例数据为教学构造,用于验证逻辑,不代表实测结果。

import glob
import numpy as np
import pandas as pd

def clean_curve(df):
    """去重、按时间排序、剔除孤立毛刺点"""
    df = df.dropna(subset=["time", "torque"]).copy()
    df = df.sort_values("time").drop_duplicates("time")
    # 中值滤波式毛刺剔除:偏离邻域中位数过大的点置为 NaN 后插值
    med = df["torque"].rolling(5, center=True, min_periods=3).median()
    df["torque"] = df["torque"].where((df["torque"] - med).abs() < 0.5, med)
    df["torque"] = df["torque"].interpolate()
    return df.reset_index(drop=True)

def find_plateaus(df, head_frac=0.12, tail_frac=0.15):
    """起始平台取前段中位数,后段平台取末段中位数"""
    n = len(df)
    head = df.iloc[:max(3, int(n * head_frac))]
    tail = df.iloc[-max(3, int(n * tail_frac)):]
    ML = float(head["torque"].median())
    MH = float(tail["torque"].median())
    return ML, MH

def cross_time(t, y, level):
    """线性插值求 y 首次达到 level 的时刻"""
    idx = np.where(y >= level)[0]
    if len(idx) == 0:
        return np.nan
    i = idx[0]
    if i == 0:
        return float(t[0])
    y0, y1 = y[i - 1], y[i]
    if y1 == y0:
        return float(t[i])
    return float(t[i - 1] + (level - y0) * (t[i] - t[i - 1]) / (y1 - y0))

def extract_features(path):
    raw = pd.read_csv(path)
    df = clean_curve(raw)
    t = df["time"].to_numpy()
    y = df["torque"].to_numpy()
    ML, MH = find_plateaus(df)
    dM = MH - ML
    t10 = cross_time(t, y, ML + 0.10 * dM)
    t90 = cross_time(t, y, ML + 0.90 * dM)

    # 平台段斜率(末 15% 数据线性拟合)
    tail = df.iloc[-max(3, int(len(df) * 0.15)):]
    slope = np.polyfit(tail["time"], tail["torque"], 1)[0]

    flags = []
    if dM < 0.8: flags.append("flat")
    if not (t10 < t90 <= 60): flags.append("order")
    if abs(slope) > 0.02: flags.append("no_plateau")
    if len(raw) < 0.95 * len(df) or (np.diff(t) <= 0).any(): flags.append("data")

    return {
        "file": path.split("/")[-1],
        "ML": round(ML, 3), "MH": round(MH, 3), "dM": round(dM, 3),
        "t10": round(t10, 2) if t10 == t10 else None,
        "t90": round(t90, 2) if t90 == t90 else None,
        "tail_slope": round(float(slope), 4),
        "flag": ";".join(flags) if flags else "ok",
    }

rows = [extract_features(p) for p in sorted(glob.glob("data/*.csv"))]
result = pd.DataFrame(rows)
result.to_csv("cure_features.csv", index=False)
print(result.to_string(index=False))

结果表与判读

脚本输出一张结构化特征表,每行对应一条曲线。下表为教学示例输出(数据为构造值,用于说明字段含义与判读方式):

fileMLMHdMt10 / mint90 / mintail_slopeflag
170C_A.csv1.8212.4610.640.714.350.003ok
170C_B.csv1.7912.5110.720.684.280.002ok
160C_A.csv1.8512.3810.531.126.940.004ok
150C_A.csv1.8811.9210.041.8611.200.006ok
150C_B.csv1.9012.0510.151.7910.850.005ok
170C_C.csv1.762.310.550.522.100.001flat
160C_B.csv1.8112.4410.631.05—0.031no_plateau
判读要点:同一温度下 A/B 两条曲线的 t90 差异应小于约 5%,若超出则优先怀疑装胶量、模温波动或试样差异;flat 曲线多为胶料失效或装胶不足;no_plateau 说明测试时间不够长,需延长采集窗口后重测,而不是直接拿 t90 去拟合活化能。
两个易错点:① 用全局 max() 取 MH 会被单个尖峰拉高,务必用后段中位数;② 若导出文件时间列单位是秒,需先除以 60 再计算,否则 t90 会整体放大 60 倍,后续阿伦尼乌斯拟合将完全失真。

可复用的特征表字段规范

为了让下一环节的拟合脚本能直接消费,建议固定以下字段并写入文件头注释:file, temperature_C, ML, MH, dM, t10, t90, tail_slope, flag。其中 temperature_C 从文件名或单独的实验记录表关联,flag 为 ok 的行才进入拟合集,异常行保留在表中供追溯。这样一张表同时承担了“特征提取结果”和“数据质量台账”两个作用,避免后续反复回查原始曲线。