MFC质量流量控制器重复性怎么测试?多流量点、升降序列与Python统计代码
摘要
本文中的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重复性测试系统怎么搭建

3.1 独立参考流量标准
参考仪器的量程、不确定度、气体适用性、压力条件和动态响应应覆盖被测范围。参考标准自身的不确定度和短期波动会进入测试结果,因此不能仅凭被测MFC反馈值证明实际流量重复性。
3.2 压力、温度和气体条件
入口压力和出口压力会改变控制阀工作点,温度和气体种类会影响热式测量及气体换算。每个循环都应保存这些条件,而不是只在报告开头写一次名义值。
3.3 稳定判据
到达设定点后,应使用预先定义的稳定判据和稳定窗口计算参考流量均值。固定等待时间并不自动等于稳定;若系统尚在漂移,窗口均值会受到循环时序影响。
4. 多流量点与升降序列怎么设计

实际设定点、保持时间和循环次数由规格或测试计划确定。
- 按制造商要求完成预热、零点检查、气路吹扫和泄漏检查。
- 定义测试气体、满量程、设定点、入口压力、出口压力、温度和安装方向。
- 按低到高顺序执行上升序列,在每个点满足稳定判据后计算窗口均值。
- 按高到低顺序执行下降序列,并使用相同稳定判据。
- 将一个完整升降过程定义为一个循环,重复规定次数。
- 分别按设定点和方向分组,计算重复性;再单独计算两个方向的均值差。
设定点数量和重复次数应由产品规格、客户验收文件或实验设计确定。本文自检采用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 一次测试能作为长期稳定性结论吗
不能。短期重复性不包含长期漂移、维护、污染、温度循环和批次差异。长期稳定性需要跨时间的独立测试方案。
10. 结论
MFC重复性测试应围绕“同一条件下多次参考流量结果有多分散”来设计。核心步骤是使用独立参考流量标准、固定气体与压力边界、预先定义稳定窗口、重复完整升降循环,并按设定点和方向分组统计。
样本标准差和极差用于描述组内离散,平均误差属于准确度问题,升降方向均值差用于提示迟滞。三者不能混成一个“重复精度”。本文Python代码适合分析稳定窗口汇总数据,但不能替代原始时间序列、仪器校准、不确定度评估和气体安全控制。
参考资料
- 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
- 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
- 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
- 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
- Python Software Foundation,statistics - Mathematical statistics functions,Python 3.12文档。https://docs.python.org/3.12/library/statistics.html
说明:标准可能需要购买,并可能存在更新版本。正式测试应以企业当前采用的标准、客户验收文件、MFC规格书和参考仪器手册为准。
更多推荐



所有评论(0)