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)。
数据清洗与特征点提取的实现
下面脚本用 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))
结果表与判读
脚本输出一张结构化特征表,每行对应一条曲线。下表为教学示例输出(数据为构造值,用于说明字段含义与判读方式):
| file | ML | MH | dM | t10 / min | t90 / min | tail_slope | flag |
|---|---|---|---|---|---|---|---|
| 170C_A.csv | 1.82 | 12.46 | 10.64 | 0.71 | 4.35 | 0.003 | ok |
| 170C_B.csv | 1.79 | 12.51 | 10.72 | 0.68 | 4.28 | 0.002 | ok |
| 160C_A.csv | 1.85 | 12.38 | 10.53 | 1.12 | 6.94 | 0.004 | ok |
| 150C_A.csv | 1.88 | 11.92 | 10.04 | 1.86 | 11.20 | 0.006 | ok |
| 150C_B.csv | 1.90 | 12.05 | 10.15 | 1.79 | 10.85 | 0.005 | ok |
| 170C_C.csv | 1.76 | 2.31 | 0.55 | 0.52 | 2.10 | 0.001 | flat |
| 160C_B.csv | 1.81 | 12.44 | 10.63 | 1.05 | — | 0.031 | no_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 的行才进入拟合集,异常行保留在表中供追溯。这样一张表同时承担了“特征提取结果”和“数据质量台账”两个作用,避免后续反复回查原始曲线。