python的工业过程控制场景模拟第四十八篇:分析前馈—反馈控制历史数据,量化前馈补偿降低的参数波动幅度。

前馈—反馈控制历史数据分析系统 —— 量化前馈补偿效果的实战工具

"前馈控制的道理谁都懂——扰动还没影响到被控量之前就先动手。但问题是:你怎么向领导证明前馈真的有用?'我感觉有了前馈之后稳多了'——这不是工程师该说的话。你需要的是数据:同样的扰动进来,有前馈和没前馈,被控量的波动幅度到底差了多少?用数字说话。"

—— 哈尔滨工程大学《工业过程控制》课程核心思想延伸

一、实际应用场景描述

在流程工业中,前馈—反馈复合控制是解决可测不可控扰动的标准方案。典型场景如下:

┌──────────────────────────────────────────────┐

│ 换热器温度控制系统 │

│ │

│ 进料流量 F ──→┌──────────────────┐ │

│ (可测扰动) │ 前馈补偿器 FF │──┐ │

│ │ └──────────────────┘ │ │

│ │ ▼ │

│ │ ┌──────────┐ │

│ │ │ 加法器 │ │

│ │ └────┬─────┘ │

│ │ │ │

│ │ ┌──────────┐ │ │

│ │ │ 反馈 PID │◄────────┤ │

│ │ └──────────┘ 偏差 │ │

│ │ │ │ │

│ │ ▼ │ │

│ │ ┌──────────┐ │ │

│ └───────►│ 控制阀 │◄────────┘ │

│ └────┬─────┘ │

│ │ │

│ 出料温度 T ◄────────┘ │

│ (被控量) │

└──────────────────────────────────────────────┘

常见的前馈应用场景

场景 主要扰动 被控量 前馈变量

换热温度控制 进料流量/温度 出料温度 进料流量

锅炉汽包水位 蒸汽负荷 水位 蒸汽流量

精馏塔温度 进料组分 塔顶温度 进料流量+组分

反应器温度 进料温度 反应温度 夹套水温

哈尔滨工程大学《工业过程控制》课程在第七章"复杂控制系统"中系统讲解了前馈控制:

"前馈控制的核心思想是'按扰动大小进行控制',其控制品质优于单纯的反馈控制。但这种优势需要用数据来证明——通过对比相同扰动下有无前馈时的被控量偏差,可以定量评估前馈补偿器的效果,进而指导参数整定。"

二、引入痛点

2.1 现场的真实困境

场景 现场发生了什么 根因

效果说不清 "领导问前馈到底有多大用,我说不上来" 缺乏量化对比方法

参数拍脑袋 "前馈增益设了个 0.5,感觉差不多" 没有基于数据的优化

扰动不重复 "上次那个扰动再来一次,我对比看看"——不可能 扰动不可复现

数据沉睡 "DCS 里存了几年的前馈+反馈数据,没人分析" 缺乏分析工具

整定无依据 "前馈通道的超前滞后时间怎么设?" 没有从历史数据中提取特征

2.2 核心矛盾

前馈控制的效果评估本质上是一个因果推断问题:同一个扰动,如果有前馈和没有前馈,被控量会怎样?但你不能同时拥有两种现实。解决方案是:从历史数据中找到"相似的扰动事件",分别在有前馈作用和无前馈作用的时段进行对比——或者更实际的做法,用反馈偏差数据反推前馈的贡献。

2.3 我们要解决什么

用一段 Python 程序,构建一个前馈—反馈控制历史数据分析系统,实现:

1. 历史数据加载 —— 读取 DCS 导出的 CSV 数据(PV/SP/MV/FF/扰动量)

2. 扰动事件检测 —— 自动识别显著的扰动发生时刻

3. 效果量化对比 —— 计算有/无前馈时的偏差统计量

4. 前馈贡献度评估 —— 用方差缩减率等指标衡量补偿效果

5. 可视化分析 —— 叠加对比曲线 + 统计柱状图

6. 面向对象设计 —— 分层清晰,可扩展

三、核心逻辑讲解

3.1 理论基础:前馈控制的效果评估

本工具基于哈工程《工业过程控制》第七章"复杂控制系统":

① 前馈控制的基本原理

对于可测扰动 D(s) ,前馈补偿器的目标是使扰动对被控量 C(s) 的影响为零:

G_{ff}(s) = -\frac{G_d(s)}{G_p(s)}

其中 G_d(s) 是扰动通道传递函数, G_p(s) 是控制通道传递函数。

② 效果量化指标

指标 公式 含义

偏差方差缩减率 VRR = 1 - \frac{\sigma_{with\_ff}^2}{\sigma_{without\_ff}^2} 前馈消除了多少扰动影响

峰值偏差降低率 $PRR = 1 - \frac{ e_{with_ff}

恢复时间缩短率 TRR = 1 - \frac{t_{settle,with}}{t_{settle,without}} 恢复多快

积分误差降低率 IAER = 1 - \frac{IAE_{with}}{IAE_{without}} 总偏差减少了多少

③ 扰动事件检测

扰动事件 = 扰动变量的变化量超过阈值

AND 变化速率超过阈值

AND 被控量产生相应偏差

④ 虚拟"无前馈"对比

当历史数据中同时存在有前馈和无前馈的时段时,可以直接对比。否则,利用反馈控制器的输出变化来反推:

e_{virtual\_no\_ff} = e_{actual} + G_{ff}^{-1} \cdot u_{ff}

3.2 系统数据流

┌──────────────────────────────────────────────┐

│ DCS 历史数据 CSV │

│ (timestamp, PV, SP, MV, FF, Disturbance) │

└──────────────┬───────────────────────────────┘

┌──────────────▼───────────────┐

│ ① 数据加载 & 预处理 │

│ 对齐时间戳、去异常值 │

└──────────────┬───────────────┘

┌──────────────▼───────────────┐

│ ② 扰动事件检测 │

│ 滑动窗口 + 变化率阈值 │

└──────────────┬───────────────┘

┌──────────────▼───────────────┐

│ ③ 效果量化计算 │

│ 方差缩减 / 峰值降低 / IAE │

└──────────────┬───────────────┘

┌──────────────▼───────────────┐

│ ④ 贡献度评估 │

│ 前馈贡献百分比 │

└──────────────┬───────────────┘

┌──────────────▼───────────────┐

│ ⑤ 可视化 & 报告 │

│ 对比曲线 + 统计摘要 │

└──────────────────────────────┘

四、代码讲解(面向对象设计)

4.1 类结构总览

类名 职责 设计模式

"ControlDataRecord" 单条控制数据记录(dataclass) 值对象

"AnalysisConfig" 分析配置参数(值对象) 值对象

"DisturbanceEvent" 扰动事件(namedtuple) 值对象

"EffectMetrics" 效果量化指标(dataclass) 值对象

"DataLoader" CSV 数据加载与预处理 封装

"DisturbanceDetector" 扰动事件检测器 策略模式

"EffectQuantifier" 效果量化计算器 策略模式

"ContributionAssessor" 前馈贡献度评估器 封装

"TrendVisualizer" 趋势可视化器 封装

"ReportGenerator" 分析报告生成器 模板方法

"FeedforwardAnalysisSystem" 系统编排器(聚合根) 聚合根

4.2 数据模型层

from dataclasses import dataclass, field

from typing import List, Dict, Optional, Tuple, NamedTuple

from enum import Enum

import numpy as np

import csv

from pathlib import Path

from datetime import datetime, timedelta

from collections import defaultdict

class ControlMode(Enum):

"""控制模式"""

FEEDFORWARD_ACTIVE = "前馈投入"

FEEDFORWARD_BYPASS = "前馈旁路"

FEEDBACK_ONLY = "纯反馈"

class DisturbanceType(Enum):

"""扰动类型"""

STEP = "阶跃"

RAMP = "斜坡"

PULSE = "脉冲"

FLUCTUATION = "波动"

@dataclass(frozen=True)

class ControlDataRecord:

"""单条控制数据记录 —— 值对象"""

timestamp: datetime

pv: float # 过程变量 (被控量)

sp: float # 设定值

mv: float # 控制器输出 (总)

ff_output: float = 0.0 # 前馈输出分量

fb_output: float = 0.0 # 反馈输出分量

disturbance: float = 0.0 # 可测扰动量

mode: ControlMode = ControlMode.FEEDFORWARD_ACTIVE

@dataclass(frozen=True)

class AnalysisConfig:

"""分析配置"""

disturbance_threshold: float = 5.0 # 扰动检测阈值

disturbance_rate_threshold: float = 2.0 # 变化率阈值 (%/min)

settling_band: float = 0.02 # 调节时间判定带 (±2%)

min_event_duration: float = 30.0 # 最短事件持续 (s)

comparison_window: float = 300.0 # 对比窗口 (s)

sampling_interval: float = 1.0 # 采样间隔 (s)

class DisturbanceEvent(NamedTuple):

"""扰动事件"""

start_time: datetime

end_time: datetime

peak_disturbance: float

disturbance_type: DisturbanceType

max_pv_deviation_with_ff: float

max_pv_deviation_virtual_no_ff: float

@dataclass

class EffectMetrics:

"""效果量化指标"""

variance_reduction_rate: float = 0.0 # 方差缩减率

peak_reduction_rate: float = 0.0 # 峰值降低率

iae_reduction_rate: float = 0.0 # IAE降低率

settling_time_reduction: float = 0.0 # 恢复时间缩短

contribution_percentage: float = 0.0 # 前馈贡献度

4.3 数据加载器

class DataLoader:

"""

控制历史数据加载器

CSV 格式:

timestamp,pv,sp,mv,ff_output,fb_output,disturbance,mode

2024-06-01 08:00:00,150.2,150.0,45.5,12.3,33.2,120.5,FF_ACTIVE

...

支持:

- 多列对齐

- 缺失值插值

- 模式标记解析

"""

def __init__(self):

self.records: List[ControlDataRecord] = []

def load_csv(self, file_path: str) -> List[ControlDataRecord]:

"""从 CSV 加载数据"""

self.records.clear()

with open(file_path, 'r', encoding='utf-8') as f:

reader = csv.DictReader(f)

for row in reader:

try:

ts = datetime.strptime(row['timestamp'], "%Y-%m-%d %H:%M:%S")

except ValueError:

continue

mode_str = row.get('mode', 'FF_ACTIVE').strip().upper()

mode = self._parse_mode(mode_str)

record = ControlDataRecord(

timestamp=ts,

pv=float(row.get('pv', 0)),

sp=float(row.get('sp', 0)),

mv=float(row.get('mv', 0)),

ff_output=float(row.get('ff_output', 0)),

fb_output=float(row.get('fb_output', 0)),

disturbance=float(row.get('disturbance', 0)),

mode=mode

)

self.records.append(record)

return self.records

def _parse_mode(self, mode_str: str) -> ControlMode:

"""解析控制模式"""

mapping = {

'FF_ACTIVE': ControlMode.FEEDFORWARD_ACTIVE,

'FF_BYPASS': ControlMode.FEEDFORWARD_BYPASS,

'FB_ONLY': ControlMode.FEEDBACK_ONLY

}

return mapping.get(mode_str, ControlMode.FEEDFORWARD_ACTIVE)

def split_by_mode(self, records: List[ControlDataRecord]) -> Dict[ControlMode, List[ControlDataRecord]]:

"""按控制模式分组"""

groups = defaultdict(list)

for r in records:

groups[r.mode].append(r)

return dict(groups)

4.4 扰动事件检测器

class DisturbanceDetector:

"""

扰动事件检测器 —— 滑动窗口 + 变化率阈值

检测逻辑:

1. 计算扰动变量的差分序列

2. 滑动窗口内变化量超过阈值 → 候选事件

3. 合并相邻候选事件

4. 计算对应的 PV 偏差

"""

def __init__(self, config: AnalysisConfig):

self.cfg = config

def detect(self, records: List[ControlDataRecord]) -> List[DisturbanceEvent]:

"""

检测所有显著扰动事件

Args:

records: 控制数据记录

Returns:

扰动事件列表

"""

if len(records) < 2:

return []

events = []

i = 0

while i < len(records) - 1:

# 计算变化率 (%/min)

delta_d = records[i+1].disturbance - records[i].disturbance

rate = abs(delta_d) / self.cfg.sampling_interval * 60.0

if abs(delta_d) > self.cfg.disturbance_threshold and rate > self.cfg.disturbance_rate_threshold:

# 找到事件起点

start_idx = i

peak_d = records[i].disturbance

max_dev_with_ff = abs(records[i].pv - records[i].sp)

# 向前扩展寻找真正的起点

while start_idx > 0:

prev_delta = abs(records[start_idx].disturbance - records[start_idx-1].disturbance)

if prev_delta < self.cfg.disturbance_threshold / 2:

break

start_idx -= 1

# 向后追踪直到扰动结束

j = i + 1

while j < len(records):

peak_d = max(peak_d, records[j].disturbance)

dev = abs(records[j].pv - records[j].sp)

max_dev_with_ff = max(max_dev_with_ff, dev)

if j > i + 10: # 至少追踪一段时间

recent_change = abs(records[j].disturbance - records[j-5].disturbance)

if recent_change < self.cfg.disturbance_threshold / 5:

break

j += 1

end_idx = min(j, len(records) - 1)

# 判断扰动类型

d_type = self._classify_disturbance(records[start_idx:end_idx+1])

event = DisturbanceEvent(

start_time=records[start_idx].timestamp,

end_time=records[end_idx].timestamp,

peak_disturbance=peak_d,

disturbance_type=d_type,

max_pv_deviation_with_ff=max_dev_with_ff,

max_pv_deviation_virtual_no_ff=max_dev_with_ff * 1.5 # 简化估计

)

events.append(event)

i = j + 1

else:

i += 1

return events

def _classify_disturbance(self, segment: List[ControlDataRecord]) -> DisturbanceType:

"""分类扰动类型"""

if len(segment) < 3:

return DisturbanceType.FLUCTUATION

disturbances = [r.disturbance for r in segment]

diffs = np.diff(disturbances)

# 阶跃: 大部分变化集中在前几个点

if np.std(diffs[:5]) < 0.1 * np.mean(np.abs(diffs)):

return DisturbanceType.STEP

# 斜坡: 变化率相对稳定

elif np.std(diffs) < 0.3 * np.mean(np.abs(diffs)):

return DisturbanceType.RAMP

# 脉冲: 先增后减

elif disturbances[-1] - disturbances[0] < 0.2 * max(disturbances):

return DisturbanceType.PULSE

else:

return DisturbanceType.FLUCTUATION

4.5 效果量化计算器(核心算法)

class EffectQuantifier:

"""

前馈效果量化计算器 —— 策略模式

核心算法:

1. 对比同一扰动事件在有/无前馈时的 PV 偏差

2. 计算方差缩减率、峰值降低率、IAE降低率

"""

def __init__(self, config: AnalysisConfig):

self.cfg = config

def quantify(self, records: List[ControlDataRecord],

events: List[DisturbanceEvent]) -> EffectMetrics:

"""

量化前馈控制效果

Args:

records: 控制数据记录

events: 检测到的扰动事件

Returns:

效果指标

"""

if not events or not records:

return EffectMetrics()

# 收集所有事件窗口内的偏差数据

deviations_with_ff = []

deviations_virtual_no_ff = []

for event in events:

# 找到事件对应的数据段

event_records = [r for r in records

if event.start_time <= r.timestamp <= event.end_time]

for r in event_records:

err_with = abs(r.pv - r.sp)

# 虚拟无前馈偏差 = 实际偏差 + 前馈输出对应的等效偏差

# 简化: 假设过程增益为 1

err_virtual_no = err_with + abs(r.ff_output) * 0.5

deviations_with_ff.append(err_with)

deviations_virtual_no_ff.append(err_virtual_no)

deviations_with_ff = np.array(deviations_with_ff)

deviations_virtual_no_ff = np.array(deviations_virtual_no_ff)

# 方差缩减率

var_with = np.var(deviations_with_ff)

var_without = np.var(deviations_virtual_no_ff)

vrr = 1.0 - var_with / (var_without + 1e-10)

# 峰值降低率

peak_with = np.max(deviations_with_ff)

peak_without = np.max(deviations_virtual_no_ff)

prr = 1.0 - peak_with / (peak_without + 1e-10)

# IAE降低率

iae_with = np.sum(deviations_with_ff) * self.cfg.sampling_interval

iae_without = np.sum(deviations_virtual_no_ff) * self.cfg.sampling_interval

iaer = 1.0 - iae_with / (iae_without + 1e-10)

return EffectMetrics(

variance_reduction_rate=round(vrr * 100, 2),

peak_reduction_rate=round(prr * 100, 2),

iae_reduction_rate=round(iaer * 100, 2),

settling_time_reduction=round(0.0, 2), # 需要更复杂的计算

contribution_percentage=round(vrr * 100, 2)

)

def compare_periods(self, ff_records: List[ControlDataRecord],

no_ff_records: List[ControlDataRecord]) -> EffectMetrics:

"""

对比前馈投入和旁路两个时段的效果

Args:

ff_records: 前馈投入时段数据

no_ff_records: 前馈旁路时段数据

Returns:

效果指标

"""

if not ff_records or not no_ff_records:

return EffectMetrics()

# 提取偏差序列

err_ff = np.array([abs(r.pv - r.sp) for r in ff_records])

err_no_ff = np.array([abs(r.pv - r.sp) for r in no_ff_records])

# 确保长度一致(取较短的)

min_len = min(len(err_ff), len(err_no_ff))

err_ff = err_ff[:min_len]

err_no_ff = err_no_ff[:min_len]

# 方差缩减率

var_ff = np.var(err_ff)

var_no_ff = np.var(err_no_ff)

vrr = 1.0 - var_ff / (var_no_ff + 1e-10)

# 峰值降低率

peak_ff = np.max(err_ff)

peak_no_ff = np.max(err_no_ff)

prr = 1.0 - peak_ff / (peak_no_ff + 1e-10)

# IAE降低率

iae_ff = np.sum(err_ff) * self.cfg.sampling_interval

iae_no_ff = np.sum(err_no_ff) * self.cfg.sampling_interval

iaer = 1.0 - iae_ff / (iae_no_ff + 1e-10)

return EffectMetrics(

variance_reduction_rate=round(vrr * 100, 2),

peak_reduction_rate=round(prr * 100, 2),

iae_reduction_rate=round(iaer * 100, 2),

contribution_percentage=round(vrr * 100, 2)

)

4.6 前馈贡献度评估器

class ContributionAssessor:

"""

前馈贡献度评估器

评估前馈补偿器在整个控制中的贡献比例:

Contribution = Var(FF component) / Var(Total MV)

理想情况下:

- 前馈贡献高 → 大部分扰动在被控量变化前已被补偿

- 反馈贡献高 → 残余偏差由 PID 消除

"""

def assess(self, records: List[ControlDataRecord]) -> dict:

"""

评估前馈贡献

Args:

records: 控制数据记录

Returns:

贡献分析结果

"""

if not records:

return {}

ff_outputs = np.array([r.ff_output for r in records])

fb_outputs = np.array([r.fb_output for r in records])

total_mv = np.array([r.mv for r in records])

# 方差贡献

var_ff = np.var(ff_outputs)

var_fb = np.var(fb_outputs)

var_total = np.var(total_mv)

# 能量占比

energy_ff = np.sum(ff_outputs ** 2)

energy_fb = np.sum(fb_outputs ** 2)

energy_total = energy_ff + energy_fb

ff_energy_ratio = energy_ff / (energy_total + 1e-10) * 100

fb_energy_ratio = energy_fb / (energy_total + 1e-10) * 100

# 前馈利用率 (非零输出的比例)

ff_active_ratio = np.sum(np.abs(ff_outputs) > 0.1) / len(records) * 100

return {

'ff_variance_contribution': round(var_ff / (var_total + 1e-10) * 100, 2),

'fb_variance_contribution': round(var_fb / (var_total + 1e-10) * 100, 2),

'ff_energy_ratio': round(ff_energy_ratio, 2),

'fb_energy_ratio': round(fb_energy_ratio, 2),

'ff_active_ratio': round(ff_active_ratio, 2),

'avg_ff_output': round(np.mean(np.abs(ff_outputs)), 2),

'avg_fb_output': round(np.mean(np.abs(fb_outputs)), 2)

}

4.7 趋势可视化器

class TrendVisualizer:

"""

趋势可视化器

生成:

- PV vs SP 对比曲线

- 扰动变量曲线

- 前馈/反馈输出分量

- 偏差对比直方图

"""

def plot_comparison(self, records: List[ControlDataRecord],

events: List[DisturbanceEvent],

output_path: str = "ff_analysis.png"):

"""绘制综合分析图"""

try:

import matplotlib.pyplot as plt

import matplotlib.dates as mdates

fig, axes = plt.subplots(4, 1, figsize=(14, 10), sharex=True)

times = [r.timestamp for r in records]

pv = [r.pv for r in records]

sp = [r.sp for r in records]

dist = [r.disturbance for r in records]

ff = [r.ff_output for r in records]

fb = [r.fb_output for r in records]

err = [r.pv - r.sp for r in records]

# 子图1: PV vs SP

axes[0].plot(times, pv, 'b-', label='PV', linewidth=1)

axes[0].plot(times, sp, 'r--', label='SP', linewidth=1)

axes[0].fill_between(times, sp, pv, alpha=0.2, color='gray', label='Deviation')

axes[0].set_ylabel('PV / SP')

axes[0].legend()

axes[0].grid(True, alpha=0.3)

# 子图2: 扰动变量

axes[1].plot(times, dist, 'g-', linewidth=1)

axes[1].set_ylabel('Disturbance')

axes[1].grid(True, alpha=0.3)

# 子图3: 前馈 vs 反馈输出

axes[2].plot(times, ff, 'c-', label='FF Output', linewidth=1)

axes[2].plot(times, fb, 'm-', label='FB Output', linewidth=1)

axes[2].plot(times, [f + b for f, b in zip(ff, fb)], 'k--', label='Total MV', linewidth=0.8)

axes[2].set_ylabel('Controller Output')

axes[2].legend()

axes[2].grid(True, alpha=0.3)

# 子图4: 偏差

axes[3].plot(times, err, 'r-', linewidth=1)

axes[3].axhline(y=0, color='k', linestyle='-', linewidth=0.5)

axes[3].set_ylabel('Error (PV-SP)')

axes[3].set_xlabel('Time')

axes[3].grid(True, alpha=0.3)

# 标记扰动事件

for event in events[:5]: # 最多标记5个

for ax in axes:

ax.axvspan(event.start_tim

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