摘要

本文中的MFC指质量流量控制器,英文为Mass Flow Controller。MFC重复性测试的基本方法,是在气体、入口压力、出口压力、温度、安装姿态、预热时间和参考仪器均保持规定状态时,对同一台MFC的同一设定点进行多次独立测量,并统计独立参考流量的离散程度。

测试不能只重复读取MFC自身反馈值。应使用经过校准、量程和动态能力合适的独立参考流量标准。升流量和降流量数据需要分组统计,否则迟滞会混入重复性。建议至少报告样本数、均值、样本标准差、极差、相对于满量程的标准差,以及稳定窗口、循环顺序和异常值处理规则。

本文提供一套仅依赖Python标准库的离线分析代码。代码读取每个稳定步骤的汇总CSV,输出各流量点和方向的重复性指标,并附带确定性的教学自检数据。自检结果不是MFC产品实测数据,也不构成规格或验收结论。

1. MFC重复性到底测什么

重复性描述的是在规定重复性条件下,同一测量对象多次测量结果的一致程度。对质量流量控制器而言,重复性关注相同设定点、相同方向和相同测试边界下,独立参考流量多次结果的离散程度。

“重复性好”不等于“准确度高”。如果所有结果都稳定地偏离设定值,离散程度可能很小,但平均误差仍然较大。因此报告必须把离散性指标和平均误差分开。

1.1 重复性与准确度

准确度关注测量结果与参考值或约定真值的接近程度。工程报告通常使用带定义的误差指标表达,而不是给“准确度”分配一个没有条件的数字。重复性则关注多次结果之间的离散。

1.2 重复性与迟滞

迟滞比较同一设定点在上升序列和下降序列中的差异。若把两个方向直接合并计算标准差,方向差异会放大离散度,使结果同时包含迟滞和重复性。

1.3 重复性与短期复现性

改变操作人员、装夹、日期、气路重连或仪器配置后,测试条件已经发生变化,通常不再是最严格意义下的同条件重复。报告应明确哪些因素保持不变,哪些因素被有意改变。

2. MFC重复性指标怎么定义

指标 定义 用途与边界
样本数n 同一设定点、同一方向下的有效重复次数 样本太少时,标准差估计不稳定
参考流量均值 该组独立参考流量的算术平均 用于计算平均误差,不代表单次结果
样本标准差 以n减1为分母估计组内离散程度 必须与单位、样本数和条件一起报告
极差 组内最大参考流量减去最小参考流量 直观但容易受单个极端值影响
标准差占满量程比例 样本标准差除以MFC满量程,再乘100百分比 便于跨设定点比较,但必须说明满量程
平均误差占满量程比例 参考流量均值减设定值,再除以满量程 属于平均误差,不是重复性
方向差占满量程比例 下降序列均值减上升序列均值,再除以满量程 用于提示迟滞,不应混入重复性结论

3. MFC重复性测试系统怎么搭建

MFC重复性测试链路。气路顺序应根据参考仪器要求和实际压力边界确认

3.1 独立参考流量标准

参考仪器的量程、不确定度、气体适用性、压力条件和动态响应应覆盖被测范围。参考标准自身的不确定度和短期波动会进入测试结果,因此不能仅凭被测MFC反馈值证明实际流量重复性。

3.2 压力、温度和气体条件

入口压力和出口压力会改变控制阀工作点,温度和气体种类会影响热式测量及气体换算。每个循环都应保存这些条件,而不是只在报告开头写一次名义值。

3.3 稳定判据

到达设定点后,应使用预先定义的稳定判据和稳定窗口计算参考流量均值。固定等待时间并不自动等于稳定;若系统尚在漂移,窗口均值会受到循环时序影响。

4. 多流量点与升降序列怎么设计

单个升降循环示意
实际设定点、保持时间和循环次数由规格或测试计划确定。

  1. 按制造商要求完成预热、零点检查、气路吹扫和泄漏检查。
  2. 定义测试气体、满量程、设定点、入口压力、出口压力、温度和安装方向。
  3. 按低到高顺序执行上升序列,在每个点满足稳定判据后计算窗口均值。
  4. 按高到低顺序执行下降序列,并使用相同稳定判据。
  5. 将一个完整升降过程定义为一个循环,重复规定次数。
  6. 分别按设定点和方向分组,计算重复性;再单独计算两个方向的均值差。

设定点数量和重复次数应由产品规格、客户验收文件或实验设计确定。本文自检采用3个设定点和5个循环,只用于验证代码,不是通用验收方案。

5. 软件、硬件与数据条件

项目 本文代码与实际测试要求
操作系统 Windows 11 64位;其他支持Python 3.12的平台可运行
Python Python 3.12,仅使用csv、json、statistics等标准库
输入数据 每个稳定步骤一行,包含cycle_id、direction、setpoint_sccm、reference_sccm
可选记录 气体、温度、入口绝对压力、出口绝对压力、MFC反馈和原始时间序列路径
输出 mfc_repeatability_summary.json和mfc_repeatability_groups.csv
代码边界 不连接真实MFC、参考仪器、阀门或安全联锁
真实硬件条件 由MFC、参考标准、压力传感器、温度记录、气路和数据采集系统构成

6. Python重复性统计代码

代码要求每一行已经是稳定窗口均值,而不是原始高速采样点。它按设定点和方向分组,计算样本标准差、极差和满量程比例,并把升降方向均值差单独输出。

from __future__ import annotations

import argparse
import csv
import json
import math
import statistics
from collections import defaultdict
from pathlib import Path

REQUIRED_COLUMNS = {
    "cycle_id",
    "direction",
    "setpoint_sccm",
    "reference_sccm",
}

def read_rows(path: Path, full_scale_sccm: float) -> list[dict[str, object]]:
    rows: list[dict[str, object]] = []
    seen: set[tuple[str, str, float]] = set()
    with path.open("r", encoding="utf-8-sig", newline="") as handle:
        reader = csv.DictReader(handle)
        if reader.fieldnames is None:
            raise ValueError("CSV has no header")
        missing = REQUIRED_COLUMNS.difference(reader.fieldnames)
        if missing:
            raise ValueError(f"missing columns: {sorted(missing)}")

        for line_number, raw in enumerate(reader, start=2):
            cycle_id = raw["cycle_id"].strip()
            direction = raw["direction"].strip().lower()
            if not cycle_id:
                raise ValueError(f"empty cycle_id at line {line_number}")
            if direction not in {"up", "down"}:
                raise ValueError(f"direction must be up or down at line {line_number}")
            try:
                setpoint = float(raw["setpoint_sccm"])
                reference = float(raw["reference_sccm"])
            except (TypeError, ValueError) as exc:
                raise ValueError(f"invalid number at line {line_number}") from exc
            if not math.isfinite(setpoint) or not math.isfinite(reference):
                raise ValueError(f"non-finite value at line {line_number}")
            if setpoint < 0.0 or setpoint > full_scale_sccm:
                raise ValueError(f"setpoint outside full scale at line {line_number}")

            key = (cycle_id, direction, setpoint)
            if key in seen:
                raise ValueError(f"duplicate cycle, direction and setpoint at line {line_number}")
            seen.add(key)
            rows.append({
                "cycle_id": cycle_id,
                "direction": direction,
                "setpoint_sccm": setpoint,
                "reference_sccm": reference,
            })

    if not rows:
        raise ValueError("CSV contains no data rows")
    return rows

def create_self_test(path: Path) -> None:
    # Deterministic teaching data only. These are not product measurements.
    full_scale = 100.0
    setpoints = [10.0, 50.0, 100.0]
    repeat_offsets = [-0.08, 0.02, 0.04, -0.03, 0.05]
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8", newline="") as handle:
        writer = csv.writer(handle)
        writer.writerow([
            "cycle_id",
            "direction",
            "setpoint_sccm",
            "reference_sccm",
        ])
        for cycle_index, offset in enumerate(repeat_offsets, start=1):
            for direction in ("up", "down"):
                direction_offset = 0.05 if direction == "down" else 0.0
                for setpoint in setpoints:
                    reference = setpoint + 0.002 * setpoint + direction_offset + offset
                    writer.writerow([
                        f"C{cycle_index}",
                        direction,
                        f"{setpoint:.3f}",
                        f"{reference:.6f}",
                    ])

def round_value(value: float | None) -> float | None:
    return None if value is None else round(value, 9)

def summarize(
    rows: list[dict[str, object]],
    full_scale_sccm: float,
) -> dict[str, object]:
    grouped: dict[tuple[float, str], list[float]] = defaultdict(list)
    for row in rows:
        key = (float(row["setpoint_sccm"]), str(row["direction"]))
        grouped[key].append(float(row["reference_sccm"]))

    groups: list[dict[str, object]] = []
    means: dict[tuple[float, str], float] = {}
    warnings: list[str] = []
    for (setpoint, direction), values in sorted(grouped.items()):
        count = len(values)
        mean_value = statistics.fmean(values)
        sample_std = statistics.stdev(values) if count >= 2 else None
        value_range = max(values) - min(values)
        if count < 3:
            warnings.append(
                f"{setpoint:g} sccm {direction} has fewer than 3 repeats"
            )
        means[(setpoint, direction)] = mean_value
        groups.append({
            "setpoint_sccm": setpoint,
            "direction": direction,
            "count": count,
            "reference_mean_sccm": round_value(mean_value),
            "sample_std_sccm": round_value(sample_std),
            "range_sccm": round_value(value_range),
            "sample_std_percent_full_scale": round_value(
                None if sample_std is None else 100.0 * sample_std / full_scale_sccm
            ),
            "range_percent_full_scale": round_value(
                100.0 * value_range / full_scale_sccm
            ),
            "mean_error_percent_full_scale": round_value(
                100.0 * (mean_value - setpoint) / full_scale_sccm
            ),
        })

    direction_differences: list[dict[str, float]] = []
    setpoints = sorted({key[0] for key in means})
    for setpoint in setpoints:
        up_key = (setpoint, "up")
        down_key = (setpoint, "down")
        if up_key in means and down_key in means:
            difference = means[down_key] - means[up_key]
            direction_differences.append({
                "setpoint_sccm": setpoint,
                "down_minus_up_sccm": round(difference, 9),
                "down_minus_up_percent_full_scale": round(
                    100.0 * difference / full_scale_sccm, 9
                ),
            })

    return {
        "data_status": "statistics from supplied stable-window means",
        "full_scale_sccm": full_scale_sccm,
        "grouping": "setpoint_sccm and direction",
        "groups": groups,
        "direction_differences": direction_differences,
        "warnings": warnings,
        "interpretation_boundary": (
            "Repeatability does not replace accuracy, uncertainty, "
            "hysteresis, long-term drift, or gas-condition validation."
        ),
    }

def write_group_csv(path: Path, groups: list[dict[str, object]]) -> None:
    fieldnames = list(groups[0].keys())
    with path.open("w", encoding="utf-8", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(groups)

def main() -> None:
    parser = argparse.ArgumentParser(
        description="Analyze MFC repeatability from stable-window means."
    )
    source = parser.add_mutually_exclusive_group(required=True)
    source.add_argument("--input", type=Path)
    source.add_argument("--self-test", action="store_true")
    parser.add_argument("--full-scale-sccm", type=float, default=None)
    parser.add_argument("--output-dir", type=Path, default=Path("."))
    args = parser.parse_args()

    args.output_dir.mkdir(parents=True, exist_ok=True)
    if args.self_test:
        full_scale = 100.0
        input_path = args.output_dir / "mfc_repeatability_self_test.csv"
        create_self_test(input_path)
    else:
        if args.full_scale_sccm is None or args.full_scale_sccm <= 0.0:
            raise ValueError("--full-scale-sccm must be positive for real data")
        full_scale = args.full_scale_sccm
        input_path = args.input

    rows = read_rows(input_path, full_scale)
    summary = summarize(rows, full_scale)
    write_group_csv(
        args.output_dir / "mfc_repeatability_groups.csv",
        list(summary["groups"]),
    )
    with (args.output_dir / "mfc_repeatability_summary.json").open(
        "w", encoding="utf-8"
    ) as handle:
        json.dump(summary, handle, ensure_ascii=False, indent=2)
    print(json.dumps(summary, ensure_ascii=False, indent=2))

if __name__ == "__main__":
    main()

6.1 运行自检

python mfc_repeatability_analyzer.py --self-test --output-dir self_test_output

6.2 分析真实数据

python mfc_repeatability_analyzer.py --input actual_mfc_repeatability.csv --full-scale-sccm 100 --output-dir result

命令中的100 sccm只是格式示例,必须替换为被测MFC在该气体和标定条件下的实际满量程。输入文件格式如下:

cycle_id,direction,setpoint_sccm,reference_sccm
C1,up,10.000,10.012
C1,down,10.000,10.061
C2,up,10.000,10.018

7. 自检数据和结果解释

本文代码在Python 3.12.13环境完成离线自检。自检使用确定性的教学数据,不使用随机数,也没有连接真实MFC或参考流量仪。
多流量点升降循环时序

项目 自检条件或结果 数据状态
满量程 100 sccm 教学参数
设定点 10、50、100 sccm 教学参数
循环与分组 5个循环,升降方向分别统计,共6组 已运行验证
各组样本标准差 约0.054314 sccm,即约0.054314百分比满量程 理论自检结果
各组极差 0.13 sccm,即0.13百分比满量程 理论自检结果
下降均值减上升均值 0.05 sccm,即0.05百分比满量程 人为加入的教学方向差
产品性能 没有真实硬件、气体或仪器数据 未测试

这些数值只能验证分组、样本标准差、极差和方向差计算是否一致。它们不能用于评价任何MFC型号,也不能与供应商规格直接比较。

8. 真实测试的数据质量检查

8.1 一个步骤只保留一个稳定窗口均值

本文代码期望每个循环、方向和设定点只有一行。若原始文件包含连续时间序列,应先保存原始数据,再根据统一稳定判据生成步骤均值表。不要从波动曲线中手工挑选最好的一段。

8.2 升降方向分开统计

方向必须来自预先定义的序列,不应根据结果大小事后修改。若只做单方向重复测试,报告应明确没有评估方向差或迟滞。

8.3 异常值不能静默删除

气源中断、压力超限、通信丢失或参考仪器过载等无效试次可以按预先规则剔除,但必须保存原因和原始记录。仅因为结果偏离均值而删除数据,会低估离散程度。
重复性、准确度与迟滞区别

8.4 环境漂移需要单独观察

如果结果随循环编号持续单向变化,可能存在预热不足、零点漂移、温度变化、压力漂移或污染。此时一个标准差无法解释全部问题,应同时绘制结果随时间或循环编号的趋势。

8.5 参考标准的不确定度不能忽略

被测结果的离散包含MFC、参考仪器、压力控制、温度和数据处理共同贡献。需要形成可用于验收的结论时,应建立测量模型和不确定度预算,而不是把参考仪器读数当作无误差真值。

9. 常见问题

9.1 MFC重复性测试至少做几次

没有脱离标准、规格和统计目标的统一次数。计算样本标准差至少需要两个结果,但工程测试通常需要更多重复。本文代码少于3次会给出警告,正式次数应由测试计划确定。

9.2 能直接用MFC反馈值计算重复性吗

可以分析电子反馈信号自身的重复读数,但不能自动证明真实质量流量重复性。验证实际流量需要独立、可追溯且适配测试条件的参考流量标准。

9.3 上升和下降数据为什么不能合并

两个方向可能存在迟滞或气路状态差异。合并后计算的离散同时包含组内重复性和方向差,难以定位问题。

9.4 重复性很好是否代表MFC准确

不代表。稳定偏差会产生很好的重复性和较差的平均误差。两类指标必须分别报告。

9.5 低流量点为什么不建议只看百分比读数

流量接近零时,以读数为分母会放大相对百分比。应同时报告sccm等绝对单位、满量程比例、零点稳定性和参考仪器低端能力。

9.6 等待固定时间能否代替稳定判据

不能自动替代。固定等待时间只有在已验证系统能在该时间内稳定时才有意义。更稳妥的方法是同时规定等待下限、稳定误差带和持续窗口。

9.7 不同气体的重复性结果可以直接比较吗

通常不能直接比较。气体热物性、密度、转换方法、压力和参考标准能力都可能不同。比较时必须说明标定气体和实际测试气体。

9.8 一次测试能作为长期稳定性结论吗

不能。短期重复性不包含长期漂移、维护、污染、温度循环和批次差异。长期稳定性需要跨时间的独立测试方案。
MFC重复性数据处理流程

10. 结论

MFC重复性测试应围绕“同一条件下多次参考流量结果有多分散”来设计。核心步骤是使用独立参考流量标准、固定气体与压力边界、预先定义稳定窗口、重复完整升降循环,并按设定点和方向分组统计。

样本标准差和极差用于描述组内离散,平均误差属于准确度问题,升降方向均值差用于提示迟滞。三者不能混成一个“重复精度”。本文Python代码适合分析稳定窗口汇总数据,但不能替代原始时间序列、仪器校准、不确定度评估和气体安全控制。

参考资料

  1. SEMI E56,Test Method for Determining Accuracy, Linearity, Repeatability, Short-Term Reproducibility, Hysteresis, and Dead Band of Thermal Mass Flow Controllers,SEMI International Standards。官方检索:https://store-us.semi.org/search?q=SEMI%20E56
  2. ISO 14511:2019,Measurement of fluid flow in closed conduits - Thermal mass flowmeters,International Organization for Standardization。官方检索:https://www.iso.org/search.html?q=ISO%2014511
  3. JCGM 200:2012,International Vocabulary of Metrology - Basic and General Concepts and Associated Terms,Joint Committee for Guides in Metrology。https://www.bipm.org/documents/20126/2071204/JCGM_200_2012.pdf
  4. Barry N. Taylor、Chris E. Kuyatt,Guidelines for Evaluating and Expressing the Uncertainty of NIST Measurement Results,NIST Technical Note 1297,1994。https://doi.org/10.6028/NIST.TN.1297
  5. Python Software Foundation,statistics - Mathematical statistics functions,Python 3.12文档。https://docs.python.org/3.12/library/statistics.html

说明:标准可能需要购买,并可能存在更新版本。正式测试应以企业当前采用的标准、客户验收文件、MFC规格书和参考仪器手册为准。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐