eis/eqpalg/algs/UNIFIED_EXPRESSION_ENGINE.md

15 KiB
Raw Permalink Blame History

统一表达式引擎设计:消除双 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 完整设计

// 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 实现要点

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/ihdfirst_fill_mm_vars)迁入
  3. 将 ExpBase 的 FunVars 管理(auto_fun_vars_resetrefresh_hold_varinit_hold_exp_str)迁入
  4. 将 ExpModule 的表达式注册机制(add_expupdate)迁入并重命名为 registerExpression / evaluate
  5. 此阶段不修改 AlgBase/ExpBase仅创建新类并编写独立单元测试

阶段二:替换 ExpModule1 天)

  1. 将 AlgBase 中的 exp_mpdule_ptr_ 替换为 expr_engine_
  2. 适配 AlgBase::init() 中的 PRR 注册
  3. 适配 AlgBase::get_prr()AlgBase::exec_mon_call() 中的 fun_reset 调用
  4. 适配所有非 ExpBase 算法TrendSlope2/3、Roller、FaultCode、GlitchDetection
  5. 删除 ExpModule 类
  6. 删除 is_exp_alg_ 标志
  7. 运行非 ExpBase 算法的回归测试

阶段三:替换 ExpBase 的表达式管理1-2 天)

  1. 将 ExpBase 中的 exp_act_/exp_feedback_/exp_result_ 改为通过 expr_engine_->evaluate() 访问
  2. 删除 ExpBase 的 fun_vars_、变量刷新方法、auto_fun_vars_reset
  3. 适配 ExpBase 的所有子类ExpBound、ExpTimes、ExpSample2D、Roller2、Roller3
  4. 运行全量回归测试

阶段四清理0.5 天)

  1. 删除 ExpBase 中不再需要的成员和方法
  2. 删除 exp_base.cpp 中 ~260 行已迁移代码
  3. 更新 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 类 存在 删除