fix: 将测试文件中 "" include 改为 <> 风格,统一项目规范

This commit is contained in:
Huamonarch 2026-05-15 15:22:04 +08:00
parent 3f10d4dd9f
commit 4032ff92ef
9 changed files with 3096 additions and 4 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,486 @@
# eqpalg 算法模板功能阐述文档
> 基于源码阅读2026-05-15
---
## 一、架构总览
### 继承层次
```
AlgBase ← 所有算法基类(数据获取、报警发送、线程调度)
├── ExpBase ← 表达式驱动算法基类(表达式求值、反馈状态机、统计学习)
│ ├── ExpTimes ← 运行时间累计 / 出现次数累计Alg 6/7
│ ├── ExpSample2D ← 多项式拟合 / 皮尔逊相关系数Alg 12/13
│ ├── ExpBound ← 数据超限幅值Alg 17
│ ├── Roller2 ← 同组离群监测Alg 9
│ └── Roller3 ← 多变量离群检测Alg 16/18
├── TrendSlope ← 斜率监控 v1已废弃build_algorithm 未使用)
├── TrendSlope2 ← 斜率监控 v2Alg 8绝对变化量
├── TrendSlope3 ← 斜率监控 v3Alg 14百分比变化率
├── Roller ← 负载平衡/离群检测 v1已废弃build_algorithm 未使用)
├── FaultCode ← 故障代码解析Alg 10/11
├── GlitchDetection ← 毛刺检测Alg 15
└── Null ← 空算法(未实现算法的占位符)
```
### 算法 ID 与类映射build_algorithm.cpp
| Alg ID | 类 | exp_type | 功能 |
|--------|-----|----------|------|
| 1 | ExpBase | 1 | 实时逻辑判断 |
| 2 | ExpBase | 2 | 监控变量-上下限 |
| 3 | ExpBase | 3 | 动作反馈-逻辑判断 |
| 4 | ExpBase | 4 | 动作反馈-上下限 |
| 5 | ExpBase | 5 | 监控变量-上下限-持续 |
| 6 | ExpTimes | 6 | 运行时间累计 |
| 7 | ExpTimes | 7 | 出现次数累计 |
| 8 | TrendSlope2 | - | 斜率监控(绝对变化量) |
| 9 | Roller2 | -1 | 同组离群监测 |
| 10 | FaultCode | 0 | 故障代码1整体解析 |
| 11 | FaultCode | 1 | 故障代码2按位解析 |
| 12 | ExpSample2D | 12 | 多项式拟合 |
| 13 | ExpSample2D | 13 | 线性相关性(皮尔逊) |
| 14 | TrendSlope3 | - | 斜率监控(百分比变化率) |
| 15 | GlitchDetection | - | 毛刺检测 |
| 16 | Roller3 | 16 | 多变量离群检测(百分比偏差) |
| 17 | ExpBound | 17 | 数据超限幅值 |
| 18 | Roller3 | 18 | 多变量离群检测(绝对值偏差) |
### 三种执行进程
每个算法可在三种上下文中运行:
- **mon** — 实时监控进程(~20ms 周期),从共享内存或 iHyperDB 获取实时数据,执行 `exec_mon()` 产生报警
- **task** — 按需历史回测进程,从 iHyperDB 获取指定时间范围数据,执行 `exec_task()` 逐时间步长分析
- **cron** — 定时统计学习进程,周期性收集 mon 进程累积的样本数据,执行 DAA::STA 分布统计并写入 DB2
---
## 二、各算法详细阐述
### 2.1 ExpBaseAlg 1-5— 表达式驱动算法
**继承关系**`AlgBase` → `ExpBase`
**核心能力**
- 表达式引擎:通过 `mix_cc::matheval::Expression` 解析和执行用户配置的数学表达式
- 变量系统:`mm_vars` 映射管理所有变量tag值、pv历史值、s快照值、mv2累积值等
- 动作反馈状态机:支持带反馈的监控流程(开始→保持→结束→超时)
- 统计学习DAA::STA 分布统计cron 进程定期学习数据分布区间)
- 双数据源共享内存DataSource::MEMORY=1或 iHyperDBDataSource::IHDB=0
**五种 exp_type算法模板**
#### Alg 1 — 实时逻辑判断
- 评估前提表达式 `exp_act_`,若结果为 true 则立即报警
- 无上下限,无统计学习,无反馈流程
- 报警内容来自配置的 `output.error`
#### Alg 2 — 监控变量-上下限
- 评估 `exp_act_` 获得当前值,与 [limit_down_, limit_up_] 比较
- 支持自学习模式:`is_learning_=true` 时将数据喂入 EqpStat → DAA::STA 分布统计
- 支持数据筛选表达式 `exp_feedback_`filter_exp仅满足时才参与统计
- 支持三种检测模式:
- Default双侧检测value < limit_down value > limit_up
- OnlyLeft仅左边界-32768 哨兵值表示无左边界 → 仅检测 > limit_up
- OnlyRight仅右边界32768/32767 哨兵值 → 仅检测 < limit_down
- cron 进程定期将累积样本写入 DB2 的 T_SAMPLE_STAT / T_SAMPLE_MAG
#### Alg 3 — 动作反馈-逻辑判断
- 包含完整反馈状态机:
1. `act_start_done()`:前提条件满足时,记录开始时间 stime快照各 tag 值s[n]、mx_tag、mi_tag初始化 mv2/up/dw 累积变量
2. `act_not_hold()`若配置了保持模式keep_mode前提条件不满足时退出
3. `act_done()`:反馈条件满足时,计算结果表达式 `exp_result_`,判断是否报警
4. `act_timeout()`:超时时重置累积变量
- 无上下限检测,报警基于结果表达式的布尔值或数值
#### Alg 4 — 动作反馈-上下限
- 结合 Alg 2 和 Alg 3反馈模式下动作结束时评估结果值并检查上下限
- 支持 `m_timemode`:若结果表达式含 "time" 且 exp_type 为 CondBound报警信息以 ms 为单位
- 支持自学习统计
#### Alg 5 — 监控变量-上下限-持续
- 类似 Alg 2但增加 `hold_time_` 参数
- 前提条件满足且值超限后,需持续超限超过 hold_time_ 才报警
- 若值在 hold_time_ 内恢复正常,重置计时
- 报警后将计时器重置
**关键逻辑细节**
1. **变量刷新**
- 共享内存模式:每次执行时将当前 tag 值存入 mm_vars["tagN"],旧值存入 mm_vars["pN"]前一周期值pv 历史pvN_0~pvN_5循环移位
- iHyperDB 模式:遍历 queried_data_ 每行数据,逐行设置变量后调用 mon_proc
2. **首次填充first_fill_mm_vars**
- 将所有 pv 历史值pvN_0~pvN_5初始化为当前值避免程序冷启动时差值计算异常
- 共享内存模式直接读当前值iHyperDB 模式查询最近 10 秒数据
3. **hold(n,T) 函数**:表达式支持 `hold_N_HE` 子串,解析后在指定时间 T 分钟内保持 tag 值,用于防抖
---
### 2.2 ExpBoundAlg 17— 数据超限幅值
**继承关系**`AlgBase` → `ExpBase``ExpBound`
**功能描述**
- 监控单个表达式值是否超过预定义阈值
- 支持两级报警Warning 和 Error
- 配置中指定 `limit_warn`(警告阈值)和 `limit_error`(报警阈值)
**执行流程mon_proc**
1. 检查前提条件表达式 `exp_feedback_`filter_exp不满足直接返回无报警
2. 评估主表达式 `exp_act_` 获取当前值
3. 若当前值 > limit_warn_ → 报警
- 若 > limit_error_ → ERROR 级别
- 否则 → WARN 级别
**初始化特殊性**
- `limit_down_` 被设为哨兵值 -32768实际仅检测上限
- 初始化时检查 `limit_warn_ > limit_error_`,若不符合则标记配置错误
---
### 2.3 ExpTimesAlg 6/7— 时间/次数累计
**继承关系**`AlgBase` → `ExpBase``ExpTimes`
**功能描述**
- **Alg 6HoldTimeAcc— 运行时间累计**
- 前提条件满足时开始计时,不满足时停止计时
- 累计运行时间(单位:小时),`running_time += time/3600000`ms→h 换算)
- 若单次持续超过 `rw_time_`默认10分钟自动分段写入防止数据丢失
- 阈值单位limit_time小时
- **Alg 7OccTimesAcc— 出现次数累计**
- 每次前提条件满足时累加 1
- 阈值单位limit_times
**持久化机制**
- mon 进程:通过 `AsyncDbWorker` 异步投递到后台线程写入 DB2T_RULE_SAMPLE_1D 表),不阻塞 20ms 主循环
- rw_time_ 控制写入周期默认10分钟防止频繁 I/O
- 同时更新共享内存 RuleStatShm 供 UI 读取
- 支持防溢出:累计值接近 `unsigned long::max()` / `double::max()` 时停止累计
**初始化恢复**
- 从 DB2 T_RULE_SAMPLE_1D 表读取上次持久化的累计值,实现进程重启后数据恢复
---
### 2.4 TrendSlope2Alg 8— 斜率监控(绝对变化量)
**继承关系**`AlgBase` → `TrendSlope2`
**功能描述**
- 监控单个 tag 在多个等间隔时间窗口内的均值变化趋势
- 检测是否存在连续 N 次的斜率超限
**配置参数**
- `interval_time`:查询均值的窗口时长(分钟,转为秒)
- `deltaX`:时间步长(分钟,转为秒)
- `CS_AVG_SIZE`:连续检测次数
- `diff`:斜率阈值(绝对变化量)
- `need_tag`:指定监控的 tag格式 "tagN",解析为列索引)
**执行流程exec_mon**
1. 固定回退 60 秒:`now_time_ = system_clock::now() - 60s`
2. 从 `now_time_ - deltaX * CS_AVG_SIZE` 开始,共查询 CS_AVG_SIZE+1 个窗口的均值
3. 逐对计算相邻窗口间的斜率:`f_slope ≈ (avg[i] - avg[i-1])`四舍五入到3位小数
4. 若任一对斜率未超限 → 立即退出,不报警(所有对必须连续超限)
5. 若连续 CS_AVG_SIZE 对全部超限 → 报警
**与原始 TrendSlope 的差异**
- 原始 TrendSlope固定6个窗口需3次连续超限
- TrendSlope2可配置窗口数和连续次数但要求 ALL 连续超限(更严格)
**存在的潜在问题**
- ⚠️ `exec_mon()` 首行 `now_time_ = system_clock::now() - 60s` 会覆盖 `exec_task()` 循环中设置的 `this->now_time_`,导致 task 模式的时间参数被忽略
---
### 2.5 TrendSlope3Alg 14— 斜率监控(百分比变化率)
**继承关系**`AlgBase` → `TrendSlope3`
**功能描述**
- 与 TrendSlope2 结构几乎相同,核心差异在于斜率计算方式
- 使用**百分比变化率**而非绝对变化量
**与 TrendSlope2 的关键差异**
1. 斜率比较方式:
- TrendSlope2直接比较 `f_slope > limit_slope_`(绝对量)
- TrendSlope3比较 `f_slope > abs(limit_slope_ * avg[i-1] / 100)`limit_slope_ % × 前值)
2. f_slope_max 计算:
- TrendSlope2取绝对斜率最大值
- TrendSlope3取百分比变化最大值 `100 * f_slope / avg[i-1]`若前值为0则取绝对斜率
**存在的潜在问题**
- ⚠️ 同样存在 `now_time_ = system_clock::now() - 60s` 覆盖 task 时间的问题
---
### 2.6 RollerAlg 9 v1已废弃— 负载平衡/离群检测
**继承关系**`AlgBase` → `Roller`
**状态**`build_algorithm.cpp` 中 Alg 9 已映射到 Roller2此算法仅保留源码未使用
**原功能**
- 查询多个 tag 在 interval_time 内的均值
- 计算去头尾均值(去掉最大值和最小值后的均值)
- 迭代寻找偏离均值超过 error_diff_% 的 tag
- 每次迭代只标记并移除一个最异常的 tag继续检测剩余 tag
---
### 2.7 Roller2Alg 9— 同组离群监测
**继承关系**`AlgBase` → `ExpBase``Roller2`
**功能描述**
- 对多组表达式X1~X9最多9个分别求值检测是否有值偏离组均值超过阈值
- 先评估前提表达式 pre_exp不满足则跳过本轮检测
**执行流程mon_proc**
1. 调用 `refresh_var_result()` 评估所有表达式
2. 若 pre_exp 条件不满足,直接返回
3. 计算所有 X 值的均值:`avg = sum(X_values) / Xsize`
4. 动态计算上下限:`[avg - |avg| * limit_over_, avg + |avg| * limit_over_]`
5. 遍历每个 X 值,若有超限则报警(指出具体哪个变量异常)
**配置**
- limit_over_百分比阈值从配置的百分数除以100转为小数
- 每个表达式可配置 name中文名称报警消息中包含
---
### 2.8 Roller3Alg 16/18— 多变量离群检测
**继承关系**`AlgBase` → `ExpBase``Roller3`
**功能描述**
- 对多个 tag 的实时值进行中位数离群检测
- 支持两种模式百分比偏差Alg 16和绝对值偏差Alg 18/OuterAct
**执行流程mon_proc**
1. 检查前提条件 `exp_act_`pre_exp不满足则返回
2. 从 mm_vars 读取各 tag 的绝对值
3. 计算中位数 median_
4. 根据 exp_type 计算允许偏差:
- Alg 16百分比`deviation = |median| * limit_% * 0.01`
- Alg 18绝对值`deviation = limit_value`(直接使用 limit 值)
5. 计算每个 tag 值与中位数的偏差
6. 找到最大偏差的 tag
**报警逻辑无持续时间要求hold_time ≤ delay_time**
- 检测最大偏差的 tag 值是否超出 [median - deviationWarn, median + deviationWarn]
- 超出 warn 阈值 → WARN 级别
- 超出 error 阈值 → ERROR 级别
**报警逻辑(有持续时间要求)**
- 为每个 tag 维护状态机last_alarm_state, last_start_time
- 连续超限且持续时间 > hold_time_ → 报警
- 若超限方向(上升/下降)改变,重新计时
**工具函数**
- `extractTagNumbers(expr)`:从 "tag1+tag2+…" 字符串中提取 tag 序号
- `calculateMedian(data)`:排序后取中位数
- `findMaxWithIndex(vec)`:返回最大值及其下标
- `get_up_down(value, is_up)`:基于 detect_mode 修正上下限(处理哨兵值)
- `detect_up_down(value)`:判断值的超限状态
---
### 2.9 FaultCodeAlg 10/11— 故障代码解析
**继承关系**`AlgBase` → `FaultCode`
**功能描述**
- 从共享内存读取故障代码整数值,查表转换为可读的故障名称和描述
- 两种解析模式:
**Alg 10code_type=0— 整体解析**
- 将故障代码作为整体在 map2fcode_ 中查找
- 若找到且启用is_usable返回故障名称+描述
- 若故障代码为 0 或不在表中 → 不报警
**Alg 11code_type=1— 按位解析**
- 对故障代码的 bit 0~15 逐位检查
- 每个置位且启用的 bit 对应的故障信息追加到报警消息
- 只要有任一启用的 bit 置位即报警
**初始化**
- 从 DB2 T_LOV_FCODE 表加载故障代码映射表(按 code_type 过滤)
- 若查询失败或为空 → is_valid_=false后续 mon 不执行
---
### 2.10 GlitchDetectionAlg 15— 毛刺检测
**继承关系**`AlgBase` → `GlitchDetection`
**功能描述**
- 累积指定长度的数据序列dataX 表达式值),满后发送至 Python 分析模块进行毛刺检测
- 本身不做任何报警判断,报警由 Python 端生成
**执行流程exec_mon**
1. 每次调用将当前 dataX 值写入 `data_[data_index_]`,索引递增
2. 当 `data_index_ >= data_size_`(数据收集完成):
- 将数据通过 ProxPyPython代理插入共享缓存
- 通过 ProxPy 发送 JSON 参数ruleid、时间范围、报警内容、glitch_per 等)到 Python 的 "glitch" 处理函数
- 重置 data_index_=0开始新一轮收集
**前提条件处理**
- get_prr() 重写:若前提条件不满足,重置 data_index_ 和起始时间(丢弃已收集的部分数据)
**配置参数**
- dataX监控变量的表达式
- lenth数据序列长度钳制在 [100, MAXLEN]
---
### 2.11 ExpSample2DAlg 12/13— 二维样本分析
**继承关系**`AlgBase` → `ExpBase``ExpSample2D`
**功能描述**
#### Alg 12PolyFit— 多项式拟合
- **task 进程**:从 iHyperDB 查询历史数据,收集样本 (X, Y),使用 DAA::LSM 最小二乘法进行多项式拟合1~9阶选出最优拟合阶数将拟合系数和样本存入 DB2
- **mon 进程**:代入当前 X 值使用拟合系数计算预测 Y_Fit若实际 Y 偏离 Y_Fit 超过 scale_% → 报警
#### Alg 13PEAR— 皮尔逊相关系数
- **task 进程**:同 PolyFit样本收集后计算 Pearson 相关系数,存储到 DB2
- **mon 进程**
- 累积样本直到达到 min_len_1000~200000
- 样本量不足时:前提条件满足则追加样本
- 样本量足够后:计算当前 Pearson 相关系数,与从 DB2 加载的基准 pear_coefs_ 比较,偏差超过 scale_% → 报警
- 计算后清空样本重新累积
**PearValue 函数细节**
- 使用 Eigen 向量化计算
- 计算 σ_x, σ_y, σ_xy
- ⚠️ 离散度检查:若 `σ_x/m1 > 1``σ_y/m2 > 1` → 返回正常相关系数;否则返回 2无效值。即要求变异系数 CV > 1标准差超过均值才认为数据有效。此阈值可能过于严格。
---
### 2.12 TrendSlopev1已废弃
**继承关系**`AlgBase` → `TrendSlope`
**状态**`build_algorithm.cpp` 中未使用Alg 8 已映射到 TrendSlope2
**原功能**
- 固定6个窗口CS_AVG_SIZE=6需连续3次斜率超限才报警
- 使用 iHyperDB 的 HD3_STATS_TYPE_ARITH_MEAN 直接获取均值(无需查询原始数据)
- 斜率按绝对变化量比较
---
### 2.13 Null — 空算法
**继承关系**`AlgBase` → `Null`
**功能**:所有虚函数返回空值/0用于未实现算法的占位符
---
## 三、关键公共机制
### 3.1 动作反馈状态机ExpBase 内)
适用于 Alg 3/4 及所有继承 ExpBase 且 feedback_mode_=true 的算法:
```
前提条件满足(act_triggered_)
→ act_start_done(): 记录 stime, 快照 tagN→sN, 初始化 mv2/up/dw 变量
→ 持续监控中...
→ act_not_hold(): keep_mode=1且前提条件不满足 → 退出
→ act_timeout(): 超时 → 重置, 可选报警
→ act_done(): 反馈条件满足 → 计算结果表达式, 判断超限/报警
```
关键变量及其含义(用于表达式中):
- `tagN`第N个tag当前值
- `pN`第N个tag上一周期值
- `sN`动作开始时刻第N个tag值快照
- `stime`动作开始时间epoch ms
- `time`动作已持续时间ms`now - stime`
- `now`当前时间epoch ms
- `etime`动作结束时间ms
- `mx_tagN`:动作期间 tagN 最大值
- `mi_tagN`:动作期间 tagN 最小值
- `mv2_tagN`:动作期间 tagN==1 的累计次数(上升沿/布尔累计)
- `mv2_pN`:动作期间 pN==1 的累计次数
- `up_tagN`:动作期间 pN==0 且 tagN==1 的次数
- `dw_tagN`:动作期间 pN==1 且 tagN==0 的次数
- `pvN_0~pvN_5`tagN 最近6个周期的历史值
### 3.2 统计学习DAA::STA
适用于 Alg 2/4/5有上下限且启用自学习的算法
- **mon 进程**:每次执行将当前值通过 `SingletonTemp<EqpStat>` 写入共享内存累积
- **cron 进程**:周期性读取累积值,喂入 `DAA::STA` 分布对象:
- 首次(未初始化):计算数据范围,用 `range/STA_SIZE_MIN` 初始化分布
- 后续:逐条 `dist_add()`
- 完成后 `store_db2()` 持久化
- **自学习区间更新**`reload_ci_dist()` 根据 dist_mode
- 0手动不自动更新使用配置的固定上下限
- 1在线从 T_RULE_FEATURE 表读取在线学习结果
- 2离线从 T_SAMPLE_MAG 表读取离线分析结果
### 3.3 数据来源
- **DataSource::MEMORY1**:从共享内存 `GlobaltemSharedMemory` 实时读取 tag 值
- **DataSource::IHDB0**:从 iHyperDB 查询历史/实时数据
- 查询使用 `interval_time` 周期窗口
- 支持 delay_time 补偿(对齐数据到达延迟)
- TrendSlope 系列固定使用 IHDB 数据源
### 3.4 前提条件PRR
- 所有算法继承自 AlgBase 的 PRR 机制
- `prr_` 字段1=有条件0=无条件
- 条件不满足时算法跳过本次执行exec_mon 在 AlgBase 层即返回)
---
## 四、发现的潜在问题
### 4.1 ⚠️ TrendSlope2/TrendSlope3 exec_mon 中的硬编码时间偏移
**位置**`trend_slope2.cpp:90`, `trend_slope3.cpp:84`
```cpp
now_time_ = system_clock::now() - 60s;
```
此行在 `exec_mon()` 首行执行,会覆盖 `exec_task()` 循环中设置的 `this->now_time_`。导致 task 模式的逐时间步长遍历实际上每次都使用实时系统时间减60秒历史数据的 task 回测可能不准确。
### 4.2 ⚠️ ExpSample2D::PearValue 的离散度检查可能过于严格
**位置**`exp_sample2D.cc:274`
```cpp
if (dis1 > 1 || dis2 > 1) {
return sigma12 / sqrt(sigma1 * sigma2); // 正常计算
} else {
return 2; // 标记为无效
}
```
只有当变异系数 CV > 1标准差 > 均值时才返回有效相关系数。对于很多实际场景CV < 1 的数据可能仍有有意义的相关系数此逻辑可能导致大量有效样本被丢弃
### 4.3 Roller3 中 exp_result_ 可能未被使用
**位置**`roller3.cpp:323-335`
`init_X_exp()` 中将 `tags_exp_`(如 "tag1+tag2+tag3")编译为 `exp_result_`,但 `mon_proc()` 中并未调用 `exp_result_->evaluate()`,而是直接从 `mm_vars` 读取 tag 值。此表达式创建后被闲置。
### 4.4 Roller2 init_X_exp 中 feedback_mode_ 设置
**位置**`roller2.cpp:123`
循环中每次迭代设置 `feedback_mode_ = true`,但 Roller2 不使用 ExpBase 的反馈状态机,此字段对其行为无影响,但属于语义不准确的标记。
### 4.5 TrendSlope v1 / Roller v1 保留但未使用
`trend_slope.cpp/h``roller.cpp/h` 仍存在于源码中,但 `build_algorithm.cpp` 已将 Alg 8/9 映射到 v2 版本。这些文件可能可以标记为废弃或移入 .do_not_use。
### 4.6 TrendSlope2 报警条件比 TrendSlope 更严格
- TrendSlope6次检查中至少3次连续超限即可报警
- TrendSlope2要求配置的 CS_AVG_SIZE 次检查**全部**连续超限(任一不满足即退出且不报警)
这可能导致 TrendSlope2 对短暂波动过于敏感(?)—— 实际上,由于要求所有检查都超限,它对偶发波动是**欠敏感**的。这种差异可能是预期的设计意图,但值得确认。

View File

@ -0,0 +1,415 @@
# ExpBase 设计分析与优化方案
> 2026-05-15
---
## 一、当前设计问题诊断
### 1.1 量化数据
| 指标 | 数值 | 健康标准 |
|------|------|----------|
| 总代码行数(.h + .cpp | 1956 | < 500 |
| `exp_type_` 类型码分支 | 16 处 | 0应用多态消除 |
| 布尔状态标志 | 12 个 | < 5 |
| `mon_proc()` 方法行数 | ~200 行 | < 50 |
| 方法总数 | 40+ | < 20 |
| 开关语句层级深度 | 3 层exp_type → feedback_mode → exp_type | < 2 |
### 1.2 核心设计问题
#### 问题一God Class上帝类
ExpBase 承担了至少 **7 项不同职责**
```
┌─────────────────────────────────────────────────────────┐
│ ExpBase │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────┐ │
│ │ 变量管理 │ │ 表达式求值 │ │ 配置加载/校验 │ │
│ │ mm_vars/pv/ │ │ exp_act_ │ │ reload_config_*│ │
│ │ hold/s/mv2等 │ │ exp_feedback_│ │ init() 分支 │ │
│ └──────────────┘ │ exp_result_ │ └────────────────┘ │
│ └──────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────┐ │
│ │ 反馈状态机 │ │ 超限检测 │ │ 数据获取 │ │
│ │ act_start/ │ │ detect_up_ │ │ MEMORY vs IHDB │ │
│ │ done/timeout │ │ down() │ │ refresh_* │ │
│ │ /not_hold │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 统计学习 & 持久化 │ │
│ │ cron_proc() / task_base_proc() / DAA::STA │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
#### 问题二:类型码代替多态
`exp_type_` 字段驱动了 16 处条件分支,这是典型的"switch-on-type-code"反模式。以 `mon_proc()` 为例,其控制流可以画成:
```
mon_proc()
├── Bound/BoundHoldTime? → 筛选+统计
├── feedback_mode_?
│ ├── act_start_done()? → return
│ ├── act_not_hold()? → reset, return
│ ├── act_done()?
│ │ ├── CondBound? → 上下限报警
│ │ └── else → 布尔报警
│ └── act_timeout()? → timeout报警
└── else (无反馈)
├── Bound? → 上下限报警
├── BoundHoldTime? → 持续时间+上下限报警
└── else → 布尔报警
```
5 种算法类型共享一个方法,通过 exp_type_ + feedback_mode_ 两个标志组合出 5 条路径。新增一种算法类型就要修改这个核心方法。
#### 问题三:子类继承不需要的复杂度
```
ExpBase (1956行)
├── ExpTimes 使用: 表达式求值,不使用: 反馈状态机/上下限/统计学习cron
├── ExpSample2D 使用: 表达式求值,不使用: 反馈状态机/上下限用自己的fit逻辑
├── ExpBound 使用: 表达式求值,不使用: 反馈状态机/上下限用自己的bound逻辑
├── Roller2 使用: 表达式求值,不使用: 反馈状态机/上下限/统计学习
└── Roller3 使用: 表达式求值,不使用: 反馈状态机用自己的median逻辑
```
每个子类都继承了 12 个布尔状态标志、40+ 个方法,但实际只用到其中一小部分。
#### 问题四:状态标志过多且隐含耦合
12 个布尔标志之间存在隐含的状态转换关系,但没有显式的状态机:
```
feedback_mode_, keep_mode_, feedback_done_, act_started_, act_triggered_,
m_timemode, is_learning_, filter_flag_, is_fun_vars_need_reset_,
exp_is_wrong_, exp_wrong_is_alarmed_, is_running_
```
这些标志分散在各处被读写,难以追踪状态转换的全貌。
---
## 二、优化方案
### 2.1 总体策略:分层解耦 + 策略模式
核心思路:将 ExpBase 的 7 项职责拆分为独立的类,然后用策略模式消除类型码分支。
### 2.2 新架构
```
┌─────────────────┐
│ AlgBase │
│ (数据获取/报警) │
└────────┬────────┘
┌────────┴────────┐
│ ExpBase │ ← 薄抽象层,仅组合各组件
│ (模板方法) │
└────────┬────────┘
┌──────────────────┼──────────────────┐
│ │ │
┌────────┴────────┐ ┌──────┴──────┐ ┌────────┴────────┐
│ LogicAlg │ │ BoundAlg │ │ FeedbackAlg │
│ (Alg 1) │ │ (Alg 2/5) │ │ (Alg 3/4) │
└─────────────────┘ └─────────────┘ └─────────────────┘
组件层(组合到 ExpBase 中):
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ VarManager │ │ BoundChecker │ │ FbStateMachine│ │ StatCollector│
│ 变量生命周期 │ │ 上下限检测 │ │ 反馈状态机 │ │ 统计学习 │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
```
### 2.3 组件详细设计
#### 组件一VarManager — 变量管理器
**职责**:变量 map 的初始化、刷新、历史值移位、hold 变量刷新
```cpp
// 约 150 行
class VarManager {
public:
// 从共享内存刷新所有变量
void refreshFromMemory(const std::vector<std::string>& tags);
// 从 iHyperDB 行数据刷新
void refreshFromIhdRow(int row, const Eigen::MatrixXd& data,
const std::vector<TimePoint>& times);
// 首次填充(冷启动)
void firstFill(const std::vector<std::string>& tags, int dataSource);
// hold(n,T) 变量刷新
void refreshHoldVars();
// 变量访问
double& operator[](const std::string& key);
std::map<std::string, double>& vars();
// 快照:记录动作开始时刻的变量状态
void snapshot(size_t tagCount);
// 更新动作期间的极值/累积变量
void updateActionVars(size_t tagCount);
private:
std::map<std::string, double> mm_vars_;
std::vector<std::string> pv_keys_; // pvN_0..pvN_5
std::vector<std::string> tag_keys_; // tagN, pN
std::map<std::string, std::unique_ptr<HoldTime>> hold_times_;
VarsCache var_cache_;
size_t pv_num_ = 6;
};
```
**收益**
- 消除 ExpBase 中 ~200 行的 `refresh_exp_vars_mem()` / `refresh_exp_vars_ihd()` / `first_fill_mm_vars()`
- 变量命名规则tagN, pN, pvN_M, sN, mx_tagN 等)封装在一处
- 可独立单元测试
#### 组件二BoundChecker — 上下限检测器
**职责**:上下限比较、检测模式判断
```cpp
// 约 60 行
class BoundChecker {
public:
void setLimits(double down, double up);
void setDetectMode(DetectMode mode);
// 判断 value 是否超限
bool isOutOfBounds(double value) const;
// 获取报警消息(包含合理区间描述)
std::string buildAlarmMsg(double value, const std::string& unit,
const std::string& errorStr) const;
double limitDown() const;
double limitUp() const;
DetectMode detectMode() const;
private:
double limit_down_ = -32768;
double limit_up_ = -32768;
DetectMode detect_mode_ = DetectMode::Default;
};
```
**收益**
- `detect_up_down()` 逻辑从 ExpBase 抽离
- Roller3 自己的 `detect_up_down()` 可以直接复用
- 哨兵值 -32768/32767/32768 的处理集中管理
#### 组件三FbStateMachine — 反馈状态机
**职责**:动作反馈的四阶段状态转换
```cpp
// 约 120 行
enum class FbState { Idle, Started, Done, Timeout, NotHold };
class FbStateMachine {
public:
// 配置
void configure(bool keepMode, TimeDur timeout, bool timeMode);
// 状态转换(每个周期调用)
FbState update(bool actTriggered, bool feedbackCondition,
TimePoint now, VarManager& vars);
// 查询
bool isActive() const;
TimePoint startTime() const;
TimeDur elapsed() const;
// 重置
void reset();
private:
bool keep_mode_ = false;
TimeDur timeout_ = 10min;
bool time_mode_ = false;
bool started_ = false;
TimePoint start_time_;
};
```
**收益**
- `act_start_done()` / `act_done()` / `act_timeout()` / `act_not_hold()` 四合一
- 状态转换显式化,可画状态图验证
- 消除 4 个布尔标志:`act_started_`, `act_triggered_`, `feedback_triggered_`, `feedback_done_`
- 可独立单元测试(模拟时间推进)
#### 组件四StatCollector — 统计学习器
**职责**DAA::STA 统计学习的生命周期管理
```cpp
// 约 80 行
class StatCollector {
public:
void configure(const std::string& ruleId, const std::string& ruleName,
int distMode, bool isLearning);
// mon 进程添加样本
void addSample(double value);
// cron 进程批量处理
int processBatch(const std::vector<double>& values);
// 从 DB2 加载置信区间
bool reloadCiDist(double& limitDown, double& limitUp);
private:
std::unique_ptr<DAA::STA> sta_ptr_;
int dist_mode_ = 0;
bool is_learning_ = true;
std::string rule_id_;
};
```
**收益**
- `cron_proc()` 约 60 行逻辑从 ExpBase 抽离
- 统计相关的 DB2 操作封装
### 2.4 算法子类化设计
ExpBase 变为只包含公共逻辑的薄层,具体算法行为通过子类多态实现:
```cpp
// ExpBase 精简后(~300 行)
class ExpBase : public AlgBase {
public:
AlarmInfo exec_mon() override; // 模板方法,调用 virtual doMonProc()
std::vector<AlarmInfo> exec_task(mix_cc::time_range_t) override;
mix_cc::json exec_cron() override;
protected:
// 钩子方法 — 子类重写
virtual AlarmInfo doMonProc() = 0; // 纯虚,每个子类实现自己的逻辑
virtual void doInitExtend() {} // 子类扩展初始化
virtual bool doFilterCheck() const { return true; }
// 共享组件
VarManager vars_;
BoundChecker bound_;
FbStateMachine fb_fsm_;
StatCollector stat_;
// 共享表达式
std::unique_ptr<Expression> exp_act_;
std::unique_ptr<Expression> exp_feedback_;
std::unique_ptr<Expression> exp_result_;
};
// 各子类实现(每个 60~150 行)
class LogicAlg : public ExpBase { // Alg 1
AlarmInfo doMonProc() override {
if (exp_act_->evaluate()) {
return buildAlarm(...);
}
return {};
}
};
class BoundAlg : public ExpBase { // Alg 2, 5
AlarmInfo doMonProc() override {
double val = exp_act_->evaluate();
if (!doFilterCheck()) return {};
if (is_learning_) stat_.addSample(val);
if (bound_.isOutOfBounds(val)) {
return buildAlarm(bound_.buildAlarmMsg(val, ...));
}
return {};
}
};
class BoundHoldAlg : public BoundAlg { // Alg 5
AlarmInfo doMonProc() override { /* 加上持续时间逻辑 */ }
};
class FeedbackAlg : public ExpBase { // Alg 3, 4
AlarmInfo doMonProc() override {
double val = exp_act_->evaluate();
auto state = fb_fsm_.update(val, exp_feedback_->evaluate(),
now_time_, vars_);
switch (state) {
case FbState::Done: return evaluateResult();
case FbState::Timeout: return timeoutAlarm();
default: return {};
}
}
};
```
### 2.5 现有子类的影响
| 子类 | 当前继承 | 优化后 | 说明 |
|------|----------|--------|------|
| ExpTimes | ExpBase | ExpBase组合 StatCollector, VarManager | 丢弃不需要的 FbStateMachine, BoundChecker |
| ExpSample2D | ExpBase | ExpBase | 使用自己的 fit/pear 逻辑 |
| ExpBound | ExpBase | ExpBase组合 BoundChecker | 复用 BoundChecker |
| Roller2 | ExpBase | ExpBase组合 VarManager | 丢弃反馈和统计组件 |
| Roller3 | ExpBase | ExpBase组合 BoundChecker, VarManager | 复用 BoundChecker |
---
## 三、实施路径
### 分三个阶段,每阶段可独立交付
#### 阶段一:提取组件(不改外部行为)
时间估算2-3 天
1. **提取 VarManager**(最大收益)
- 将 `mm_vars_` 及相关方法移入 VarManager
- ExpBase 通过 `vars_` 成员访问
- 不改变任何 public 接口
2. **提取 BoundChecker**
- `detect_up_down()` 移入 BoundChecker
- Roller3 的 `detect_up_down()` 改为复用
3. **运行现有测试验证**
#### 阶段二:提取状态机 + 统计学习器
时间估算1-2 天
4. **提取 FbStateMachine**
- `act_start_done/act_done/act_timeout/act_not_hold` 四合一
- 消除 4 个布尔标志
5. **提取 StatCollector**
- `cron_proc()` 统计逻辑移入
6. **运行验证**
#### 阶段三:子类化消除类型码
时间估算2-3 天
7. **创建 ExpBase 子类**LogicAlg, BoundAlg, FeedbackAlg, BoundHoldAlg
8. **migrate `build_algorithm.cpp`** 使用新子类
9. **清理 ExpBase**:删除不再需要的分支逻辑
10. **全量回归测试**
---
## 四、收益评估
| 指标 | 优化前 | 优化后 |
|------|--------|--------|
| ExpBase 总行数 | 1956 | ~300 |
| 每个具体算法类行数 | N/A | 60~150 |
| `exp_type_` 分支 | 16 处 | 0 |
| 布尔状态标志 | 12 | ~4 |
| `mon_proc()` 行数 | 200 | ~20模板方法 |
| 可单元测试的组件 | 1整体 | 6+VarManager, BoundChecker, FbStateMachine, StatCollector, 各算法子类) |
| 新增算法类型的改动 | 修改 ExpBase 多处分支 | 新增一个 ~100行子类 |
---
## 五、风险评估
- **风险低**:阶段一和阶段二仅做提取,不改变外部行为,配合回归测试风险可控
- **风险中**:阶段三涉及算法 ID 映射变更,需确保所有 18 个 Alg ID 的映射关系正确
- **缓解措施**:每个阶段完成后执行 task 模式对历史数据回测,对比优化前后的报警输出一致性

View File

@ -0,0 +1,573 @@
# FbStateMachine 深度分析与修正设计
> 对上轮优化方案中 FbStateMachine 设计不完整之处的修正,综合考虑 StatExp 状态函数刷新机制。
---
## 一、现有反馈状态机完整映射
### 1.1 真状态图7 状态)
阅读源码后得出的真实状态图,比上一版的 5 状态更精确:
```
┌──────────────────────────────────────┐
│ exec_mon_call() 层 │
│ get_prr()==false → 强制重置 │
│ alarm 触发 → fun_reset(PRR的FunVars) │
└──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ mon_proc() 层 │
│ │
│ ┌─────────┐ act_triggered ┌──────────┐ │
│ │ Idle │ ───────────────► │ Started │ │
│ │ (空闲) │ │ (动作开始) │ │
│ │ fun✓ │ │ fun✗ │ │
│ └─────────┘ └────┬─────┘ │
│ ▲ │ │
│ │ ┌───────┼───────┐ │
│ │ │ │ │ │
│ │ ▼ ▼ ▼ │
│ │ ┌────────┐┌──────┐┌──────────┐ │
│ │ │NotHold ││Done ││Timeout │ │
│ │ │(不保持) ││(完成) ││(超时) │ │
│ │ │ fun✓ ││ fun✓ ││ fun✓ │ │
│ │ └───┬────┘└──┬───┘└────┬─────┘ │
│ │ │ │ │ │
│ └──────────────────┴────────┴─────────┘ │
│ 下次 mon_proc() 自动回到 Idle │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ InProgress (动作进行中) │ │
│ │ act_started_=true, 但未触发终端条件 │ │
│ │ fun✗, 持续更新 mv2/up/dw/mx/mi/time │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
注释: fun✓ = 本周期设置 is_fun_vars_need_reset_ = true
fun✗ = 不设置(状态函数持续累加)
```
**关键**InProgress 不是一个独立的 `return` 分支,而是 "所有终端条件都不满足时fallthrough 到 return {}"的隐式状态。
### 1.2 七个状态详解
| 状态 | 触发条件 | act_started_ | 返回 | fun_reset | 变量操作 |
|------|---------|-------------|------|-----------|---------|
| **Idle** | act_triggered_=false | false | {} | **true** (via act_start_done else分支) | 无 |
| **Started** | 首次 act_triggered_=true | false→true | {} | **false** | 快照 sN, 初始化 mx/mi/mv2/up/dw, stime=now, time=0 |
| **InProgress** | started后, 未达终端 | true | {} | **false** | act_done()更新: time, mx=max, mi=min, mv2+=1, up+=flag, dw+=flag |
| **Done** | feedback_triggered_=true | true→false | AlarmInfo 或 {} | **true** | etime=now (在act_done内) |
| **NotHold** | keep_mode && !act_triggered_ | true→false | {} | **true** | 无额外 |
| **Timeout** | now-start > timeout | true→false | AlarmInfo 或 {} | **true** | 清零 mv2/up/dw (在act_timeout内) |
| **PRR_False** | get_prr()=false (外层) | →false | (不进入mon_proc) | **false** | act_started_/act_triggered_/feedback_triggered_ 全部重置 |
### 1.3 fun_vars 完整生命周期
```
单周期时序:
┌─────────────────────────────────────────────────────────────┐
│ 1. AlgBase::exec_mon_call() │
│ ├── get_cycled()? │
│ ├── get_prr() → 若false: 重置fb状态, return (不进入mon) │
│ └── exec_mon() → ExpBase::exec_mon() │
│ │
│ 2. ExpBase::exec_mon() │
│ ├── refresh_exp_vars_mem/ihd() │
│ │ └── fun_vars_.refresh_fun_vars(false) ← 评估funN │
│ └── mon_proc() │
│ │
│ 3. mon_proc() │
│ ├── auto_fun_vars_reset() ← 若flag, reset funN │
│ ├── evaluate expressions (使用当前funN值) │
│ ├── [反馈状态机判断] │
│ │ └── 若终端状态: is_fun_vars_need_reset_ = true │
│ └── return alarm or {} │
│ │
│ 4. 回到 exec_mon_call() │
│ └── if alarm: exp_mpdule_ptr_->fun_reset() ← PRR的funN │
└─────────────────────────────────────────────────────────────┘
关键时序规则:
- refresh_fun_vars(false) 在 mon_proc() 之前执行
- auto_fun_vars_reset() 在 mon_proc() 开头执行用的是上一周期设置的flag
- is_fun_vars_need_reset_ 在当前周期设置,下周期生效
- 报警消息使用的是本周期reset前的funN值
```
### 1.4 两套 FunVars 的独立性
代码中存在**两套独立的 FunVars 实例**
| 属性 | ExpBase::fun_vars_ | ExpModule::fun_vars_ |
|------|-------------------|---------------------|
| 所属 | ExpBase | ExpModule (exp_mpdule_ptr_) |
| 用途 | 主表达式 (exp_act_, exp_feedback_, exp_result_) 中的状态函数 | PRR 前提表达式 (pre_result) 中的状态函数 |
| 刷新 | refresh_exp_vars_mem/ihd() 中 refresh_fun_vars(false) | ExpModule::update() 中 refresh_fun_vars(false) |
| 重置 | mon_proc() 中 auto_fun_vars_reset() | AlgBase::exec_mon_call() 中 alarm 时 fun_reset() |
| is_exp_alg_=true时 | 由 ExpBase 管理 | **跳过刷新**(共享 ExpBase 的 mm_vars |
| 变量命名 | fun0,fun1... | fun0,fun1... (独立index可能冲突) |
**⚠️ 潜在问题**:当 `is_exp_alg_=true`ExpModule::update() 跳过其 fun_vars 刷新。但两个 FunVars 的 `index` 计数器是独立的——如果 PRR 表达式中也有状态函数(如 `KeepC(tag1,1)`),且 ExpBase::fun_vars_init() 将其加入了 ExpBase::fun_vars_则两套 FunVars 可能创建同名的 `fun0` 变量指向不同的表达式。幸好 `is_exp_alg_=true` 时 ExpModule 这部分逻辑被跳过。
---
## 二、上一版 FbStateMachine 设计的缺陷
### 缺陷 1未处理变量快照和更新
上一版的 FbStateMachine::update() 设计只做状态转换,但**变量操作(快照/更新)与状态转换不可分割**
- `act_start_done()` 在 Started 状态执行变量快照6 类变量初始化)
- `act_done()` 在每个 InProgress 周期更新累积变量time, mx, mi, mv2, up, dw
- `act_timeout()` 在 Timeout 状态清零累积变量
这些操作必须和状态转换在同一个事务中完成,否则反馈表达式(引用 mv2_tagN, up_tagN 等)会读到过期值。
### 缺陷 2未考虑 Idle 状态的 fun_vars 重置
`act_start_done()` 的 else 分支 `!act_started_ && !act_triggered_` 设置了 `is_fun_vars_need_reset_ = true`。这意味着**每个 Idle 周期**都会标记 fun_vars 需要重置。上一版设计中 Idle→needReset=true 是正确的,但没有考虑这个重置是通过 `act_start_done()` 的副作用实现的。
### 缺陷 3未处理 Timeout 哨兵值
`time_out_ == milliseconds(-32768)` 表示无超时限制。此时 `act_timeout()` 不检查时间,而是检查 mv2_tagN 是否接近 DBL_MAX防溢出。这是一个特殊分支上一版未覆盖。
### 缺陷 4未处理 PRR 强制重置
`get_prr()` 返回 false 时(`ExpBase` 重写版本),反馈状态标志被强制清零:
```cpp
this->act_started_ = false;
this->act_triggered_ = false;
this->feedback_triggered_ = false;
```
`is_fun_vars_need_reset_` 不设置。PRR 恢复后fun_vars 保持之前的状态。FbStateMachine 需要暴露一个 `forceReset()` 方法。
### 缺陷 5未考虑 Done 状态中 CondBound 的特殊逻辑
`act_done()` 返回 true 后mon_proc() 还有后续逻辑:
- 计算 `exp_result_->evaluate()`
- CondBound检测上下限 → 可能报警
- 非 CondBound检测布尔值 → 可能报警
- 有自学习:添加统计样本
上一版设计中把这些放在了 FbStateMachine 外部,但没有明确接口。
---
## 三、修正后的 FbStateMachine 设计
### 3.1 接口设计
```cpp
// 状态枚举
enum class FbState {
Idle, // 空闲
Started, // 刚启动(第一个周期,等待下次检查反馈条件)
InProgress, // 进行中
Done, // 反馈条件满足,动作完成
NotHold, // 不保持keep_mode下触发条件丢失
Timeout // 超时
};
// update() 返回结构
struct FbUpdateResult {
FbState state;
bool funVarsNeedReset; // 本周期是否需要标记 fun_vars 重置(下周期生效)
};
class FbStateMachine {
public:
// === 配置(替代 reload_config_exp_feedback 中的状态相关部分)===
void configure(bool keepMode, TimeDur timeout);
// === 核心:每个 mon 周期调用一次 ===
// now: 当前时间
// vars: 变量管理器引用(用于快照/更新变量)
// actTriggered: 前提表达式结果 (exp_act_->evaluate())
FbUpdateResult update(bool actTriggered, TimePoint now, VarManager& vars);
// === 条件检查(在 update 返回 InProgress 后调用)===
// fbCondition: 反馈表达式结果 (exp_feedback_->evaluate())
// 返回 true 表示反馈条件满足,应转为 Done
bool checkFeedback(bool fbCondition, TimePoint now, VarManager& vars);
// === 外部强制重置PRR=false 时调用)===
void forceReset();
// === 查询 ===
bool isActive() const; // Started 或 InProgress
TimePoint actionStartTime() const;
bool isKeepMode() const;
private:
bool keep_mode_ = false;
TimeDur timeout_ = 10min;
FbState state_ = FbState::Idle;
TimePoint start_time_;
int tag_count_ = 0;
// 防溢出(替代 act_timeout 中 timeout_ == -32768 分支)
bool checkOverflowPrevention(VarManager& vars);
};
```
### 3.2 完整状态转换实现
```cpp
FbUpdateResult FbStateMachine::update(bool actTriggered, TimePoint now,
VarManager& vars) {
FbUpdateResult result;
result.funVarsNeedReset = false;
switch (state_) {
// ── Idle ─────────────────────────────────────────────
case FbState::Idle:
if (actTriggered) {
// → Started快照变量
vars.snapshotActionStart(now);
state_ = FbState::Started;
start_time_ = now;
result.state = FbState::Started;
// funVarsNeedReset = false保持状态函数连续性
} else {
// 保持 Idle但标记 fun_vars 重置
result.state = FbState::Idle;
result.funVarsNeedReset = true;
// 原因无活动事件时KeepC/RiseEdge 不应累积
}
break;
// ── Started仅在首次触发的那个周期─────────────────
case FbState::Started:
// 更新累积变量(第一个周期也需要更新)
vars.updateActionVars(now);
if (!actTriggered && keep_mode_) {
// 触发条件在第一个周期就丢失 → NotHold
state_ = FbState::Idle; // 下周期回到 Idle
result.state = FbState::NotHold;
result.funVarsNeedReset = true;
} else if (checkOverflowPrevention(vars)) {
state_ = FbState::Idle;
result.state = FbState::Timeout;
result.funVarsNeedReset = true;
} else {
// 正常进入 InProgress
state_ = FbState::InProgress;
result.state = FbState::Started; // 返回 Started 让调用方跳过反馈检查
// funVarsNeedReset = false
}
break;
// ── InProgress ───────────────────────────────────────
case FbState::InProgress:
// 先更新累积变量
vars.updateActionVars(now);
if (!actTriggered && keep_mode_) {
state_ = FbState::Idle;
result.state = FbState::NotHold;
result.funVarsNeedReset = true;
} else if (checkOverflowPrevention(vars)) {
state_ = FbState::Idle;
result.state = FbState::Timeout;
result.funVarsNeedReset = true;
} else if (timeout_ != TimeDur(-32768) &&
(now - start_time_) > timeout_) {
// 超时
vars.clearActionAccumulators();
state_ = FbState::Idle;
result.state = FbState::Timeout;
result.funVarsNeedReset = true;
}
// 否则保持 InProgress等待 checkFeedback()
break;
// ── 终端状态Done/NotHold/Timeout由 checkFeedback 或上述分支设置)──
default:
// 上周期是终端状态,本周期自动回到 Idle
state_ = FbState::Idle;
result.state = FbState::Idle;
result.funVarsNeedReset = true; // 延续 Idle 的 reset 行为
break;
}
return result;
}
bool FbStateMachine::checkFeedback(bool fbCondition, TimePoint now,
VarManager& vars) {
if (state_ != FbState::InProgress && state_ != FbState::Started) {
return false;
}
if (fbCondition) {
vars.recordActionEnd(now); // etime = now
state_ = FbState::Idle; // 下周期回到 Idle
return true; // → Done
}
return false;
}
void FbStateMachine::forceReset() {
state_ = FbState::Idle;
start_time_ = TimePoint{};
}
bool FbStateMachine::checkOverflowPrevention(VarManager& vars) {
if (timeout_ != TimeDur(-32768)) return false;
return vars.isAccumulatorNearOverflow();
}
```
### 3.3 VarManager 需要新增的方法
为配合 FbStateMachineVarManager 需要新增以下方法:
```cpp
class VarManager {
public:
// ... 原有方法 ...
// ── 反馈状态机配合 ──
// 动作开始时快照(替代 act_start_done 中的变量初始化)
void snapshotActionStart(TimePoint now);
// 动作进行中更新累积值(替代 act_done 中前半段的变量更新)
void updateActionVars(TimePoint now);
// 动作结束时记录(替代 act_done 中 etime 设置)
void recordActionEnd(TimePoint now);
// 超时时清零累积器(替代 act_timeout 中的清零)
void clearActionAccumulators();
// 累积值是否接近溢出
bool isAccumulatorNearOverflow() const;
};
```
具体实现直接迁移现有代码:
```cpp
void VarManager::snapshotActionStart(TimePoint now) {
mm_vars_["stime"] = duration_cast<milliseconds>(now.time_since_epoch()).count();
mm_vars_["time"] = 0;
for (size_t i = 0; i < tag_count_; i++) {
std::string idx = std::to_string(i + 1);
double tag_val = mm_vars_["tag" + idx];
mm_vars_["s" + idx] = tag_val;
mm_vars_["mx_tag" + idx] = tag_val;
mm_vars_["mi_tag" + idx] = tag_val;
mm_vars_["mv2_tag" + idx] = 0;
mm_vars_["mv2_p" + idx] = 0;
double p_val = mm_vars_["p" + idx];
mm_vars_["up_tag" + idx] = (p_val == 0 && tag_val == 1) ? 1.0 : 0.0;
mm_vars_["dw_tag" + idx] = (p_val == 1 && tag_val == 0) ? 1.0 : 0.0;
if (tag_val == 1) mm_vars_["mv2_tag" + idx] = 1;
if (p_val == 1) mm_vars_["mv2_p" + idx] = 1;
}
}
void VarManager::updateActionVars(TimePoint now) {
mm_vars_["time"] = mm_vars_["now"] - mm_vars_["stime"];
for (size_t i = 0; i < tag_count_; i++) {
std::string idx = std::to_string(i + 1);
double tag_val = mm_vars_["tag" + idx];
double p_val = mm_vars_["p" + idx];
mm_vars_["mx_tag" + idx] = std::max(tag_val, mm_vars_["mx_tag" + idx]);
mm_vars_["mi_tag" + idx] = std::min(tag_val, mm_vars_["mi_tag" + idx]);
if (tag_val == 1) mm_vars_["mv2_tag" + idx] += 1;
if (p_val == 1) mm_vars_["mv2_p" + idx] += 1;
mm_vars_["up_tag" + idx] += (p_val == 0 && tag_val == 1) ? 1.0 : 0.0;
mm_vars_["dw_tag" + idx] += (p_val == 1 && tag_val == 0) ? 1.0 : 0.0;
}
}
void VarManager::recordActionEnd(TimePoint now) {
mm_vars_["etime"] = mm_vars_["now"];
}
void VarManager::clearActionAccumulators() {
for (size_t i = 0; i < tag_count_; i++) {
std::string idx = std::to_string(i + 1);
mm_vars_["mv2_tag" + idx] = 0;
mm_vars_["mv2_p" + idx] = 0;
mm_vars_["up_tag" + idx] = 0;
mm_vars_["dw_tag" + idx] = 0;
}
}
bool VarManager::isAccumulatorNearOverflow() const {
for (size_t i = 0; i < tag_count_; i++) {
std::string idx = std::to_string(i + 1);
if (std::abs(mm_vars_.at("mv2_tag" + idx) - DBL_MAX) < 2.0)
return true;
}
return false;
}
```
### 3.4 mon_proc() 重构后的完整流程
```cpp
// FeedbackAlg (Alg 3/4) 的 doMonProc()
AlarmInfo FeedbackAlg::doMonProc() {
// 1. 自动重置 fun_vars若上周期标记
vars_.autoResetFunVars();
// 2. 评估前提条件
double result_value = exp_act_->evaluate();
// 3. 如果有上下限的反馈模式(CondBound),先做筛选+统计
if (hasBoundCheck_ && is_learning_) {
bool filter_ok = checkFilter();
if (filter_ok) {
stat_.addSample(result_value);
}
}
// 4. 反馈状态机更新(含变量快照/累积更新)
auto [fbState, needFunReset] = fb_fsm_.update(
static_cast<bool>(result_value), now_time_, vars_);
if (needFunReset) {
vars_.markFunVarsNeedReset();
}
// 5. 根据状态分发
switch (fbState) {
case FbState::Started:
return {}; // 刚启动,等待下周期
case FbState::InProgress: {
// 检查反馈条件
bool fbCond = exp_feedback_->evaluate();
if (fb_fsm_.checkFeedback(fbCond, now_time_, vars_)) {
// Done! 计算结果并判断报警
vars_.markFunVarsNeedReset();
return evaluateResultAndAlarm();
}
return {};
}
case FbState::NotHold:
return {};
case FbState::Timeout:
return buildTimeoutAlarm();
case FbState::Done:
// 由 checkFeedback 内部处理,不应直接到此
return {};
case FbState::Idle:
return {};
}
return {};
}
AlarmInfo FeedbackAlg::evaluateResultAndAlarm() {
double result = exp_result_->evaluate();
query_time_range_.set_left(
query_time_range_.get_right() - milliseconds(int(vars_["time"])));
if (hasBoundCheck_) {
// CondBound: Alg 4
stat_.addSample(result);
if (boundChecker_.isOutOfBounds(result)) {
return buildBoundAlarm(result);
}
} else {
// FeedbackLogic: Alg 3
if (static_cast<bool>(result)) {
return buildLogicAlarm();
}
}
return {};
}
```
---
## 四、修正后的组件与新旧对比
### 4.1 修正后的组件清单
```
┌─────────────────┐
│ VarManager │ 变量存储、pv历史移位、hold变量
│ │ 【新增】snapshotActionStart/updateActionVars/
│ │ recordActionEnd/clearActionAccumulators
│ │ autoResetFunVars/markFunVarsNeedReset
├─────────────────┤
│ BoundChecker │ 上下限比较、检测模式、哨兵值处理
├─────────────────┤
│ FbStateMachine │ 反馈状态转换【完整7状态】
│ │ 含变量快照/更新调度、防溢出、keep模式
├─────────────────┤
│ StatCollector │ DAA::STA 统计学习、DB2 持久化
├─────────────────┤
│ FunVarsManager │ 【新增】统一管理两套 FunVars 的刷新与重置
└─────────────────┘
```
### 4.2 FunVarsManager新增组件
为避免 ExpBase::fun_vars_ 和 ExpModule::fun_vars_ 的混乱,引入统一管理:
```cpp
class FunVarsManager {
public:
// 注册主表达式的状态函数(替代 ExpBase::fun_vars_.add_exp_str
void registerMainExp(const std::string& expStr,
std::map<std::string, double>* mm_vars);
// 每个数据刷新周期调用(替代 refresh_fun_vars(false)
void refresh();
// 标记下周期重置(替代 is_fun_vars_need_reset_ = true
void markNeedReset();
// 执行延迟重置(替代 auto_fun_vars_reset()
void autoReset();
// 立即强制重置(用于 alarm 后 PRR 的 fun_vars
void forceReset();
private:
StatExp::FunVars fun_vars_;
bool need_reset_ = false;
};
```
---
## 五、与原实现的等价性验证矩阵
| 原有代码路径 | 新设计等价路径 | 验证点 |
|------------|-------------|--------|
| `act_start_done()` → true | `fb_fsm_.update()` → Started | 变量快照相同 |
| `act_start_done()` → false + setIdleFlag | `fb_fsm_.update()` → Idle + funVarsNeedReset=true | fun_reset 标记相同 |
| `act_not_hold()` → true | `fb_fsm_.update()` → NotHold | keep_mode 检查相同 |
| `act_done()` 变量更新部分 | `fb_fsm_.update()` → InProgress 中调用 `vars_.updateActionVars()` | mx/mi/mv2/up/dw/time 相同 |
| `act_done()` 反馈条件检查 | `checkFeedback(fbCond)` | exp_feedback_->evaluate() 相同 |
| `act_done()` → true + CondBound | `evaluateResultAndAlarm()` hasBoundCheck_=true | 上下限检测+报警相同 |
| `act_done()` → true + 非CondBound | `evaluateResultAndAlarm()` hasBoundCheck_=false | 布尔报警相同 |
| `act_timeout()` 正常超时 | `fb_fsm_.update()` → Timeout + clearActionAccumulators | 变量清零相同 |
| `act_timeout()` -32768 防溢出 | `checkOverflowPrevention()` | 溢出检测相同 |
| `get_prr()` → false 重置 | `fb_fsm_.forceReset()` | 状态标志清零相同 |
| `auto_fun_vars_reset()` | `FunVarsManager::autoReset()` | 延迟一周期重置相同 |
| `refresh_fun_vars(false)` | `FunVarsManager::refresh()` | funN 变量更新相同 |
| `exp_mpdule_ptr_->fun_reset()` on alarm | `FunVarsManager::forceReset()` | PRR fun_vars 重置相同 |
---
## 六、结论
上一版 FbStateMachine 的主要遗漏在于**低估了状态转换与变量操作的耦合程度**。反馈状态机的本质不是纯状态转换——它是一个**状态-变量协同机**,在每个状态转换点都需要精确的变量操作(快照、累积、清零)。
修正后的设计将这一协同完整建模:
- VarManager 提供 5 个新方法snapshot/update/record/clear/overflowCheck
- FbStateMachine 变为 7 状态(增加 Started 的独立表示和 Idle 的显式处理)
- 引入 FunVarsManager 统一两套 FunVars 的管理
- 通过等价性验证矩阵确保覆盖所有现有路径

View File

@ -0,0 +1,376 @@
# 统一表达式引擎设计:消除双 FunVars 和 is_exp_alg_ 补丁
> 针对 ExpBase::fun_vars_ 与 ExpModule::fun_vars_ 双重存在、is_exp_alg_ 补丁标志的历史遗留问题的根治方案
---
## 一、现状问题精准画像
### 1.1 重复代码对照
```
ExpBase::refresh_exp_vars_mem() ExpModule::refresh_exp_vars_mem()
───────────────────────────────── ─────────────────────────────────
更新 tagN ← 共享内存 更新 tagN ← 共享内存
更新 pN ← 旧 tagN 更新 pN ← 旧 tagN
更新 pvN_0..pvN_5 历史移位 更新 pvN_0..pvN_5 历史移位
更新 "now" ← epoch ms 更新 "now" ← epoch ms
调用 fun_vars_.refresh_fun_vars(false) (在 update() 中调用 fun_vars_.refresh_fun_vars(false))
调用 refresh_hold_var() (无 hold_var 处理)
设置 query_time_range_ (无)
```
两段代码做**几乎相同的事**,操作**同一个 mm_vars**,属于典型的 Cargo Cult 编程。
```
ExpBase::first_fill_mm_vars() ExpModule::init()
───────────────────────────────── ──────────────────
初始化所有 tagN, pN, pvN_0..pvN_5 初始化所有 tagN, pN, pvN_0..pvN_5
初始化 stime, now, etime, time 初始化 "now"
初始化 sN, mx_tagN, mi_tagN (无)
初始化 mv2_tagN, up_tagN, dw_tagN (无)
```
也是重复。
### 1.2 双 FunVars 的冲突风险
```
ExpBase::fun_vars_ ExpModule::fun_vars_
───────────────────── ─────────────────────
管理表达式: 管理表达式:
exp_act_ (带状态函数替换) pre_result (带状态函数替换)
exp_feedback_ (带状态函数替换)
exp_result_ (带状态函数替换)
pre_result (通过 fun_vars_init 共享)
内部index: 独立计数 内部index: 独立计数
创建的 mm_vars 变量: fun0,fun1,... 创建的 mm_vars 变量: fun0,fun1,...
```
**冲突场景**
- ExpModule::add_exp("pre_result", "KeepC(tag1,1)") → 创建 mm_vars["fun0"]
- ExpBase::fun_vars_init() → fun_vars_.add_exp_str("KeepC(tag1,1)") → 也创建 mm_vars["fun0"]
两个 FunVars 实例的 `index` 都从 0 开始,会向 mm_vars 写入同名的 `fun0` 变量。当 `is_exp_alg_=true` 时 ExpModule 跳过刷新,所以两个 `fun0` 不会在同一周期冲突。但如果某天代码修改导致两个 FunVars 都活跃,就会产生难以排查的 bug。
### 1.3 `is_exp_alg_` 传递链的脆弱性
```
AlgBase::is_exp_alg_ = false (默认)
│ 引用传递
ExpModule::is_exp_alg_& ← 持有 AlgBase 成员的引用
│ ExpBase 构造时
AlgBase::is_exp_alg_ = true
│ ExpModule 通过引用看到变化
ExpModule::is_exp_alg_ == true → 跳过变量刷新
```
这条引用链的问题:
- ExpModule 的生命周期依赖 AlgBase引用不能悬挂
- `is_exp_alg_` 被当作"表达式算法"的标记,实际上它是"是否需要 ExpModule 管理变量刷新"的补丁
- 新算法开发者需要理解这个隐式约定才能正确设置
---
## 二、统一方案ExpressionEngine
### 2.1 核心思路
**让 ExpModule 吃掉 ExpBase 的表达式管理职责**,成为唯一的表达式引擎。
```
BEFORE (现状): AFTER (统一后):
AlgBase AlgBase
├── exp_mpdule_ptr_ (ExpModule) └── expr_engine_ (ExpressionEngine)
│ ├── PRR 表达式 ├── 所有命名表达式
│ ├── 自己的 FunVars │ ├── "pre_result" (PRR)
│ └── 变量刷新 (当 is_exp_alg_=false) │ ├── "act" (前提表达式)
│ │ ├── "feedback" (反馈表达式)
ExpBase (is_exp_alg_=true) │ ├── "result" (结果表达式)
├── exp_act_ (独立的 Expression) │ ├── "sample_X", "sample_Y"
├── exp_feedback_ (独立的 Expression) │ ├── "dataX"
├── exp_result_ (独立的 Expression) │ └── "X1".."X9" (Roller2)
├── 自己的 FunVars ├── 唯一的 FunVars
├── 变量刷新 ├── 唯一的变量刷新
└── 自己的表达式配置加载 └── 统一的配置加载
```
### 2.2 ExpressionEngine 完整设计
```cpp
// expression_engine.h
// 统一表达式引擎 —— 替代 ExpModule + ExpBase 的表达式管理
class ExpressionEngine {
public:
// ========== 构造 ==========
ExpressionEngine(std::map<std::string, double>& mm_vars,
std::vector<std::string>& m_tags);
// ========== 表达式注册 ==========
// 注册一个命名表达式。返回 0 成功,-1 失败。
// exp_name: "act", "feedback", "result", "sample_X", "sample_Y",
// "dataX", "X1"..."X9", "pre_result" 等
// raw_exp_str: 原始表达式字符串(含 KeepC(tag1,1) 等)
// use_fun_vars: 是否启用状态函数替换(默认 true
int registerExpression(const std::string& exp_name,
const std::string& raw_exp_str,
bool use_fun_vars = true);
// ========== 每周期数据刷新 ==========
// 从共享内存刷新变量 + fun_vars
void refreshFromMemory();
// 从 IHDB 行数据刷新变量 + fun_vars
void refreshFromIhdRow(int row, const Eigen::MatrixXd& data,
const std::vector<TimePoint>& times);
// 首次填充(冷启动)
void firstFill(int data_source);
// ========== 表达式求值 ==========
double evaluate(const std::string& exp_name);
bool evaluateBool(const std::string& exp_name);
// ========== FunVars 控制 ==========
// 每周期调用一次(通常在 mon_proc 开头)
void autoResetFunVars();
// 标记下周期需要重置
void markFunVarsNeedReset();
// 立即强制重置(报警后调用)
void forceResetFunVars();
// ========== hold(n,T) 变量 ==========
void refreshHoldVars();
// ========== 查询 ==========
bool hasExpression(const std::string& exp_name) const;
std::string getExpressionStr(const std::string& exp_name) const;
std::map<std::string, double>& vars() { return mm_vars_; }
private:
std::map<std::string, double>& mm_vars_;
std::vector<std::string>& m_tags_;
// 唯一的 FunVars 实例
StatExp::FunVars fun_vars_;
bool fun_vars_need_reset_ = false;
// 表达式注册表
std::map<std::string, std::string> exp_raw_strs_; // 原始表达式字符串
std::map<std::string, std::string> exp_processed_; // FunVars 处理后的字符串
std::map<std::string, std::unique_ptr<MExp>> exp_ptrs_; // 编译后的表达式
// hold 变量管理
std::map<std::string, std::unique_ptr<HoldTime>> hold_times_;
// 变量缓存pv 键名管理)
VarsCache var_cache_;
static constexpr size_t PV_NUM = 6;
// 内部方法
void doRefreshVarsMemory();
void doRefreshVarsIhd(int row, const Eigen::MatrixXd& data,
const std::vector<TimePoint>& times);
int initHoldExpStr(const std::string& exp_str);
};
```
### 2.3 实现要点
```cpp
int ExpressionEngine::registerExpression(const std::string& exp_name,
const std::string& raw_exp_str,
bool use_fun_vars) {
if (exp_ptrs_.count(exp_name)) return 0; // 已注册
exp_raw_strs_[exp_name] = raw_exp_str;
std::string processed = raw_exp_str;
if (use_fun_vars) {
auto [ok, result] = fun_vars_.add_exp_str(raw_exp_str, &mm_vars_);
if (!ok) return -1;
processed = result;
}
exp_processed_[exp_name] = processed;
exp_ptrs_[exp_name] = std::make_unique<MExp>(processed, &mm_vars_);
return 0;
}
void ExpressionEngine::refreshFromMemory() {
// 1. 刷新 tag/pv 变量(统一的,只做一次)
for (size_t i = 0; i < m_tags_.size(); i++) {
double current = SingletonTemplate<GlobaltemSharedMemory>::GetInstance()[m_tags_[i]];
auto idx = std::to_string(i + 1);
mm_vars_[var_cache_.p_keys[i]] = mm_vars_[var_cache_.tag_keys[i]];
mm_vars_[var_cache_.tag_keys[i]] = current;
for (size_t j = PV_NUM - 1; j > 0; j--) {
mm_vars_[var_cache_.pv_keys[i][j]] = mm_vars_[var_cache_.pv_keys[i][j - 1]];
}
mm_vars_[var_cache_.pv_keys[i][0]] = current;
}
auto now = system_clock::now();
mm_vars_["now"] = duration_cast<milliseconds>(now.time_since_epoch()).count();
// 2. 刷新 fun_vars只做一次处理所有表达式的状态函数
fun_vars_.refresh_fun_vars(false, &mm_vars_);
// 3. 刷新 hold 变量
refreshHoldVars();
}
void ExpressionEngine::autoResetFunVars() {
if (fun_vars_need_reset_) {
fun_vars_.refresh_fun_vars(true, &mm_vars_);
fun_vars_need_reset_ = false;
}
}
```
### 2.4 各算法使用 ExpressionEngine 的方式
```
// ========== AlgBase ==========
class AlgBase {
protected:
std::unique_ptr<ExpressionEngine> expr_engine_; // 替代 exp_mpdule_ptr_
// 删除: is_exp_alg_
};
// AlgBase::init() 中:
expr_engine_ = std::make_unique<ExpressionEngine>(mm_vars, m_tags);
if (prr_ == 1) {
expr_engine_->registerExpression("pre_result", pre_exp_str);
}
// AlgBase::exec_mon_call() 中:
if (alarm.alarmed) {
expr_engine_->forceResetFunVars(); // 替代 exp_mpdule_ptr_->fun_reset()
}
// AlgBase::get_prr() 中:
expr_engine_->refreshFromMemory(); // 统一刷新(不再有 is_exp_alg_ 判断)
bool prr_result = expr_engine_->evaluateBool("pre_result");
// ========== ExpBase (expression algorithms) ==========
// init() 中:
expr_engine_->registerExpression("act", exp_str);
if (feedback_mode_) {
expr_engine_->registerExpression("feedback", fb_exp_str);
}
expr_engine_->registerExpression("result", result_exp_str);
expr_engine_->firstFill(data_source_);
// exec_mon() 中:
expr_engine_->refreshFromMemory(); // 统一刷新
// ... 然后 mon_proc() 使用 expr_engine_->evaluate("act") 等
// ========== TrendSlope2 (non-expression algorithm) ==========
// 只需要 PRR:
// 在 AlgBase::get_prr() 中 expr_engine_->evaluateBool("pre_result")
// 自身的数据获取不受影响
// ========== Roller2 (multi-expression) ==========
// init() 中:
expr_engine_->registerExpression("pre_exp", pre_str);
expr_engine_->registerExpression("X1", x1_str);
expr_engine_->registerExpression("X2", x2_str);
// ...
```
---
## 三、消除项清单
统一后,以下内容可以**删除**
### 从 ExpBase 删除
| 删除项 | 行数 | 说明 |
|--------|------|------|
| `ExpBase::fun_vars_` | 1 行声明 | 合并到 ExpressionEngine |
| `ExpBase::refresh_exp_vars_mem()` | ~25 行 | 合并到 ExpressionEngine::refreshFromMemory() |
| `ExpBase::refresh_exp_vars_ihd()` | ~45 行 | 合并到 ExpressionEngine::refreshFromIhdRow() |
| `ExpBase::first_fill_mm_vars()` | ~110 行 | 合并到 ExpressionEngine::firstFill() |
| `ExpBase::auto_fun_vars_reset()` | ~15 行 | 合并到 ExpressionEngine::autoResetFunVars() |
| `ExpBase::fun_vars_init()` | ~6 行 | 不再需要(注册时自动处理) |
| `ExpBase::refresh_hold_var()` | ~10 行 | 合并到 ExpressionEngine |
| `ExpBase::init_hold_exp_str()` | ~30 行 | 合并到 ExpressionEngine |
| `ExpBase::exp_messy_code_check()` | ~8 行 | 合并到 ExpressionEngine |
| `exp_act_`, `exp_feedback_`, `exp_result_` | 3 个成员 | 改为通过 expr_engine_->evaluate("act") 等 |
### 从 ExpModule 删除
| 删除项 | 说明 |
|--------|------|
| **整个 ExpModule 类** | 被 ExpressionEngine 完全替代 |
### 从 AlgBase 删除
| 删除项 | 说明 |
|--------|------|
| `is_exp_alg_` | 补丁标志不再需要 |
| `exp_mpdule_ptr_` | 替换为 `expr_engine_` |
### 总计消除
- **重复的变量刷新代码**~180 行ExpBase+ExpModule 各一套)
- **重复的 FunVars 实例**1 个
- **补丁标志 `is_exp_alg_`**1 个布尔 + 引用传递链
- **ExpModule 整个类**~140 行(被 ExpressionEngine 取代)
- **ExpBase 可精简**~260 行
---
## 四、实施路径
### 阶段一:创建 ExpressionEngine1 天)
1. 新建 `eqpalg/utility/expression_engine.h/.cpp`
2. 将 ExpBase 的变量刷新逻辑(`refresh_exp_vars_mem/ihd`、`first_fill_mm_vars`)迁入
3. 将 ExpBase 的 FunVars 管理(`auto_fun_vars_reset`、`refresh_hold_var`、`init_hold_exp_str`)迁入
4. 将 ExpModule 的表达式注册机制(`add_exp`、`update`)迁入并重命名为 `registerExpression` / `evaluate`
5. **此阶段不修改 AlgBase/ExpBase仅创建新类并编写独立单元测试**
### 阶段二:替换 ExpModule1 天)
6. 将 AlgBase 中的 `exp_mpdule_ptr_` 替换为 `expr_engine_`
7. 适配 `AlgBase::init()` 中的 PRR 注册
8. 适配 `AlgBase::get_prr()``AlgBase::exec_mon_call()` 中的 fun_reset 调用
9. 适配所有非 ExpBase 算法TrendSlope2/3、Roller、FaultCode、GlitchDetection
10. **删除 ExpModule 类**
11. **删除 `is_exp_alg_` 标志**
12. 运行非 ExpBase 算法的回归测试
### 阶段三:替换 ExpBase 的表达式管理1-2 天)
13. 将 ExpBase 中的 `exp_act_`/`exp_feedback_`/`exp_result_` 改为通过 `expr_engine_->evaluate()` 访问
14. 删除 ExpBase 的 `fun_vars_`、变量刷新方法、`auto_fun_vars_reset` 等
15. 适配 ExpBase 的所有子类ExpBound、ExpTimes、ExpSample2D、Roller2、Roller3
16. 运行全量回归测试
### 阶段四清理0.5 天)
17. 删除 ExpBase 中不再需要的成员和方法
18. 删除 `exp_base.cpp` 中 ~260 行已迁移代码
19. 更新 build_algorithm.cpp 如需要
---
## 五、收益评估
| 指标 | 优化前 | 优化后 |
|------|--------|--------|
| FunVars 实例数 | 2 | 1 |
| 变量刷新代码份数 | 2ExpBase + ExpModule | 1ExpressionEngine |
| `is_exp_alg_` 补丁 | 存在 | 消除 |
| 表达式管理入口 | 3 处ExpBase独立3成员 + ExpModule map | 1 处ExpressionEngine 统一 map |
| 新算法接入方式 | 需了解 is_exp_alg_ 约定 | 直接 registerExpression |
| 状态函数冲突风险 | 存在(同名 funN | 消除 |
| ExpModule 类 | 存在 | 删除 |

View File

@ -14,7 +14,7 @@
// - test_fb_state_machine.cc — FbStateMachine
// 本文件在这些已有覆盖的基础上增加算法级集成测试和算法特有逻辑的测试。
#include "test_harness.h"
#include <eqpalg/test/test_harness.h>
#include <eqpalg/utility/expression_engine.h>
#include <eqpalg/utility/bound_checker.h>
#include <eqpalg/utility/fb_state_machine.h>

View File

@ -1,5 +1,5 @@
// eqpalg/test/test_expression_engine.cc
#include "test_harness.h"
#include <eqpalg/test/test_harness.h>
#include <eqpalg/utility/expression_engine.h>
#include <map>
#include <string>

View File

@ -1,5 +1,5 @@
// eqpalg/test/test_fb_state_machine.cc
#include "test_harness.h"
#include <eqpalg/test/test_harness.h>
#include <eqpalg/utility/fb_state_machine.h>
#include <chrono>
#include <map>

View File

@ -1,5 +1,5 @@
// eqpalg/test/test_main.cc
#include "test_harness.h"
#include <eqpalg/test/test_harness.h>
int main() {
std::cout << "ExpressionEngine Tests\n======================\n\n";