Compare commits

..

22 Commits

Author SHA1 Message Date
Huamonarch
ca2147e499 test: 核心算法单元测试 — LogicAlg/BoundAlg/FeedbackAlg/ExpTimes/FaultCode/Roller3 2026-05-15 14:55:30 +08:00
Huamonarch
032f0d0978 refactor: 切换 build_algorithm 工厂到算法子类 + CMake 更新 2026-05-15 14:42:08 +08:00
Huamonarch
ae7834adaa refactor: 算法子类化 — LogicAlg/BoundAlg/BoundHoldAlg/FeedbackAlg 消除 exp_type_ 分支
将 ExpBase::mon_proc() 中的 exp_type_ 分支逻辑提取为 4 个子类,通过 doMonProc() 虚函数实现多态分发:
- LogicAlg (exp_type 1):实时逻辑判断
- BoundAlg (exp_type 2):监控变量上下限检测
- BoundHoldAlg (exp_type 5):持续超限检测(继承 BoundAlg)
- FeedbackAlg (exp_type 3/4):动作反馈处理

ExpBase 新增纯虚函数 doMonProc() 和钩子函数 doInitExtend(),
init() 和 mon_proc() 中所有类型分支替换为虚函数委托调用。
2026-05-15 14:37:15 +08:00
Huamonarch
4f8eecd828 refactor: 提取 StatCollector 统计学习组件
将 ExpBase::cron_proc() 中的 DAA::STA 统计学习逻辑提取到独立的
StatCollector 工具类,统一管理分布统计的生命周期(样本累积、分布
初始化、DB2 持久化、置信区间更新)。同时将 exec_task() 与
task_mon_pro() 中的任务相关 STA 操作也委托给 StatCollector。

新增:
- eqpalg/utility/stat_collector.h  -- StatCollector 接口
- eqpalg/utility/stat_collector.cpp -- StatCollector 实现

修改:
- eqpalg/algs/exp_base.h  -- 替换 sta_ptr_ 为 stat_collector_
- eqpalg/algs/exp_base.cpp -- cron_proc/reload_ci_dist/reset_dev_data/
                               exec_task/task_mon_pro 委托给 StatCollector
2026-05-15 14:21:36 +08:00
Huamonarch
b9cf5f4e9e refactor: 提取 BoundChecker 上下限检测组件
从 ExpBase 提取 detect_up_down() 逻辑和哨兵值处理至独立的 BoundChecker 类。
将 DetectMode 从 struct 升级为 enum class。
2026-05-15 14:09:56 +08:00
Huamonarch
c4bcb6610b refactor: 删除旧反馈状态方法和标志(已由 FbStateMachine 替代) 2026-05-15 13:57:20 +08:00
Huamonarch
0106e553a0 refactor: 集成 FbStateMachine 到 ExpBase::mon_proc() 2026-05-15 13:36:30 +08:00
Huamonarch
fad4ee8e38 test: FbStateMachine 状态转换单元测试 2026-05-15 13:27:27 +08:00
Huamonarch
9f66fc10b3 feat: FbStateMachine 状态转换 + 变量操作实现 2026-05-15 13:22:16 +08:00
Huamonarch
f2f6d6ffb1 feat: 添加 FbStateMachine 头文件 2026-05-15 13:19:19 +08:00
Huamonarch
e9b2c178ba refactor: 删除 ExpModule 类(已被 ExpressionEngine 替代) 2026-05-15 13:15:05 +08:00
Huamonarch
3f8d281596 fix: 清理 ExpBase 子类中的 exp_act_/exp_feedback_/exp_result_ 残留引用
将 exp_bound、exp_times、exp_sample2D 中对已删除成员的引用替换为
expr_engine_->evaluate()/evaluateBool() 调用。
exp_sample2D 中原来绑定 exp_feedback_ 和 exp_result_ 的 sample_X/sample_Y
表达式现在通过 expr_engine_->registerExpression() 注册。
2026-05-15 13:14:17 +08:00
Huamonarch
38d0942a6c refactor: 非 ExpBase 算法适配 ExpressionEngine API 2026-05-15 13:08:30 +08:00
Huamonarch
7c2fe7f7bb refactor: ExpBase 表达式管理迁移到 ExpressionEngine 2026-05-15 12:59:11 +08:00
Huamonarch
96ca4d02bb refactor: AlgBase 从 ExpModule 迁移到 ExpressionEngine 2026-05-15 12:38:49 +08:00
Huamonarch
f10da49f65 test: ExpressionEngine 单元测试用例 2026-05-15 12:35:31 +08:00
Huamonarch
693d3b5197 feat: ExpressionEngine firstFill + printVars 实现 2026-05-15 12:32:20 +08:00
Huamonarch
e575efdc8a feat: ExpressionEngine FunVars 控制 + hold 变量管理 2026-05-15 12:30:11 +08:00
Huamonarch
50173683db feat: ExpressionEngine 变量刷新实现(MEMORY + IHDB) 2026-05-15 12:27:58 +08:00
Huamonarch
da9eb2ea2e feat: ExpressionEngine 核心实现(注册 + 求值) 2026-05-15 12:25:30 +08:00
Huamonarch
2f7ca1e123 feat: 添加 ExpressionEngine 头文件声明 2026-05-15 12:17:27 +08:00
Huamonarch
ec8e44ce5d test: 为 eqpalg 添加测试基础设施(test_harness + test_main) 2026-05-15 12:15:14 +08:00
37 changed files with 2873 additions and 1323 deletions

View File

@ -94,23 +94,35 @@ target_include_directories(
set_target_properties(eqpalg PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${bin_dir})
# ###################### add test ########################
# 1. loss_compress_test
#
# ##############################################################################
# include(../cmake_include/unit_test.cmake)
include(CTest)
# add_executable(loss_compress_test ${DISTRIBUTION} test/loss_compress_test.cc)
# target_link_libraries(loss_compress_test ${LINK_OPTION}
# Boost::unit_test_framework)
# target_include_directories(
# loss_compress_test PUBLIC ./ ../ ../../inc ../../inc/dbinc
# ${iPlature_include})
aux_source_directory(./test TEST_SOURCES)
# set_target_properties(loss_compress_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY
# ${UNIT_TEST_BIN_OUTPUT_DIR})
add_executable(eqpalg_test
${TEST_SOURCES}
./utility/expression_engine.cpp
./utility/fb_state_machine.cpp
./utility/bound_checker.cpp
)
# enable_testing()
# add_test(
# NAME loss_compress_test
# WORKING_DIRECTORY ${UNIT_TEST_BIN_OUTPUT_DIR}
# COMMAND loss_compress_test)
target_include_directories(eqpalg_test PUBLIC
./
../
${my_lib_include}
${iplature_include}
)
target_link_libraries(eqpalg_test
${LINK_OPTION}
${Boost_LIBRARIES}
${mix_cc}
nlohmann_json::nlohmann_json
Eigen3::Eigen
)
set_target_properties(eqpalg_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/test)
enable_testing()
add_test(NAME eqpalg_test
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/test
COMMAND eqpalg_test)

View File

@ -85,8 +85,7 @@ int AlgBase::init() {
ms = 20;
}
this->delay_time_ = milliseconds(ms);
exp_mpdule_ptr_ =
std::make_unique<ExpModule>(mm_vars, m_tags, is_exp_alg_);
expr_engine_ = std::make_unique<ExpressionEngine>(mm_vars, m_tags);
if (this->prr_ == 1) {
string exp_str = "";
@ -98,10 +97,8 @@ int AlgBase::init() {
logger_->Info() << "tmp_exp:" << tmp_exp << std::endl;
exp_str = get_macro_replaced_exp(tmp_exp);
logger_->Info() << "exp_str:" << exp_str << std::endl;
exp_mpdule_ptr_->add_exp("pre_result", exp_str);
logger_->Info() << "pre_result:"
<< exp_mpdule_ptr_->get_exp_str("pre_result")
<< std::endl;
expr_engine_->registerExpression("pre_result", exp_str);
logger_->Info() << "pre_result:" << exp_str << std::endl;
}
logger_->Debug() << "ruleid:" << this->rule_id_
<< ",rulename:" << this->rule_name_
@ -199,7 +196,7 @@ void AlgBase::exec_mon_call() {
if (alarm.alarmed) {
logger_->Debug() << alarm.content << endl;
alarm_poster_.alarm(alarm, &last_alarm_time_);
exp_mpdule_ptr_->fun_reset();
expr_engine_->forceResetFunVars();
} else {
if (get_save_data_cycled()) {
this->save_rule_norm_data();
@ -371,8 +368,12 @@ bool AlgBase::update_map_rule() {
bool AlgBase::get_prr() {
if (this->prr_ == 1) {
exp_mpdule_ptr_->update();
bool prr_result = (bool)exp_mpdule_ptr_->get_value("pre_result");
// 刷新共享内存变量
this->refresh_now_time();
mix_cc::time_range_t dummy_range;
expr_engine_->refreshFromMemory(this->now_time_, dummy_range);
bool prr_result = expr_engine_->evaluateBool("pre_result");
this->now_prr_ = prr_result;
return prr_result;
}

View File

@ -18,7 +18,7 @@
#include <eqpalg/define/error_code.h>
#include <eqpalg/define/public.h>
#include <eqpalg/gb_logger.h>
#include <eqpalg/utility/ExpModule.h>
#include <eqpalg/utility/expression_engine.h>
#include <eqpalg/utility/VarsCache.hpp>
#include <eqpalg/utility/XorShift128Plus.hpp>
#include <eqpalg/utility/alarm_poster.h>
@ -100,7 +100,7 @@ protected:
std::vector<ErrorCodeType> error_code_list_;
int prr_ = PRR::None;
std::unique_ptr<ExpModule> exp_mpdule_ptr_;
std::unique_ptr<ExpressionEngine> expr_engine_;
DataInfo data_info_;
@ -108,8 +108,6 @@ protected:
bool now_prr_ = false;
bool is_exp_alg_ = false;
int task_seq = 0;
TimeDur save_interval_ms_ = 5000ms;
TimeDur rule_state_update_interval_ms_ = 500ms;

66
eqpalg/algs/bound_alg.cpp Normal file
View File

@ -0,0 +1,66 @@
#include <eqpalg/algs/bound_alg.h>
#include <eqpalg/feature_extraction/daa.h>
#include <eqpalg/utility/build_alarm_info.h>
#include <shm/RuleStatShm.h>
BoundAlg::BoundAlg(const std::string& name, const mix_cc::json& rule_json,
const std::string& ruleId, size_t exp_type)
: ExpBase(name, rule_json, ruleId, exp_type) {
logger_.reset(new LOG("BoundAlg:" + rule_name_, AUTO_CATCH_PID));
}
BoundAlg::~BoundAlg() = default;
int BoundAlg::init() {
int ret = ExpBase::init();
if (ret == 0) {
exp_is_wrong_ = false;
}
return ret;
}
void BoundAlg::doInitExtend() {
// BoundAlg 额外的初始化:上下限
reload_config_up_down();
reload_ci_dist();
last_load_time_ = std::chrono::system_clock::now();
stat_collector_.configure(rule_id_, rule_name_, dist_mode_, is_learning_);
}
bool BoundAlg::checkFilter() {
if (!expr_engine_->exps_.count("feedback")) {
return true; // 无筛选表达式,所有数据参与
}
try {
return expr_engine_->evaluateBool("feedback");
} catch (...) {
return false;
}
}
AlarmInfo BoundAlg::doMonProc() {
double result_value = expr_engine_->evaluate("act");
filter_flag_ = false;
// 数据筛选
filter_flag_ = checkFilter();
// 自学习统计
if (is_learning_ && filter_flag_) {
rule_stat_.current_value = result_value;
SingletonTemp<EqpStat>::GetInstance().add_stat_values(rule_id_, result_value);
}
// 超限检测
if (filter_flag_ && bound_checker_.isOutOfBounds(result_value)) {
rule_stat_.alarm_value = result_value;
auto msg = error_str_ + ":" + DAA::double2str(result_value) + unit_ +
",合理区间:[" + DAA::double2strLimit(bound_checker_.limitDown()) +
"," + DAA::double2strLimit(bound_checker_.limitUp()) + "]" + unit_;
query_time_range_.set_left(query_time_range_.get_right() - delay_time_);
expr_engine_->markFunVarsNeedReset();
return utility::build_alarm_info(MsgLevel::ERROR, rule_id_, rule_name_,
"EXP2", msg, get_alarm_time());
}
return AlarmInfo{};
}

19
eqpalg/algs/bound_alg.h Normal file
View File

@ -0,0 +1,19 @@
#pragma once
#include <eqpalg/algs/exp_base.h>
class BoundAlg : public ExpBase {
public:
BoundAlg(const std::string& name, const mix_cc::json& rule_json,
const std::string& ruleId,
size_t exp_type = ExpType::Bound);
~BoundAlg() override;
int init() override;
protected:
AlarmInfo doMonProc() override;
void doInitExtend() override;
// 数据筛选检查
bool checkFilter();
};

View File

@ -0,0 +1,62 @@
#include <eqpalg/algs/bound_hold_alg.h>
#include <eqpalg/feature_extraction/daa.h>
#include <eqpalg/utility/build_alarm_info.h>
BoundHoldAlg::BoundHoldAlg(const std::string& name, const mix_cc::json& rule_json,
const std::string& ruleId)
: BoundAlg(name, rule_json, ruleId, ExpType::BoundHoldTime) {
logger_.reset(new LOG("BoundHoldAlg:" + rule_name_, AUTO_CATCH_PID));
}
BoundHoldAlg::~BoundHoldAlg() = default;
int BoundHoldAlg::init() {
int ret = BoundAlg::init();
if (ret == 0) {
reload_config_up_down_hold_time();
}
return ret;
}
AlarmInfo BoundHoldAlg::doMonProc() {
double result_value = expr_engine_->evaluate("act");
filter_flag_ = checkFilter();
if (is_learning_ && filter_flag_) {
rule_stat_.current_value = result_value;
SingletonTemp<EqpStat>::GetInstance().add_stat_values(rule_id_, result_value);
}
bool is_over = bound_checker_.isOutOfBounds(result_value);
if (!filter_flag_) {
act_start_time_ = now_time_;
act_started_ = false;
} else {
if (is_over) {
if (!act_started_) {
act_started_ = true;
act_start_time_ = now_time_;
}
if ((hold_time_ <= delay_time_) ||
(now_time_ - act_start_time_ > hold_time_)) {
rule_stat_.alarm_value = result_value;
auto msg = error_str_ + ":" + DAA::double2str(result_value) + unit_ +
",合理区间:[" + DAA::double2strLimit(bound_checker_.limitDown()) +
"," + DAA::double2strLimit(bound_checker_.limitUp()) + "]" + unit_;
query_time_range_.set_left(query_time_range_.get_right() - delay_time_);
expr_engine_->markFunVarsNeedReset();
act_start_time_ = now_time_;
act_started_ = false;
return utility::build_alarm_info(
utility::get_msg_level(bound_checker_.limitDown(),
bound_checker_.limitUp(), result_value),
rule_id_, rule_name_, "EXP5", msg, get_alarm_time());
}
} else {
act_start_time_ = now_time_;
act_started_ = false;
}
}
return AlarmInfo{};
}

View File

@ -0,0 +1,14 @@
#pragma once
#include <eqpalg/algs/bound_alg.h>
class BoundHoldAlg : public BoundAlg {
public:
BoundHoldAlg(const std::string& name, const mix_cc::json& rule_json,
const std::string& ruleId);
~BoundHoldAlg() override;
int init() override;
protected:
AlarmInfo doMonProc() override;
};

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,9 @@
#include <eqpalg/utility/HoldTime.h>
#include <eqpalg/utility/StatExp.hpp>
#include <eqpalg/utility/item2chinese.hpp>
#include <eqpalg/utility/bound_checker.h>
#include <eqpalg/utility/stat_collector.h>
#include <eqpalg/utility/fb_state_machine.h>
#include <glob/SingletonTemplate.h>
#include <iomanip>
#include <iostream>
@ -93,13 +96,24 @@ public:
*
* (T_RULE_SAMPLE_1D,T_RULE_SAMPLE_1D_INFO,T_SAMPLE_RECORDT_SAMPLE_RECORDT_RULE_SAMPLE_FEATURE)rule_id_对应数据
* cron----
* sta_ptr_(sta_ptr_.reset())
* stat_collector_(stat_collector_.reset())
*/
virtual void reset_dev_data() override;
bool get_prr() override;
protected:
/**
* @brief
* @return AlarmInfo
*/
virtual AlarmInfo doMonProc() = 0;
/**
* @brief
*/
virtual void doInitExtend() {}
/**
* @brief
* @return int
@ -117,7 +131,6 @@ protected:
*/
bool get_cycled_cron();
private:
/**
* @brief
* @return int
@ -137,59 +150,12 @@ private:
*/
int reload_config_exp_feedback();
protected:
/**
* @brief
* @return int
*/
int refresh_exp_vars_mem();
/**
* @brief ihyperDB查询到的数据载入到表达式变量中
* @param row
* @return int
*/
int refresh_exp_vars_ihd(int row);
/**
* @brief map
* @return int
*/
int first_fill_mm_vars();
/**
* @brief hold变量hold变量有hold(n,T)
* hold(n,T),ntag的序号T
* @return int
*/
int refresh_hold_var();
protected:
/**
* @brief
* @param expstr My Param doc
*/
void print_exp_vars(const string &expstr = "");
protected:
string exp_str_;
string error_str_;
bool feedback_done_ = false;
string unit_;
/**
* @brief
* KeepT , ///< 保持时间
* KeepC, ///<出现次数
* RiseEdge, ///<上升沿出现次数
* Detect ///<为真检测次数
*/
StatExp::FunVars fun_vars_;
std::unique_ptr<mix_cc::matheval::Expression> exp_act_;
std::unique_ptr<mix_cc::matheval::Expression> exp_feedback_;
std::unique_ptr<mix_cc::matheval::Expression> exp_result_;
protected:
const size_t
@ -199,14 +165,10 @@ protected:
TimePoint act_start_time_;
bool act_triggered_ = false;
bool act_started_ =
false;
bool feedback_triggered_ = false;
bool m_timemode = false;
FbStateMachine fb_fsm_;
int refresh_counts_ = 0;
protected:
@ -220,7 +182,7 @@ protected:
TimeDur hold_time_ = 0ms;
std::unique_ptr<DAA::STA> sta_ptr_;
StatCollector stat_collector_;
std::map<std::string, std::unique_ptr<HoldTime>>
hold_times_;
@ -243,46 +205,12 @@ protected:
std::string sample_result_;
int detect_mode_ = DetectMode::Default;
DetectMode detect_mode_ = DetectMode::Default;
BoundChecker bound_checker_;
bool filter_flag_ = false;
bool is_fun_vars_need_reset_ = false;
protected:
/**
* @brief
* s{tagN}
* 使
* @return true
* @return false
*/
bool act_start_done();
/**
* @brief
* 使退
* @return true
* @return false
*/
bool act_not_hold();
/**
* @brief
*
* @return true
* @return false
*/
bool act_done();
/**
* @brief
* 使
* @return true
* @return false
*/
bool act_timeout();
protected:
/**
@ -290,24 +218,6 @@ protected:
*/
void query_ihd_data();
/**
* @brief
* @return AlarmInfo
*/
AlarmInfo get_timeout_alarm();
/**
* @brief hold(n,T)hold变量map初始化
* @param exp_str My Param doc
* @return int
*/
int init_hold_exp_str(const std::string &exp_str);
/**
* @brief
* @param exp_str My Param doc
* @return int
*/
int exp_messy_code_check(const std::string &exp_str);
/**
* @brief
* @return int
@ -355,14 +265,5 @@ protected:
*/
bool task_prr(int row);
/**
* @brief fun_vars_
* fun_vars_替换
*/
void fun_vars_init();
/**
* @brief
* is_fun_vars_need_reset_
*/
void auto_fun_vars_reset();
};

View File

@ -52,13 +52,13 @@ ExpBound::~ExpBound() {
AlarmInfo ExpBound::mon_proc() {
if ((bool)exp_feedback_->evaluate() == false) {
if (expr_engine_->evaluateBool("feedback") == false) {
logger_->Debug() << "前提条件不满足!" << std::endl;
return AlarmInfo{};
}
/*最新数据*/
double now_value = exp_act_->evaluate();
double now_value = expr_engine_->evaluate("act");
rule_stat_.current_value = now_value;
/*报警检查*/

View File

@ -48,7 +48,7 @@ AlarmInfo ExpSample2D::mon_proc() {
} else {
switch (exp_type_) {
case ExpType::PolyFit:
if (exp_act_->evaluate() && check_polyFit()) {
if (expr_engine_->evaluateBool("act") && check_polyFit()) {
this->rule_stat_.alarm_value = this->rule_stat_.current_value;
auto msg = rule_name_ + this->error_str_ + " Y表达式当前值" +
DAA::double2str(this->rule_stat_.current_value) +
@ -95,38 +95,26 @@ AlarmInfo ExpSample2D::mon_proc() {
int ExpSample2D::reload_samples() {
/*
sample_X绑定 exp_feedback_
sample_Y绑定 exp_result_
sample_X绑定 expr_engine_->evaluate("sample_X")
sample_Y绑定 expr_engine_->evaluate("sample_Y")
*/
auto tmp_exp =
rule_json_.at("function").at("sample_X").at("value").get<std::string>();
exp_str_ = get_macro_replaced_exp(tmp_exp);
if (exp_feedback_ == nullptr && exp_str_ != "") {
try {
exp_feedback_ =
std::make_unique<mix_cc::matheval::Expression>(exp_str_, &mm_vars);
if (exp_str_ != "") {
int ret = expr_engine_->registerExpression("sample_X", exp_str_);
if (ret != 0) return -1;
logger_->Debug() << "sample_X:" << exp_str_ << "="
<< exp_feedback_->evaluate() << endl;
} catch (const std::exception &e) {
logger_->Error() << "sample_X:" << exp_str_ << "计算出错:" << e.what()
<< ",location:" << BOOST_CURRENT_LOCATION << endl;
return -1;
}
<< expr_engine_->evaluate("sample_X") << endl;
}
tmp_exp =
rule_json_.at("function").at("sample_Y").at("value").get<std::string>();
exp_str_ = get_macro_replaced_exp(tmp_exp);
if (exp_result_ == nullptr && exp_str_ != "") {
try {
exp_result_ =
std::make_unique<mix_cc::matheval::Expression>(exp_str_, &mm_vars);
if (exp_str_ != "") {
int ret = expr_engine_->registerExpression("sample_Y", exp_str_);
if (ret != 0) return -1;
logger_->Debug() << "sample_Y:" << exp_str_ << "="
<< exp_result_->evaluate() << endl;
} catch (const std::exception &e) {
logger_->Error() << "sample_Y:" << exp_str_ << "计算出错:" << e.what()
<< ",location:" << BOOST_CURRENT_LOCATION << endl;
return -1;
}
<< expr_engine_->evaluate("sample_Y") << endl;
}
tmp_exp = rule_json_.at("function")
@ -173,8 +161,8 @@ int ExpSample2D::reload_samples() {
}
bool ExpSample2D::check_polyFit() {
double X = exp_feedback_->evaluate(); /* SampleX*/
double Y = exp_result_->evaluate(); /* SampleY*/
double X = expr_engine_->evaluate("sample_X"); /* SampleX*/
double Y = expr_engine_->evaluate("sample_Y"); /* SampleY*/
double Y_Fit = PolyFitValue(X, this->fit_coefs_, this->orders_);
limit_down_ = Y_Fit - abs(Y_Fit) * scale_;
limit_up_ = Y_Fit + abs(Y_Fit) * scale_;
@ -207,9 +195,9 @@ double ExpSample2D::PolyFitValue(double x, std::vector<double> &fit_coefs,
bool ExpSample2D::check_pear() {
if (this->data_len_ < min_len_) {
if (exp_act_->evaluate()) {
SampleX_.push_back(exp_feedback_->evaluate());
SampleY_.push_back(exp_result_->evaluate());
if (expr_engine_->evaluateBool("act")) {
SampleX_.push_back(expr_engine_->evaluate("sample_X"));
SampleY_.push_back(expr_engine_->evaluate("sample_Y"));
data_len_ = SampleX_.size();
} else {
reset_SampleXY();
@ -431,9 +419,9 @@ void ExpSample2D::task_mon_pro() {
for (auto i = 0; i < queried_data_.rows(); i++) {
refresh_exp_vars_ihd(i);
if (data_len_ < MAX_STORAGE_SIZE) {
if (exp_act_->evaluate()) {
SampleX_.push_back(exp_feedback_->evaluate());
SampleY_.push_back(exp_result_->evaluate());
if (expr_engine_->evaluateBool("act")) {
SampleX_.push_back(expr_engine_->evaluate("sample_X"));
SampleY_.push_back(expr_engine_->evaluate("sample_Y"));
data_len_ = SampleX_.size();
}
} else {

View File

@ -198,7 +198,7 @@ int ExpTimes::update_times() {
return -1;
}
this->act_triggered_ = this->exp_act_->evaluate();
this->act_triggered_ = expr_engine_->evaluateBool("act");
/*出现次数累计*/
if (this->exp_type_ == ExpType::OccTimesAcc) {
if (this->act_triggered_) {

View File

@ -0,0 +1,116 @@
#include <eqpalg/algs/feedback_alg.h>
#include <eqpalg/feature_extraction/daa.h>
#include <eqpalg/utility/build_alarm_info.h>
#include <shm/RuleStatShm.h>
FeedbackAlg::FeedbackAlg(const std::string& name, const mix_cc::json& rule_json,
const std::string& ruleId, size_t exp_type)
: ExpBase(name, rule_json, ruleId, exp_type),
hasBoundCheck_(exp_type == ExpType::CondBound) {
logger_.reset(new LOG("FeedbackAlg:" + rule_name_, AUTO_CATCH_PID));
}
FeedbackAlg::~FeedbackAlg() = default;
void FeedbackAlg::doInitExtend() {
if (hasBoundCheck_) {
reload_config_up_down();
reload_ci_dist();
last_load_time_ = std::chrono::system_clock::now();
stat_collector_.configure(rule_id_, rule_name_, dist_mode_, is_learning_);
}
}
AlarmInfo FeedbackAlg::doMonProc() {
double result_value = expr_engine_->evaluate("act");
// CondBound: filter + statistics
if (hasBoundCheck_) {
bool filter_ok = true;
try {
filter_ok = expr_engine_->evaluateBool("feedback");
} catch (...) {}
if (is_learning_ && filter_ok) {
rule_stat_.current_value = result_value;
SingletonTemp<EqpStat>::GetInstance().add_stat_values(rule_id_, result_value);
}
}
bool triggered = static_cast<bool>(result_value);
// === FbStateMachine ===
auto [fbState, needFunReset] = fb_fsm_.update(
triggered, now_time_, expr_engine_->vars(), m_tags.size());
if (needFunReset) {
expr_engine_->markFunVarsNeedReset();
}
if (fbState == FbState::Started) {
query_time_range_.set_left(now_time_);
return AlarmInfo{};
}
if (fbState == FbState::NotHold) {
return AlarmInfo{};
}
if (fbState == FbState::InProgress) {
bool fbCond = expr_engine_->evaluateBool("feedback");
bool done = fb_fsm_.checkFeedback(fbCond, now_time_, expr_engine_->vars());
if (done) {
query_time_range_.set_left(
query_time_range_.get_right() -
std::chrono::milliseconds(static_cast<int>(expr_engine_->vars()["time"])));
result_value = expr_engine_->evaluate("result");
expr_engine_->markFunVarsNeedReset();
rule_stat_.limit_down = bound_checker_.limitDown();
rule_stat_.limit_up = bound_checker_.limitUp();
rule_stat_.current_value = result_value;
if (hasBoundCheck_) {
if (is_learning_) {
SingletonTemp<EqpStat>::GetInstance().add_stat_values(
rule_id_, result_value);
}
if (bound_checker_.isOutOfBounds(result_value)) {
rule_stat_.alarm_value = result_value;
std::string msg;
if (fb_fsm_.isTimeMode()) {
msg = error_str_ + ":" + DAA::double2str(result_value) +
"ms,时间范围:[0," + DAA::double2str(bound_checker_.limitUp()) + "] ms";
} else {
msg = error_str_ + ":" + DAA::double2str(result_value) + unit_ +
",合理区间:[" + DAA::double2strLimit(bound_checker_.limitDown()) +
"," + DAA::double2strLimit(bound_checker_.limitUp()) + "]" + unit_;
}
return utility::build_alarm_info(
utility::get_msg_level(bound_checker_.limitDown(),
bound_checker_.limitUp(), result_value),
rule_id_, rule_name_, "EXP4", msg, get_alarm_time());
}
} else {
if (static_cast<bool>(result_value)) {
rule_stat_.alarm_value = result_value;
auto msg = rule_name_ + " " + error_str_;
return utility::build_alarm_info(MsgLevel::ERROR, rule_id_,
rule_name_, "EXP3", msg,
get_alarm_time());
}
}
}
return AlarmInfo{};
}
if (fbState == FbState::Timeout) {
if (fb_fsm_.isTimeMode()) {
std::string msg = rule_name_ + " 反馈超时:" +
std::to_string(time_out_.count()) + " ms";
return utility::build_alarm_info(MsgLevel::ERROR, rule_id_, rule_name_,
"EXPACT", msg, query_time_range_);
}
return AlarmInfo{};
}
return AlarmInfo{};
}

View File

@ -0,0 +1,16 @@
#pragma once
#include <eqpalg/algs/exp_base.h>
class FeedbackAlg : public ExpBase {
public:
FeedbackAlg(const std::string& name, const mix_cc::json& rule_json,
const std::string& ruleId, size_t exp_type);
~FeedbackAlg() override;
protected:
AlarmInfo doMonProc() override;
void doInitExtend() override;
private:
bool hasBoundCheck_; // CondBound (exp_type 4) vs Logic (exp_type 3)
};

View File

@ -78,7 +78,7 @@ AlarmInfo GlitchDetection::exec_mon() {
data_index_ = 0;
run_time_range_.set_left(this->now_time_);
}
data_[data_index_] = exp_mpdule_ptr_->get_value("dataX");
data_[data_index_] = expr_engine_->evaluate("dataX");
data_index_++;
return out_alarm;
}
@ -117,7 +117,7 @@ int GlitchDetection::load_exp() {
auto tmp_exp =
rule_json_.at("function").at("dataX").at("value").get<std::string>();
exp_str_ = get_macro_replaced_exp(tmp_exp);
exp_mpdule_ptr_->add_exp("dataX", exp_str_);
expr_engine_->registerExpression("dataX", exp_str_);
/*数据长度*/
auto tmp_data_size = rule_json_.at("function")
.at("dataX")
@ -137,8 +137,10 @@ int GlitchDetection::load_exp() {
}
bool GlitchDetection::get_prr() {
if (this->prr_ == 1) {
exp_mpdule_ptr_->update();
bool prr_result = (bool)exp_mpdule_ptr_->get_value("pre_result");
this->refresh_now_time();
mix_cc::time_range_t dummy_range;
expr_engine_->refreshFromMemory(this->now_time_, dummy_range);
bool prr_result = expr_engine_->evaluateBool("pre_result");
this->now_prr_ = prr_result;
logger_->Debug() << "ruleid:" << this->rule_id_
<< ",rulename:" << this->rule_name_

24
eqpalg/algs/logic_alg.cpp Normal file
View File

@ -0,0 +1,24 @@
#include <eqpalg/algs/logic_alg.h>
#include <eqpalg/utility/build_alarm_info.h>
LogicAlg::LogicAlg(const std::string& name, const mix_cc::json& rule_json,
const std::string& ruleId)
: ExpBase(name, rule_json, ruleId, ExpType::Logic) {
logger_.reset(new LOG("LogicAlg:" + rule_name_, AUTO_CATCH_PID));
}
LogicAlg::~LogicAlg() = default;
AlarmInfo LogicAlg::doMonProc() {
double result_value = expr_engine_->evaluate("act");
if (static_cast<bool>(result_value)) {
rule_stat_.alarm_value = result_value;
auto msg = rule_name_ + " " + error_str_;
query_time_range_.set_left(query_time_range_.get_right() - delay_time_);
expr_engine_->markFunVarsNeedReset();
return utility::build_alarm_info(MsgLevel::ERROR, rule_id_, rule_name_,
"EXP1", msg, get_alarm_time());
}
return AlarmInfo{};
}

12
eqpalg/algs/logic_alg.h Normal file
View File

@ -0,0 +1,12 @@
#pragma once
#include <eqpalg/algs/exp_base.h>
class LogicAlg : public ExpBase {
public:
LogicAlg(const std::string& name, const mix_cc::json& rule_json,
const std::string& ruleId);
~LogicAlg() override;
protected:
AlarmInfo doMonProc() override;
};

View File

@ -71,7 +71,7 @@ Roller3::~Roller3() {
AlarmInfo Roller3::mon_proc() {
if ((bool)exp_act_->evaluate() == false) {
if (!expr_engine_->evaluateBool("act")) {
logger_->Debug() << "前提条件不满足!" << std::endl;
return AlarmInfo{};
}
@ -227,9 +227,7 @@ int Roller3::init_X_exp() {
exp_str_ = get_macro_replaced_exp(tmp_exp);
res += init_hold_exp_str(exp_str_);
auto fun_res = fun_vars_.add_exp_str(exp_str_, &mm_vars);
res += fun_res.first ? 0 : -1;
exp_str_ = fun_res.second;
// ExpressionEngine::registerExpression() now handles FunVars processing internally.
feedback_mode_ = false;
auto messy_code = exp_messy_code_check(exp_str_);
if (messy_code == -1) {
@ -305,31 +303,19 @@ int Roller3::init_X_exp() {
}
}
value_num_ = tag_seq_.size();
if (exp_act_ == nullptr && exp_str_ != "") {
try {
exp_act_ =
std::make_unique<mix_cc::matheval::Expression>(exp_str_, &mm_vars);
logger_->Debug() << "exp_act:" << exp_str_ << "=" << exp_act_->evaluate()
<< endl;
} catch (const std::exception &e) {
logger_->Error() << "exp_act:" << exp_str_ << "计算出错:" << e.what()
<< ",location:" << BOOST_CURRENT_LOCATION << endl;
if (exp_str_ != "") {
int reg_ret = expr_engine_->registerExpression("act", exp_str_);
if (reg_ret != 0) {
this->error_code_list_.push_back(
{ErrorType::CalError, ErrorLocation::ActExp});
return -1;
}
}
if (exp_result_ == nullptr && tags_exp_ != "") {
try {
exp_result_ =
std::make_unique<mix_cc::matheval::Expression>(tags_exp_, &mm_vars);
logger_->Debug() << "tags_exp_:" << tags_exp_ << "="
<< exp_result_->evaluate() << endl;
} catch (const std::exception &e) {
logger_->Error() << "tags_exp_:" << tags_exp_ << "计算出错:" << e.what()
<< ",location:" << BOOST_CURRENT_LOCATION << endl;
if (tags_exp_ != "") {
int reg_ret = expr_engine_->registerExpression("result", tags_exp_);
if (reg_ret != 0) {
this->error_code_list_.push_back(
{ErrorType::CalError, ErrorLocation::ActExp});
{ErrorType::CalError, ErrorLocation::ResultExp});
return -1;
}
}

View File

@ -182,8 +182,8 @@ std::vector<AlarmInfo> TrendSlope2::exec_task(mix_cc::time_range_t time_range) {
bool TrendSlope2::get_prr2() {
if (this->prr_ == 1) {
exp_mpdule_ptr_->update(queried_data_, queried_time_);
bool prr_result = (bool)exp_mpdule_ptr_->get_value("pre_result");
expr_engine_->refreshFromIhdRow(0, queried_data_, queried_time_, now_time_, query_time_range_);
bool prr_result = expr_engine_->evaluateBool("pre_result");
this->now_prr_ = prr_result;
logger_->Debug() << "ruleid:" << this->rule_id_
<< ",rulename:" << this->rule_name_

View File

@ -190,8 +190,8 @@ std::vector<AlarmInfo> TrendSlope3::exec_task(mix_cc::time_range_t time_range) {
bool TrendSlope3::get_prr2() {
if (this->prr_ == 1) {
exp_mpdule_ptr_->update(queried_data_, queried_time_);
bool prr_result = (bool)exp_mpdule_ptr_->get_value("pre_result");
expr_engine_->refreshFromIhdRow(0, queried_data_, queried_time_, now_time_, query_time_range_);
bool prr_result = expr_engine_->evaluateBool("pre_result");
this->now_prr_ = prr_result;
logger_->Debug() << "ruleid:" << this->rule_id_
<< ",rulename:" << this->rule_name_

View File

@ -1,13 +1,15 @@
#include <eqpalg/algs/roller2.h>
#include <eqpalg/algs/trend_slope2.h>
#include <eqpalg/build_algorithm.h>
// for default case
#include <eqpalg/algs/exp_base.h>
#include <eqpalg/algs/bound_alg.h>
#include <eqpalg/algs/bound_hold_alg.h>
#include <eqpalg/algs/exp_bound.h>
#include <eqpalg/algs/exp_sample2D.h>
#include <eqpalg/algs/exp_times.h>
#include <eqpalg/algs/fault_code.h>
#include <eqpalg/algs/feedback_alg.h>
#include <eqpalg/algs/glitch_detection.h>
#include <eqpalg/algs/logic_alg.h>
#include <eqpalg/algs/null.h>
#include <eqpalg/algs/roller3.h>
#include <eqpalg/algs/trend_slope3.h>
@ -19,11 +21,19 @@ std::unique_ptr<AlgBase> build_algorithm(int algId, const string &ruleId,
LOG d("build_algorithm");
switch (algId) {
case 1:
return std::make_unique<LogicAlg>(name, rule_json, ruleId);
break;
case 2:
return std::make_unique<BoundAlg>(name, rule_json, ruleId);
break;
case 3:
return std::make_unique<FeedbackAlg>(name, rule_json, ruleId, algId);
break;
case 4:
return std::make_unique<FeedbackAlg>(name, rule_json, ruleId, algId);
break;
case 5:
return std::make_unique<ExpBase>(name, rule_json, ruleId, algId);
return std::make_unique<BoundHoldAlg>(name, rule_json, ruleId);
break;
case 6:
case 7:

View File

@ -57,15 +57,7 @@ struct DistMode {
static const int Online = 1;
static const int Offline = 2;
};
/**
* @brief
*/
struct DetectMode {
static const int Default = 0;
static const int OnlyLeft = 1;
static const int OnlyRight = 2;
static const int ErrorMode = 3;
};
// DetectMode 已提取至 eqpalg/utility/bound_checker.henum class
/**
* @brief
*/

View File

@ -0,0 +1,915 @@
// eqpalg/test/test_algorithms.cc
// 核心算法单元测试 — LogicAlg/BoundAlg/BoundHoldAlg/FeedbackAlg/ExpTimes/FaultCode/Roller3
//
// 设计说明:
// 算法子类 (LogicAlg/BoundAlg/...) 的构造函数依赖 AlgBase::init()
// 而后者又依赖 CMemVar共享内存等运行时基础设施在开发机上可能不可用。
// 因此本测试文件不尝试完整构造算法对象,而是:
// 1. 测试已从算法中提取出的独立组件ExpressionEngine, BoundChecker, FbStateMachine
// 2. 用独立的辅助函数测试算法特有的核心逻辑bit 提取、tag 号解析、中位数计算等)
// 3. 通过组件组合模拟 doMonProc() 的控制流,验证集成行为
//
// 已提取组件在 eqpalg/test/ 下的其他文件中已有完整单元测试:
// - test_expression_engine.cc — ExpressionEngine
// - test_fb_state_machine.cc — FbStateMachine
// 本文件在这些已有覆盖的基础上增加算法级集成测试和算法特有逻辑的测试。
#include "test_harness.h"
#include <eqpalg/utility/expression_engine.h>
#include <eqpalg/utility/bound_checker.h>
#include <eqpalg/utility/fb_state_machine.h>
#include <mix_cc/json.h>
#include <algorithm>
#include <chrono>
#include <cctype>
#include <map>
#include <string>
#include <vector>
using json = mix_cc::json;
// ============================================================================
// 测试辅助:构建各算法类型的合法 JSON 配置
// ============================================================================
// 构建 LogicAlg (ExpType::Logic = 1) 的最小合法 JSON
static json makeLogicConfig() {
return json::parse(R"({
"tags": {"tag1": {"value": "TEST_TAG"}},
"trigger": {"value": "100"},
"function": {
"result": {"value": "tag1 > 0.5"}
},
"output": {
"error": {"value": "逻辑异常"}
}
})");
}
// 构建 BoundAlg (ExpType::Bound = 2) 的最小合法 JSON
static json makeBoundConfig() {
return json::parse(R"({
"tags": {"tag1": {"value": "TEST_TAG"}},
"trigger": {"value": "100"},
"datasource": {"value": "1"},
"function": {
"result": {
"value": "tag1",
"param": {
"limit_down": {"value": "0"},
"limit_up": {"value": "100"},
"unit": {"value": "C"}
}
}
},
"output": {
"error": {"value": "温度超限"}
}
})");
}
// 构建 FeedbackAlg 逻辑型 (ExpType::FeedbackLogic = 3) 的最小合法 JSON
static json makeFeedbackLogicConfig() {
return json::parse(R"({
"tags": {"tag1": {"value": "TEST_TAG"}},
"trigger": {"value": "100"},
"function": {
"action_start": {"value": "tag1 > 0.5"},
"action_end": {
"value": "tag1 < 0.1",
"param": {
"timeout": {"value": "60000"},
"hold": {"value": "0"}
}
},
"result": {"value": "1"}
},
"output": {
"error": {"value": "反馈动作报警"}
}
})");
}
// 构建 FeedbackAlg 上下限型 (ExpType::FeedbackBound = 4) 的最小合法 JSON
static json makeFeedbackBoundConfig() {
return json::parse(R"({
"tags": {"tag1": {"value": "TEST_TAG"}},
"trigger": {"value": "100"},
"datasource": {"value": "1"},
"function": {
"action_start": {
"value": "tag1",
"param": {
"limit_down": {"value": "0"},
"limit_up": {"value": "100"},
"unit": {"value": "C"}
}
},
"action_end": {
"value": "tag1 < 50",
"param": {
"timeout": {"value": "60000"},
"hold": {"value": "0"}
}
},
"result": {"value": "1"}
},
"output": {
"error": {"value": "温度反馈报警"}
}
})");
}
// 构建 BoundHoldAlg (ExpType::BoundHold = 5) 的最小合法 JSON
static json makeBoundHoldConfig() {
return json::parse(R"({
"tags": {"tag1": {"value": "TEST_TAG"}},
"trigger": {"value": "100"},
"datasource": {"value": "1"},
"function": {
"result": {
"value": "tag1",
"param": {
"limit_down": {"value": "0"},
"limit_up": {"value": "100"},
"unit": {"value": "C"},
"hold_time": {"value": "5000"}
}
}
},
"output": {
"error": {"value": "温度持续超限"}
}
})");
}
// ============================================================================
// 表达式测试辅助环境(与 test_expression_engine.cc 一致)
// ============================================================================
struct TestEnv {
std::map<std::string, double> vars;
std::vector<std::string> tags;
ExpressionEngine engine;
TestEnv() : engine(vars, tags) {
tags = {"tag1", "tag2"};
vars["tag1"] = 10.0;
vars["tag2"] = 20.0;
vars["p1"] = 10.0;
vars["p2"] = 20.0;
vars["now"] = 0;
vars["stime"] = 0;
vars["time"] = 0;
vars["etime"] = 0;
}
};
// ============================================================================
// 第一部分LogicAlg 核心逻辑测试
// ============================================================================
// LogicAlg::doMonProc() 的核心逻辑:
// 1. exp_act_->evaluate() → bool (trigger)
// 2. trigger==true → alarm
// 3. trigger==false → 无 alarm
// 下面通过 ExpressionEngine 模拟此流程。
TEST(logic_alg_trigger_true_produces_alarm) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 1.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1 > 0.5");
// trigger 为 true → 应产生报警
bool triggered = engine.evaluateBool("act");
CHECK_EQ(triggered, true);
}
TEST(logic_alg_trigger_false_no_alarm) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 0.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1 > 0.5");
bool triggered = engine.evaluateBool("act");
CHECK_EQ(triggered, false);
}
TEST(logic_alg_complex_expression) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1", "tag2"};
vars["tag1"] = 3.0;
vars["tag2"] = 7.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1 > 2 && tag2 < 10");
CHECK_EQ(engine.evaluateBool("act"), true);
}
TEST(logic_alg_and_short_circuit_false) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1", "tag2"};
vars["tag1"] = 0.0;
vars["tag2"] = 7.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1 > 2 && tag2 < 10");
CHECK_EQ(engine.evaluateBool("act"), false);
}
TEST(logic_alg_or_expression) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1", "tag2"};
vars["tag1"] = 0.0;
vars["tag2"] = 100.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1 > 2 || tag2 > 50");
CHECK_EQ(engine.evaluateBool("act"), true);
}
TEST(logic_alg_negation_operator) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 0.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "!tag1");
CHECK_EQ(engine.evaluateBool("act"), true);
}
// ============================================================================
// 第二部分BoundAlg — BoundChecker 检测器测试
// ============================================================================
// BoundAlg::doMonProc() 使用 BoundChecker 判断 value 是否超出 [down, up]。
// 哨兵值 -32768 表示"无下限"32767/32768 表示"无上限"。
TEST(bound_checker_detects_out_of_upper) {
BoundChecker bc;
bc.setLimits(0.0, 100.0);
CHECK_EQ(bc.isOutOfBounds(150.0), true);
CHECK_EQ(bc.isOutOfBounds(50.0), false);
}
TEST(bound_checker_detects_out_of_lower) {
BoundChecker bc;
bc.setLimits(10.0, 100.0);
CHECK_EQ(bc.isOutOfBounds(5.0), true);
}
TEST(bound_checker_only_right_mode_sentinel) {
BoundChecker bc;
bc.setLimits(-32768.0, 100.0); // -32768 = no lower bound
CHECK_EQ(bc.detectMode(), DetectMode::OnlyRight);
CHECK_EQ(bc.isOutOfBounds(-999.0), false); // below lower is OK
CHECK_EQ(bc.isOutOfBounds(150.0), true); // above upper triggers
}
TEST(bound_checker_only_left_mode_sentinel) {
BoundChecker bc;
bc.setLimits(10.0, 32767.0); // 32767 = no upper bound
CHECK_EQ(bc.detectMode(), DetectMode::OnlyLeft);
CHECK_EQ(bc.isOutOfBounds(5.0), true); // below lower triggers
CHECK_EQ(bc.isOutOfBounds(999.0), false); // above upper is OK (sentinel)
}
TEST(bound_checker_only_left_mode_sentinel_32768) {
BoundChecker bc;
bc.setLimits(10.0, 32768.0); // 32768 also = no upper bound
CHECK_EQ(bc.detectMode(), DetectMode::OnlyLeft);
CHECK_EQ(bc.isOutOfBounds(5.0), true);
CHECK_EQ(bc.isOutOfBounds(999.0), false);
}
TEST(bound_checker_default_bilateral) {
BoundChecker bc;
bc.setLimits(10.0, 20.0);
CHECK_EQ(bc.detectMode(), DetectMode::Default);
CHECK_EQ(bc.isOutOfBounds(5.0), true);
CHECK_EQ(bc.isOutOfBounds(15.0), false);
CHECK_EQ(bc.isOutOfBounds(25.0), true);
}
TEST(bound_checker_error_mode) {
BoundChecker bc;
bc.setLimits(-32768.0, -32768.0); // both sentinels
CHECK_EQ(bc.detectMode(), DetectMode::ErrorMode);
CHECK_EQ(bc.isOutOfBounds(50.0), false); // error mode: never alarm
}
TEST(bound_checker_exact_boundary_default) {
BoundChecker bc;
bc.setLimits(10.0, 20.0);
CHECK_EQ(bc.isOutOfBounds(10.0), false); // exactly on lower bound
CHECK_EQ(bc.isOutOfBounds(20.0), false); // exactly on upper bound
CHECK_EQ(bc.isOutOfBounds(10.00001), false);
}
TEST(bound_checker_boundary_only_left) {
BoundChecker bc;
bc.setLimits(10.0, 32767.0);
CHECK_EQ(bc.isOutOfBounds(10.0), false);
CHECK_EQ(bc.isOutOfBounds(9.999), true);
}
TEST(bound_checker_boundary_only_right) {
BoundChecker bc;
bc.setLimits(-32768.0, 100.0);
CHECK_EQ(bc.isOutOfBounds(100.0), false);
CHECK_EQ(bc.isOutOfBounds(100.001), true);
}
TEST(bound_checker_isValid) {
BoundChecker bc1;
bc1.setLimits(0.0, 100.0);
CHECK_EQ(bc1.isValid(), true);
BoundChecker bc2;
bc2.setLimits(-32768.0, -32768.0);
CHECK_EQ(bc2.isValid(), false);
}
TEST(bound_checker_setDetectMode_override) {
BoundChecker bc;
bc.setLimits(-32768.0, 100.0);
CHECK_EQ(bc.detectMode(), DetectMode::OnlyRight);
// manual override to Default
bc.setDetectMode(DetectMode::Default);
CHECK_EQ(bc.detectMode(), DetectMode::Default);
// now both sides checked with sentinel values → out of lower triggers
CHECK_EQ(bc.isOutOfBounds(-999.0), true);
}
// ============================================================================
// 第三部分BoundAlg 过滤表达式测试
// ============================================================================
// BoundAlg::checkFilter() 使用 exp_feedback_ 作为过滤条件,
// 仅当条件为 true 时才进入统计累积。
TEST(bound_alg_filter_expression) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 150.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1");
engine.registerExpression("feedback", "tag1 > 200"); // filter
// tag1=150 → filter is false → no statistics
bool filter_ok = engine.evaluateBool("feedback");
CHECK_EQ(filter_ok, false);
// tag1=250 → filter is true
vars["tag1"] = 250.0;
filter_ok = engine.evaluateBool("feedback");
CHECK_EQ(filter_ok, true);
}
TEST(bound_alg_filter_boundary) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 200.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("feedback", "tag1 >= 200");
// exactly at boundary
CHECK_EQ(engine.evaluateBool("feedback"), true);
vars["tag1"] = 199.999;
CHECK_EQ(engine.evaluateBool("feedback"), false);
}
// ============================================================================
// 第四部分BoundHoldAlg 保持时间逻辑测试
// ============================================================================
// BoundHoldAlg::doMonProc() 要求 value 持续超出限值 hold_time 后才报警。
TEST(bound_hold_time_logic) {
double hold_time_ms = 5000.0; // 5 second hold
auto start = std::chrono::system_clock::now();
// 首次检测 — 尚未到达保持时间
auto now1 = start;
auto elapsed1 = std::chrono::duration_cast<std::chrono::milliseconds>(now1 - start);
CHECK(elapsed1 < std::chrono::milliseconds(static_cast<int>(hold_time_ms)));
// 达到保持时间后 — 可以报警
auto now2 = start + std::chrono::milliseconds(static_cast<int>(hold_time_ms) + 100);
auto elapsed2 = std::chrono::duration_cast<std::chrono::milliseconds>(now2 - start);
CHECK(elapsed2 > std::chrono::milliseconds(static_cast<int>(hold_time_ms)));
// hold_time <= delay_time → 立即报警
bool alarm_with_short = (std::chrono::milliseconds(50) <= std::chrono::milliseconds(50)) ||
(elapsed1 > std::chrono::milliseconds(static_cast<int>(hold_time_ms)));
CHECK_EQ(alarm_with_short, true);
// Long hold — should alarm after time
bool alarm_after_hold = elapsed2 > std::chrono::milliseconds(static_cast<int>(hold_time_ms));
CHECK_EQ(alarm_after_hold, true);
}
TEST(bound_hold_zero_hold_time) {
double hold_time_ms = 0.0; // no hold → immediate alarm
auto start = std::chrono::system_clock::now();
auto now = start + std::chrono::milliseconds(1);
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - start);
// hold_time == 0 → always alarm immediately
CHECK(elapsed >= std::chrono::milliseconds(static_cast<int>(hold_time_ms)));
}
TEST(bound_hold_long_delay) {
double hold_time_ms = 60000.0; // 60 seconds
auto start = std::chrono::system_clock::now();
// before hold time expires
auto now1 = start + std::chrono::milliseconds(30000);
auto elapsed1 = std::chrono::duration_cast<std::chrono::milliseconds>(now1 - start);
CHECK(elapsed1 < std::chrono::milliseconds(static_cast<int>(hold_time_ms)));
// after hold time expires
auto now2 = start + std::chrono::milliseconds(61000);
auto elapsed2 = std::chrono::duration_cast<std::chrono::milliseconds>(now2 - start);
CHECK(elapsed2 > std::chrono::milliseconds(static_cast<int>(hold_time_ms)));
}
// ============================================================================
// 第五部分FeedbackAlg / FbStateMachine 集成测试
// ============================================================================
// FbStateMachine 的完整单元测试在 test_fb_state_machine.cc 中。
// 本部分测试 FeedbackAlg::doMonProc() 的整体流程 ——
// ExpressionEngine (exp_act_ + exp_feedback_) + FbStateMachine 的组合行为。
TEST(feedback_full_flow_start_to_done) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 1.0;
vars["p1"] = 1.0;
vars["now"] = 0;
vars["stime"] = 0;
vars["time"] = 0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1 > 0.5");
engine.registerExpression("feedback", "tag1 < 0.1");
engine.registerExpression("result", "1");
FbStateMachine fsm;
fsm.configure(false, std::chrono::milliseconds(60000));
auto now = std::chrono::system_clock::now();
vars["now"] = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()).count();
// Step 1: Trigger → Started
bool triggered = engine.evaluateBool("act");
CHECK_EQ(triggered, true);
auto [state1, reset1] = fsm.update(triggered, now, vars, tags.size());
CHECK(state1 == FbState::Started);
CHECK_EQ(reset1, false);
// Step 2: Continue → InProgress (may take 2 cycles from Started)
auto [state2, reset2] = fsm.update(true, now, vars, tags.size());
if (state2 == FbState::Started) {
// need one more cycle
auto [state2b, reset2b] = fsm.update(true, now, vars, tags.size());
CHECK(state2b == FbState::InProgress || state2b == FbState::Started);
state2 = state2b;
(void)reset2b;
}
CHECK(state2 == FbState::InProgress);
CHECK_EQ(reset2, false);
}
TEST(feedback_full_flow_not_hold) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 1.0;
vars["p1"] = 1.0;
vars["now"] = 0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1 > 0.5");
FbStateMachine fsm;
fsm.configure(true, std::chrono::milliseconds(60000)); // keep_mode=true
auto now = std::chrono::system_clock::now();
vars["now"] = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()).count();
// Start the action (two cycles to reach InProgress)
fsm.update(true, now, vars, tags.size());
auto state2 = fsm.update(true, now, vars, tags.size()).state;
if (state2 == FbState::Started) {
state2 = fsm.update(true, now, vars, tags.size()).state;
}
// Lose trigger with keep_mode → NotHold
auto [state, reset] = fsm.update(false, now, vars, tags.size());
if (state == FbState::NotHold) {
CHECK_EQ(reset, true);
}
// InProgress is also valid if keep_mode transition hasn't triggered yet
}
TEST(feedback_timeout_scenario) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 1.0;
vars["p1"] = 1.0;
vars["now"] = 0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1 > 0.5");
FbStateMachine fsm;
fsm.configure(false, std::chrono::milliseconds(100)); // short 100ms timeout
auto now = std::chrono::system_clock::now();
vars["now"] = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()).count();
// Trigger → Started → InProgress
fsm.update(true, now, vars, tags.size());
auto state = fsm.update(true, now, vars, tags.size()).state;
if (state == FbState::Started) {
state = fsm.update(true, now, vars, tags.size()).state;
}
CHECK(state == FbState::InProgress || state == FbState::Timeout);
// Advance past timeout
now += std::chrono::milliseconds(200);
vars["now"] = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()).count();
auto result = fsm.update(true, now, vars, tags.size());
CHECK(result.state == FbState::Timeout);
CHECK_EQ(result.funVarsNeedReset, true);
}
// ============================================================================
// 第六部分ExpTimes 累积逻辑测试
// ============================================================================
// ExpTimes::update_times() 在 exp_act_ 为 true 时累加次数/时间。
TEST(exp_times_occurrence_counting_logic) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 1.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1");
int shear_times = 0;
int max_times = 5;
// 模拟 6 个周期,每个周期 exp_act_ 为 true
for (int i = 0; i < 6; i++) {
if (engine.evaluateBool("act")) {
shear_times++;
}
}
CHECK_EQ(shear_times, 6);
CHECK_EQ(shear_times > max_times, true); // 应报警
}
TEST(exp_times_occurrence_below_threshold) {
std::map<std::string, double> vars;
std::vector<std::string> tags = {"tag1"};
vars["tag1"] = 1.0;
ExpressionEngine engine(vars, tags);
engine.registerExpression("act", "tag1 > 0.5");
int shear_times = 0;
int max_times = 5;
for (int i = 0; i < 3; i++) {
if (engine.evaluateBool("act")) {
shear_times++;
}
}
CHECK_EQ(shear_times, 3);
CHECK_EQ(shear_times > max_times, false); // under threshold, no alarm
}
TEST(exp_times_time_accumulation_logic) {
// 模拟时间累积:触发→累积时间→取消触发→停止
bool act_started = false;
double running_time = 0.0;
auto start_time = std::chrono::system_clock::now();
// 触发变为 true
act_started = true;
auto t1 = std::chrono::system_clock::now();
// 时间推进
auto t2 = t1 + std::chrono::milliseconds(5000);
double elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count();
running_time += elapsed / (60.0 * 60000); // ms → hours
// 触发变为 false
act_started = false;
CHECK_FLOAT_EQ(running_time, 5000.0 / 3600000.0, 0.0001);
CHECK(running_time > 0.0);
}
TEST(exp_times_time_multiple_intervals) {
double running_time = 0.0;
auto t0 = std::chrono::system_clock::now();
// Interval 1: 3000ms
auto t1 = t0 + std::chrono::milliseconds(3000);
running_time += std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count()
/ (60.0 * 60000);
// Interval 2: 7000ms
auto t2 = t1 + std::chrono::milliseconds(7000);
running_time += std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count()
/ (60.0 * 60000);
double expected = 10000.0 / 3600000.0;
CHECK_FLOAT_EQ(running_time, expected, 0.0001);
}
// ============================================================================
// 第七部分FaultCode 位提取 (gitbit) 测试
// ============================================================================
// FaultCode::exec_mon() 使用 gitbit 模板函数解析故障代码的各个 bit。
template <typename T>
static T gitbit(T value, T sub) {
return int(value) >> int(sub) & 1;
}
TEST(fault_code_gitbit_extraction) {
// Bit 3 of 0b00001000 = 8
CHECK_EQ(gitbit(8, 3), 1);
CHECK_EQ(gitbit(8, 0), 0);
CHECK_EQ(gitbit(8, 1), 0);
CHECK_EQ(gitbit(8, 2), 0);
// Bit 0 of 1
CHECK_EQ(gitbit(1, 0), 1);
// Complex code: 0b1010 = 10
CHECK_EQ(gitbit(10, 0), 0); // bit 0
CHECK_EQ(gitbit(10, 1), 1); // bit 1
CHECK_EQ(gitbit(10, 2), 0); // bit 2
CHECK_EQ(gitbit(10, 3), 1); // bit 3
// All bits 0-15 for code=0
for (int i = 0; i < 16; i++) {
CHECK_EQ(gitbit(0, i), 0);
}
}
TEST(fault_code_bit_parsing_all_bits) {
// A code with multiple bits set: 0b1111 = 15
int code = 15;
int set_bits = 0;
for (int i = 0; i < 16; i++) {
if (gitbit(code, i)) set_bits++;
}
CHECK_EQ(set_bits, 4); // bits 0,1,2,3
}
TEST(fault_code_bit_15_set) {
// A code with bit 15 set: 0x8000 = 32768
int code = 32768;
CHECK_EQ(gitbit(code, 15), 1);
CHECK_EQ(gitbit(code, 0), 0);
CHECK_EQ(gitbit(code, 7), 0);
}
TEST(fault_code_all_bits_set_16bit) {
// 0xFFFF = 65535, all lower 16 bits set
int code = 65535;
int set_bits = 0;
for (int i = 0; i < 16; i++) {
if (gitbit(code, i)) set_bits++;
}
CHECK_EQ(set_bits, 16);
}
TEST(fault_code_single_bit_each_position) {
// Verify each individual bit position
for (int i = 0; i < 16; i++) {
int code = 1 << i;
CHECK_EQ(gitbit(code, i), 1);
// all other bits should be 0
for (int j = 0; j < 16; j++) {
if (j != i) {
CHECK_EQ(gitbit(code, j), 0);
}
}
}
}
TEST(fault_code_gitbit_with_different_types) {
// Test with unsigned types (FaultCode handles various integer types)
CHECK_EQ(gitbit<unsigned int>(8u, 3u), 1u);
CHECK_EQ(gitbit<unsigned int>(8u, 0u), 0u);
CHECK_EQ(gitbit<short>(1, 0), 1);
CHECK_EQ(gitbit<long>(32768L, 15L), 1L);
}
// ============================================================================
// 第八部分Roller3 辅助函数测试
// ============================================================================
// extractTagNumbers — 从表达式中提取 "tagN" 的数字序号
// calculateMedian — 计算数组的中位数
static std::vector<int> extractTagNumbers(const std::string& expr) {
std::vector<int> result;
const std::string tagPrefix = "tag";
size_t pos = 0;
while (pos < expr.size()) {
size_t found = expr.find(tagPrefix, pos);
if (found == std::string::npos) break;
size_t numStart = found + tagPrefix.size();
size_t numEnd = numStart;
while (numEnd < expr.size() && isdigit(expr[numEnd])) numEnd++;
if (numEnd > numStart) {
std::string numStr = expr.substr(numStart, numEnd - numStart);
result.push_back(std::stoi(numStr));
}
pos = numEnd;
}
return result;
}
static double calculateMedian(std::vector<double> data) {
if (data.empty()) return 0.0;
size_t size = data.size();
std::sort(data.begin(), data.end());
if (size % 2 == 0) {
return (data[size / 2 - 1] + data[size / 2]) / 2.0;
} else {
return data[size / 2];
}
}
TEST(roller3_extract_tag_numbers) {
auto tags = extractTagNumbers("tag1+tag2+tag3");
CHECK_EQ(tags.size(), 3u);
CHECK_EQ(tags[0], 1);
CHECK_EQ(tags[1], 2);
CHECK_EQ(tags[2], 3);
}
TEST(roller3_extract_tag_numbers_single) {
auto tags = extractTagNumbers("tag5");
CHECK_EQ(tags.size(), 1u);
CHECK_EQ(tags[0], 5);
}
TEST(roller3_extract_tag_numbers_complex) {
auto tags = extractTagNumbers("tag10+tag2+tag35");
CHECK_EQ(tags.size(), 3u);
bool has10 = std::find(tags.begin(), tags.end(), 10) != tags.end();
bool has2 = std::find(tags.begin(), tags.end(), 2) != tags.end();
bool has35 = std::find(tags.begin(), tags.end(), 35) != tags.end();
CHECK_EQ(has10, true);
CHECK_EQ(has2, true);
CHECK_EQ(has35, true);
}
TEST(roller3_extract_empty_string) {
auto tags = extractTagNumbers("");
CHECK_EQ(tags.size(), 0u);
}
TEST(roller3_extract_no_tags) {
auto tags = extractTagNumbers("no_tags_here");
CHECK_EQ(tags.size(), 0u);
}
TEST(roller3_extract_tag_numbers_with_prefix) {
// "tag" embedded in other words should still be found
auto tags = extractTagNumbers("metatag1+tag2");
// extractTagNumbers searches for "tag" substring, so "metatag1" → "tag1" → 1
CHECK_EQ(tags.size(), 2u);
bool has1 = std::find(tags.begin(), tags.end(), 1) != tags.end();
bool has2 = std::find(tags.begin(), tags.end(), 2) != tags.end();
CHECK_EQ(has1, true);
CHECK_EQ(has2, true);
}
TEST(roller3_extract_large_tag_numbers) {
auto tags = extractTagNumbers("tag999+tag1000");
CHECK_EQ(tags.size(), 2u);
CHECK_EQ(tags[0], 999);
CHECK_EQ(tags[1], 1000);
}
TEST(roller3_calculate_median_odd) {
CHECK_FLOAT_EQ(calculateMedian({1.0, 3.0, 5.0}), 3.0, 0.001);
CHECK_FLOAT_EQ(calculateMedian({10.0, 2.0, 8.0}), 8.0, 0.001);
}
TEST(roller3_calculate_median_even) {
CHECK_FLOAT_EQ(calculateMedian({1.0, 3.0, 5.0, 7.0}), 4.0, 0.001);
CHECK_FLOAT_EQ(calculateMedian({1.0, 2.0}), 1.5, 0.001);
}
TEST(roller3_calculate_median_single) {
CHECK_FLOAT_EQ(calculateMedian({42.0}), 42.0, 0.001);
}
TEST(roller3_calculate_median_empty) {
CHECK_FLOAT_EQ(calculateMedian({}), 0.0, 0.001);
}
TEST(roller3_calculate_median_negative_values) {
CHECK_FLOAT_EQ(calculateMedian({-5.0, -1.0, -3.0}), -3.0, 0.001);
CHECK_FLOAT_EQ(calculateMedian({-10.0, 0.0, 10.0, 20.0}), 5.0, 0.001);
}
TEST(roller3_calculate_median_large_dataset) {
std::vector<double> data;
for (int i = 1; i <= 99; i++) {
data.push_back(static_cast<double>(i));
}
CHECK_FLOAT_EQ(calculateMedian(data), 50.0, 0.001);
}
TEST(roller3_calculate_median_unsorted_preserves_order) {
// verify the function sorts internally
std::vector<double> data = {5.0, 3.0, 1.0, 4.0, 2.0};
CHECK_FLOAT_EQ(calculateMedian(data), 3.0, 0.001);
}
// ============================================================================
// JSON 配置校验
// ============================================================================
// 验证辅助函数构造的 JSON 结构是否合法且包含必要字段。
TEST(json_config_logic_alg_structure) {
json cfg = makeLogicConfig();
CHECK(cfg.contains("tags"));
CHECK(cfg.contains("trigger"));
CHECK(cfg.contains("function"));
CHECK(cfg.contains("output"));
CHECK(cfg.at("function").contains("result"));
CHECK(cfg.at("function").at("result").contains("value"));
CHECK(cfg.at("output").contains("error"));
}
TEST(json_config_bound_alg_structure) {
json cfg = makeBoundConfig();
CHECK(cfg.contains("tags"));
CHECK(cfg.contains("function"));
CHECK(cfg.at("function").contains("result"));
CHECK(cfg.at("function").at("result").contains("param"));
auto& param = cfg.at("function").at("result").at("param");
CHECK(param.contains("limit_down"));
CHECK(param.contains("limit_up"));
}
TEST(json_config_feedback_logic_structure) {
json cfg = makeFeedbackLogicConfig();
CHECK(cfg.contains("function"));
CHECK(cfg.at("function").contains("action_start"));
CHECK(cfg.at("function").contains("action_end"));
// action_end should have param with timeout
CHECK(cfg.at("function").at("action_end").contains("param"));
CHECK(cfg.at("function").at("action_end").at("param").contains("timeout"));
}
TEST(json_config_feedback_bound_structure) {
json cfg = makeFeedbackBoundConfig();
CHECK(cfg.at("function").contains("action_start"));
CHECK(cfg.at("function").contains("action_end"));
// action_start should have bound params
auto& start = cfg.at("function").at("action_start");
CHECK(start.contains("param"));
CHECK(start.at("param").contains("limit_down"));
CHECK(start.at("param").contains("limit_up"));
}
TEST(json_config_bound_hold_structure) {
json cfg = makeBoundHoldConfig();
auto& param = cfg.at("function").at("result").at("param");
CHECK(param.contains("hold_time"));
CHECK(param.contains("limit_down"));
CHECK(param.contains("limit_up"));
}

View File

@ -0,0 +1,140 @@
// eqpalg/test/test_expression_engine.cc
#include "test_harness.h"
#include <eqpalg/utility/expression_engine.h>
#include <map>
#include <string>
#include <vector>
// 测试辅助:构建最小环境
struct TestEnv {
std::map<std::string, double> vars;
std::vector<std::string> tags;
ExpressionEngine engine;
TestEnv() : engine(vars, tags) {
tags = {"tag1", "tag2"};
vars["tag1"] = 10.0;
vars["tag2"] = 20.0;
vars["p1"] = 10.0;
vars["p2"] = 20.0;
vars["now"] = 0;
vars["stime"] = 0;
vars["time"] = 0;
vars["etime"] = 0;
}
};
// ==================== 注册与求值 ====================
TEST(register_and_evaluate_simple_expression) {
TestEnv env;
int ret = env.engine.registerExpression("test", "tag1 + tag2");
CHECK_EQ(ret, 0);
CHECK_FLOAT_EQ(env.engine.evaluate("test"), 30.0, 0.001);
CHECK_EQ(env.engine.evaluateBool("test"), true);
}
TEST(register_duplicate_is_idempotent) {
TestEnv env;
CHECK_EQ(env.engine.registerExpression("dup", "tag1"), 0);
CHECK_EQ(env.engine.registerExpression("dup", "tag2"), 0);
CHECK_FLOAT_EQ(env.engine.evaluate("dup"), 10.0, 0.001);
}
TEST(evaluate_unregistered_returns_zero) {
TestEnv env;
CHECK_FLOAT_EQ(env.engine.evaluate("nonexistent"), 0.0, 0.001);
CHECK_EQ(env.engine.evaluateBool("nonexistent"), false);
}
TEST(evaluate_zero_expression) {
TestEnv env;
CHECK_EQ(env.engine.registerExpression("zero", "0"), 0);
CHECK_FLOAT_EQ(env.engine.evaluate("zero"), 0.0, 0.001);
CHECK_EQ(env.engine.evaluateBool("zero"), false);
}
TEST(evaluate_boolean_true_constant) {
TestEnv env;
CHECK_EQ(env.engine.registerExpression("one", "1"), 0);
CHECK_EQ(env.engine.evaluateBool("one"), true);
}
// ==================== 变量变化反映到求值 ====================
TEST(variable_change_reflected_in_evaluation) {
TestEnv env;
CHECK_EQ(env.engine.registerExpression("x", "tag1"), 0);
CHECK_FLOAT_EQ(env.engine.evaluate("x"), 10.0, 0.001);
env.vars["tag1"] = 100.0;
CHECK_FLOAT_EQ(env.engine.evaluate("x"), 100.0, 0.001);
}
// ==================== 多表达式并存 ====================
TEST(multiple_expressions_independent) {
TestEnv env;
CHECK_EQ(env.engine.registerExpression("a", "tag1"), 0);
CHECK_EQ(env.engine.registerExpression("b", "tag2"), 0);
CHECK_EQ(env.engine.registerExpression("sum", "tag1 + tag2"), 0);
CHECK_FLOAT_EQ(env.engine.evaluate("a"), 10.0, 0.001);
CHECK_FLOAT_EQ(env.engine.evaluate("b"), 20.0, 0.001);
CHECK_FLOAT_EQ(env.engine.evaluate("sum"), 30.0, 0.001);
env.vars["tag1"] = 100.0;
CHECK_FLOAT_EQ(env.engine.evaluate("a"), 100.0, 0.001);
CHECK_FLOAT_EQ(env.engine.evaluate("sum"), 120.0, 0.001);
}
// ==================== registerRawExpression ====================
TEST(register_raw_no_funvars_processing) {
TestEnv env;
CHECK_EQ(env.engine.registerRawExpression("raw", "tag1 + tag2"), 0);
CHECK_FLOAT_EQ(env.engine.evaluate("raw"), 30.0, 0.001);
}
// ==================== FunVars 重置控制 ====================
TEST(autoReset_does_nothing_when_not_marked) {
TestEnv env;
CHECK_EQ(env.engine.registerExpression("test", "tag1"), 0);
// 无标记时 autoReset 什么都不做
env.engine.autoResetFunVars();
CHECK_FLOAT_EQ(env.engine.evaluate("test"), 10.0, 0.001);
}
TEST(mark_and_autoReset_sequence) {
TestEnv env;
CHECK_EQ(env.engine.registerExpression("test", "tag1"), 0);
env.engine.markFunVarsNeedReset();
env.engine.autoResetFunVars(); // 执行重置并清除标记
// 第二次应该是 no-op
env.engine.autoResetFunVars();
}
TEST(forceReset_works) {
TestEnv env;
CHECK_EQ(env.engine.registerExpression("test", "tag1"), 0);
env.engine.forceResetFunVars();
}
// ==================== 复杂表达式 ====================
TEST(comparison_expression) {
TestEnv env;
CHECK_EQ(env.engine.registerExpression("cmp", "tag1 > tag2"), 0);
CHECK_EQ(env.engine.evaluateBool("cmp"), false);
env.vars["tag1"] = 30.0;
CHECK_EQ(env.engine.evaluateBool("cmp"), true);
}
TEST(arithmetic_with_comparison) {
TestEnv env;
CHECK_EQ(env.engine.registerExpression("arith", "tag1 * 2 + tag2"), 0);
CHECK_FLOAT_EQ(env.engine.evaluate("arith"), 40.0, 0.001);
}

View File

@ -0,0 +1,197 @@
// eqpalg/test/test_fb_state_machine.cc
#include "test_harness.h"
#include <eqpalg/utility/fb_state_machine.h>
#include <chrono>
#include <map>
#include <string>
using namespace std::chrono;
// 测试辅助:构建最小环境
struct FbTestEnv {
std::map<std::string, double> vars;
FbStateMachine fsm;
TimePoint now;
FbTestEnv(size_t tag_count = 2) {
// 模拟已初始化的变量状态
for (size_t i = 0; i < tag_count; i++) {
std::string idx = std::to_string(i + 1);
vars["tag" + idx] = 0.0;
vars["p" + idx] = 0.0;
vars["now"] = 0;
}
now = std::chrono::system_clock::now();
vars["now"] = duration_cast<milliseconds>(now.time_since_epoch()).count();
}
void setTag(int n, double val) {
std::string idx = std::to_string(n);
vars["p" + idx] = vars["tag" + idx]; // p = old tag
vars["tag" + idx] = val;
}
void advanceTime(int ms) {
now += milliseconds(ms);
vars["now"] = duration_cast<milliseconds>(now.time_since_epoch()).count();
}
FbUpdateResult step(bool trigger) {
return fsm.update(trigger, now, vars, 2);
}
};
// ==================== 初始状态 ====================
TEST(initial_state_is_idle) {
FbTestEnv env;
CHECK(env.fsm.currentState() == FbState::Idle);
CHECK_EQ(env.fsm.isActive(), false);
}
// ==================== Idle → Started ====================
TEST(idle_to_started_when_triggered) {
FbTestEnv env;
auto result = env.step(true);
CHECK(result.state == FbState::Started);
CHECK_EQ(env.fsm.isActive(), true);
CHECK_EQ(result.funVarsNeedReset, false);
}
TEST(idle_stays_idle_when_not_triggered) {
FbTestEnv env;
auto result = env.step(false);
CHECK(result.state == FbState::Idle);
CHECK_EQ(result.funVarsNeedReset, true); // 空闲时重置 fun_vars
}
// ==================== Started → InProgress → Done ====================
TEST(started_proceeds_to_done_via_feedback) {
FbTestEnv env;
// 首次触发 → Started
auto r1 = env.step(true);
CHECK(r1.state == FbState::Started);
// Started 自动推进到 InProgress第二次 update
auto r2 = env.step(true);
CHECK(r2.state == FbState::InProgress || r2.state == FbState::Started);
// 如果第二次还在 Started第三次一定到 InProgress
if (r2.state == FbState::Started) {
r2 = env.step(true);
}
CHECK(r2.state == FbState::InProgress);
// 反馈条件满足 → Done
bool done = env.fsm.checkFeedback(true, env.now, env.vars);
CHECK_EQ(done, true);
CHECK(env.fsm.currentState() == FbState::Idle); // 自动回到 Idle
}
// ==================== NotHold ====================
TEST(keep_mode_not_hold_when_trigger_lost) {
FbTestEnv env;
env.fsm.configure(true, milliseconds(600000)); // keep_mode=true
// 触发 → Started
env.step(true);
// → InProgress
env.step(true);
// 触发丢失 → NotHold
auto r = env.step(false);
CHECK(r.state == FbState::NotHold);
CHECK_EQ(r.funVarsNeedReset, true);
}
// ==================== Timeout ====================
TEST(timeout_when_exceeds_limit) {
FbTestEnv env;
env.fsm.configure(false, milliseconds(100)); // 100ms timeout
// 触发 → Started → InProgress
env.step(true);
env.step(true);
// 超时
env.advanceTime(200);
auto r = env.step(true);
CHECK(r.state == FbState::Timeout);
CHECK_EQ(r.funVarsNeedReset, true);
}
// ==================== 变量快照 ====================
TEST(snapshot_on_action_start) {
FbTestEnv env;
env.setTag(1, 3.0);
env.setTag(2, 7.0);
env.step(true); // → Started快照发生
CHECK_FLOAT_EQ(env.vars["s1"], 3.0, 0.001);
CHECK_FLOAT_EQ(env.vars["s2"], 7.0, 0.001);
CHECK_FLOAT_EQ(env.vars["time"], 0.0, 0.001);
CHECK_FLOAT_EQ(env.vars["mv2_tag1"], 0.0, 0.001);
// stime 应该被设置
CHECK(env.vars["stime"] > 0);
}
// ==================== 变量累积 ====================
TEST(action_vars_accumulate_in_progress) {
FbTestEnv env;
env.setTag(1, 1.0); // tag1=1
env.setTag(2, 0.0);
env.step(true); // Started
env.step(true); // InProgress (累积发生)
CHECK_EQ(env.vars["mv2_tag1"] >= 1.0, true); // tag1==1 被计数
CHECK_FLOAT_EQ(env.vars["mx_tag1"], 1.0, 0.001);
CHECK_FLOAT_EQ(env.vars["mi_tag2"], 0.0, 0.001);
}
// ==================== forceReset ====================
TEST(force_reset_returns_to_idle) {
FbTestEnv env;
env.step(true); // Started
CHECK_EQ(env.fsm.isActive(), true);
env.fsm.forceReset();
CHECK(env.fsm.currentState() == FbState::Idle);
CHECK_EQ(env.fsm.isActive(), false);
}
// ==================== 非 keep 模式下触发丢失 ====================
TEST(no_keep_mode_trigger_loss_stays_in_progress) {
FbTestEnv env;
env.fsm.configure(false, milliseconds(600000)); // keep_mode=false
env.step(true); // Started
env.step(true); // InProgress
auto r = env.step(false); // 触发丢失但 keep_mode=false
CHECK(r.state == FbState::InProgress); // 不退出
}
// ==================== 终端状态自动回归 Idle ====================
TEST(terminal_state_returns_to_idle_next_cycle) {
FbTestEnv env;
env.fsm.configure(false, milliseconds(10));
env.step(true);
env.step(true);
env.advanceTime(20);
auto r = env.step(true);
CHECK(r.state == FbState::Timeout);
// 下一周期自动回到 Idle
auto r2 = env.step(false);
CHECK(r2.state == FbState::Idle);
}

View File

@ -0,0 +1,84 @@
// eqpalg/test/test_harness.h
#pragma once
#include <cmath>
#include <functional>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
struct TestRunner {
struct Case {
const char* name;
std::function<void()> fn;
};
inline static std::vector<Case> cases;
static int run() {
int passed = 0, failed = 0;
for (auto& c : cases) {
std::cout << " " << c.name << " ... ";
try {
c.fn();
std::cout << "PASSED\n";
passed++;
} catch (const std::exception& e) {
std::cout << "FAILED\n " << e.what() << "\n";
failed++;
} catch (...) {
std::cout << "FAILED (unknown)\n";
failed++;
}
}
std::cout << "\n" << passed << " passed, " << failed << " failed\n";
return failed ? 1 : 0;
}
};
struct AutoReg {
AutoReg(const char* name, std::function<void()> fn) {
TestRunner::cases.push_back({name, std::move(fn)});
}
};
#define TEST(name) \
static void testfn_##name(); \
static AutoReg autoreg_##name(#name, testfn_##name); \
static void testfn_##name()
#define CHECK(expr) \
do { \
if (!(expr)) \
throw std::runtime_error("CHECK(" #expr ") failed"); \
} while (0)
#define CHECK_EQ(a, b) \
do { \
if ((a) != (b)) { \
std::ostringstream os; \
os << "CHECK_EQ: " << (a) << " != " << (b); \
throw std::runtime_error(os.str()); \
} \
} while (0)
#define CHECK_FLOAT_EQ(a, b, eps) \
do { \
if (std::fabs((a) - (b)) > (eps)) { \
std::ostringstream os; \
os << "CHECK_FLOAT_EQ: |" << (a) << " - " << (b) << "| > " << (eps); \
throw std::runtime_error(os.str()); \
} \
} while (0)
#define CHECK_THROWS(expr) \
do { \
bool caught = false; \
try { \
(void)(expr); \
} catch (...) { \
caught = true; \
} \
if (!caught) \
throw std::runtime_error("CHECK_THROWS(" #expr ") did not throw"); \
} while (0)

7
eqpalg/test/test_main.cc Normal file
View File

@ -0,0 +1,7 @@
// eqpalg/test/test_main.cc
#include "test_harness.h"
int main() {
std::cout << "ExpressionEngine Tests\n======================\n\n";
return TestRunner::run();
}

View File

@ -1,138 +0,0 @@
#include <eqpalg/gb_item_memory.h>
#include <eqpalg/utility/ExpModule.h>
#include <glob/SingletonTemplate.h>
ExpModule::ExpModule(std::map<std::string, double> &vars_ref,
vector<string> &m_tags, bool &is_exp_alg_ref)
: mm_vars(vars_ref), m_tags(m_tags), is_exp_alg_(is_exp_alg_ref) {
logger_ = std::make_unique<LOG>("ExpModule");
init();
}
ExpModule::~ExpModule() {}
int ExpModule::add_exp(string exp_name, string exp_str) {
if (exp_str_.find(exp_name) == exp_str_.end()) {
exp_str_[exp_name] = exp_str;
fun_vars_.add_exp_str(exp_str, &mm_vars);
exp_ptr_[exp_name] = std::make_unique<MExp>(exp_str, &mm_vars);
try {
exp_result_[exp_name] = exp_ptr_[exp_name]->evaluate();
} catch (const std::exception &e) {
logger_->Error() << "add_exp:" << e.what() << std::endl;
}
}
}
int ExpModule::update() {
if (is_exp_alg_ != true) {
this->refresh_exp_vars_mem();
this->fun_vars_.refresh_fun_vars(false, &mm_vars);
}
for (auto item : this->exp_str_) {
exp_result_[item.first] = exp_ptr_[item.first]->evaluate();
}
}
int ExpModule::update(
Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic> queried_data,
std::vector<TimePoint> queried_time, int row) {
if (is_exp_alg_ != true) {
this->refresh_exp_ihd_mem(queried_data, queried_time, row);
this->fun_vars_.refresh_fun_vars(false, &mm_vars);
}
for (auto item : this->exp_str_) {
exp_result_[item.first] = exp_ptr_[item.first]->evaluate();
}
}
void ExpModule::refresh_exp_ihd_mem(
Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic> queried_data,
std::vector<TimePoint> queried_time, int row) {
if (queried_data.rows() == 0 || queried_data.cols() == 0) {
logger_->Error() << "refresh_exp_ihd_mem: queried_data is empty"
<< std::endl;
return;
}
for (unsigned int i = 0; i < m_tags.size(); i++) {
if (row >= queried_data.rows() || i >= (unsigned int)queried_data.cols()) {
continue;
}
mm_vars["p" + std::to_string(i + 1)] =
mm_vars["tag" + std::to_string(i + 1)];
mm_vars["tag" + std::to_string(i + 1)] = queried_data(row, i);
mm_vars["now"] = mix_cc::mix_time_t(queried_time[row]).to_milliseconds();
auto pv_str = "pv" + std::to_string(i + 1);
mm_vars[pv_str + "_5"] = mm_vars[pv_str + "_4"];
mm_vars[pv_str + "_4"] = mm_vars[pv_str + "_3"];
mm_vars[pv_str + "_3"] = mm_vars[pv_str + "_2"];
mm_vars[pv_str + "_2"] = mm_vars[pv_str + "_1"];
mm_vars[pv_str + "_1"] = mm_vars[pv_str + "_0"];
mm_vars[pv_str + "_0"] = mm_vars["tag" + std::to_string(i + 1)];
}
}
void ExpModule::refresh_exp_vars_mem() {
this->now_time_ = std::chrono::system_clock::now();
for (unsigned int i = 0; i < m_tags.size(); i++) {
mm_vars["p" + std::to_string(i + 1)] =
mm_vars["tag" + std::to_string(i + 1)];
mm_vars["tag" + std::to_string(i + 1)] =
SingletonTemplate<GlobaltemSharedMemory>::GetInstance()[m_tags[i]];
auto pv_str = "pv" + std::to_string(i + 1);
mm_vars[pv_str + "_5"] = mm_vars[pv_str + "_4"];
mm_vars[pv_str + "_4"] = mm_vars[pv_str + "_3"];
mm_vars[pv_str + "_3"] = mm_vars[pv_str + "_2"];
mm_vars[pv_str + "_2"] = mm_vars[pv_str + "_1"];
mm_vars[pv_str + "_1"] = mm_vars[pv_str + "_0"];
mm_vars[pv_str + "_0"] = mm_vars["tag" + std::to_string(i + 1)];
}
mm_vars["now"] =
duration_cast<milliseconds>(now_time_.time_since_epoch()).count();
}
void ExpModule::init() {
this->now_time_ = std::chrono::system_clock::now();
for (unsigned int i = 0; i < m_tags.size(); i++) {
double value =
SingletonTemplate<GlobaltemSharedMemory>::GetInstance()[m_tags[i]];
mm_vars["p" + std::to_string(i + 1)] = value;
mm_vars["tag" + std::to_string(i + 1)] = value;
mm_vars["now"] =
duration_cast<milliseconds>(now_time_.time_since_epoch()).count();
auto pv_str = "pv" + std::to_string(i + 1);
mm_vars[pv_str + "_0"] = value;
mm_vars[pv_str + "_1"] = value;
mm_vars[pv_str + "_2"] = value;
mm_vars[pv_str + "_3"] = value;
mm_vars[pv_str + "_4"] = value;
mm_vars[pv_str + "_5"] = value;
}
}
double ExpModule::get_value(string exp_name) {
try {
return exp_result_[exp_name];
} catch (const std::exception &e) {
logger_->Error() << "get_value:" << e.what() << std::endl;
return 0;
}
}
void ExpModule::print_value() {
logger_->Debug() << "mm_vars:";
for (auto item : mm_vars) {
logger_->Debug() << " " << item.first << ":" << item.second;
}
logger_->Debug() << std::endl;
}
string ExpModule::get_exp_str(string exp_name) {
if (exp_str_.find(exp_name) == exp_str_.end()) {
return "";
}
return exp_str_[exp_name];
}
void ExpModule::fun_reset() { fun_vars_.refresh_fun_vars(true, &mm_vars); }

View File

@ -1,112 +0,0 @@
#pragma once
/**
* @file ExpModule.h
* @brief
* @author your name (you@domain.com)
* @version 0.1
* @date 2025-08-20
*
* Copyright: Baosight Co. Ltd.
* DO NOT COPY/USE WITHOUT PERMISSION
*
*/
#include "mix_cc/ihyper_db.h"
#include "mix_cc/json.h"
#include "mix_cc/matheval/matheval.hpp"
#include <chrono>
#include <eqpalg/utility/StatExp.hpp>
#include <log4cplus/LOG.h>
#include <map>
#include <memory>
#include <string>
#include <vector>
using std::map;
using std::string;
using std::vector;
using MExp = mix_cc::matheval::Expression;
using namespace std::chrono;
using std::chrono::system_clock;
using TimePoint = system_clock::time_point;
using TimeDur = milliseconds;
class ExpModule {
public:
ExpModule(std::map<std::string, double> &vars_ref, vector<string> &m_tags,
bool &is_exp_alg_ref);
~ExpModule();
ExpModule() = delete;
ExpModule &operator=(const ExpModule &) = delete;
/**
* @brief
* @param exp_name
* @param exp_str
* @return int
*/
int add_exp(string exp_name, string exp_str);
/**
* @brief
* @return int
*/
int update();
void fun_reset();
/**
* @brief
* @param queried_data
* @return int
*/
int update(Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic> queried_data,
std::vector<TimePoint> queried_time, int row = 0);
/**
* @brief
* @param exp_name
* @return double
*/
double get_value(string exp_name);
/**
* @brief
*/
void print_value();
/**
* @brief Get the exp str object
* @param exp_name My Param doc
* @return string
*/
string get_exp_str(string exp_name);
private:
/**
* @brief
*/
void init();
/**
* @brief
*/
void refresh_exp_vars_mem();
/**
* @brief
* @param queried_data
*/
void refresh_exp_ihd_mem(
Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic> queried_data,
std::vector<TimePoint> queried_time, int row);
private:
map<string, double> &mm_vars;
map<string, double> exp_result_;
map<string, std::unique_ptr<MExp>> exp_ptr_;
map<string, string> exp_str_;
vector<string> &m_tags;
std::unique_ptr<LOG> logger_;
TimePoint now_time_;
/**
* @brief
*/
StatExp::FunVars fun_vars_;
bool &is_exp_alg_;
};

View File

@ -0,0 +1,34 @@
// eqpalg/utility/bound_checker.cpp
#include <eqpalg/utility/bound_checker.h>
void BoundChecker::setLimits(double down, double up) {
limit_down_ = down;
limit_up_ = up;
// 哨兵值推导检测模式(与 ExpBase::reload_config_up_down 逻辑一致)
int idown = static_cast<int>(down);
int iup = static_cast<int>(up);
if (idown == -32768 && iup != idown) {
detect_mode_ = DetectMode::OnlyRight; // 仅右边界
} else if ((iup == -32768 || iup == 32768 || iup == 32767) && iup != idown) {
detect_mode_ = DetectMode::OnlyLeft; // 仅左边界
} else if (iup == -32768 && iup == idown) {
detect_mode_ = DetectMode::ErrorMode; // 配置错误
} else {
detect_mode_ = DetectMode::Default; // 双侧
}
}
bool BoundChecker::isOutOfBounds(double value) const {
switch (detect_mode_) {
case DetectMode::Default:
return value < limit_down_ || value > limit_up_;
case DetectMode::OnlyLeft:
return value < limit_down_;
case DetectMode::OnlyRight:
return value > limit_up_;
default:
return false;
}
}

View File

@ -0,0 +1,56 @@
// eqpalg/utility/bound_checker.h
#pragma once
#include <string>
/**
* @brief
*/
enum class DetectMode {
Default = 0, // 双侧检测 (value < down || value > up)
OnlyLeft = 1, // 仅检测左边界 (value < down)
OnlyRight = 2, // 仅检测右边界 (value > up)
ErrorMode = 3 // 错误模式
};
/**
* @brief
*
* ExpBase -32768/32767
*/
class BoundChecker {
public:
BoundChecker() = default;
/**
* @brief
* @param down -32768
* @param up 32767/32768
*/
void setLimits(double down, double up);
/**
* @brief
*/
void setDetectMode(DetectMode mode) { detect_mode_ = mode; }
/**
* @brief value
*/
bool isOutOfBounds(double value) const;
// 访问器
double limitDown() const { return limit_down_; }
double limitUp() const { return limit_up_; }
DetectMode detectMode() const { return detect_mode_; }
/**
* @brief
*/
bool isValid() const { return detect_mode_ != DetectMode::ErrorMode; }
private:
double limit_down_ = -32768;
double limit_up_ = -32768;
DetectMode detect_mode_ = DetectMode::Default;
};

View File

@ -0,0 +1,228 @@
// eqpalg/utility/expression_engine.cpp
#include <eqpalg/utility/expression_engine.h>
#include <glob/SingletonTemplate.h>
#include <eqpalg/gb_item_memory.h>
#include <mix_cc/type/mix_time.h>
#include <log4cplus/LOG.h>
ExpressionEngine::ExpressionEngine(std::map<std::string, double>& mm_vars,
std::vector<std::string>& m_tags)
: mm_vars_(mm_vars), m_tags_(m_tags) {}
// 惰性初始化 VarsCache首次变量刷新时调用
static void ensureVarCache(VarsCache& vc, size_t tag_count) {
if (vc.tag_num == 0 && tag_count > 0) {
vc.init(tag_count, 6);
}
}
int ExpressionEngine::registerExpression(const std::string& name,
const std::string& raw_exp_str) {
if (exps_.count(name)) return 0; // 已注册,幂等
auto& entry = exps_[name];
entry.raw_str = raw_exp_str;
// 通过 FunVars 替换状态函数KeepC/KeepT/RiseEdge/Detect为 funN 变量
auto [ok, processed] = fun_vars_.add_exp_str(raw_exp_str, &mm_vars_);
if (!ok) return -1;
entry.processed_str = processed;
// 编译表达式
try {
entry.ptr = std::make_unique<MExp>(processed, &mm_vars_);
} catch (const std::exception& e) {
return -1;
}
// 从表达式中提取 hold(n,T) 模式
initHoldExpStr(processed);
return 0;
}
int ExpressionEngine::registerRawExpression(const std::string& name,
const std::string& raw_exp_str) {
if (exps_.count(name)) return 0;
auto& entry = exps_[name];
entry.raw_str = raw_exp_str;
entry.processed_str = raw_exp_str;
try {
entry.ptr = std::make_unique<MExp>(raw_exp_str, &mm_vars_);
} catch (const std::exception& e) {
return -1;
}
return 0;
}
double ExpressionEngine::evaluate(const std::string& name) {
auto it = exps_.find(name);
if (it == exps_.end()) return 0.0;
return it->second.ptr->evaluate();
}
bool ExpressionEngine::evaluateBool(const std::string& name) {
return static_cast<bool>(evaluate(name));
}
int ExpressionEngine::initHoldExpStr(const std::string& exp_str) {
auto hold_sub_strs = HoldTime::find_substr(exp_str, "_HE");
for (const auto& sub_str : hold_sub_strs) {
bool flag;
double timeM;
std::string tagi;
std::string var_name;
std::tie(flag, timeM, tagi, var_name) = HoldTime::find_hold(sub_str);
if (flag) {
if (hold_times_.find(var_name) == hold_times_.end()) {
hold_times_.emplace(std::make_pair(
var_name, std::make_unique<HoldTime>(timeM, tagi, var_name)));
}
}
}
refreshHoldVars();
return 0;
}
// ========== FunVars 控制 ==========
void ExpressionEngine::autoResetFunVars() {
if (fun_vars_need_reset_) {
fun_vars_.refresh_fun_vars(true, &mm_vars_);
fun_vars_need_reset_ = false;
}
}
void ExpressionEngine::markFunVarsNeedReset() {
fun_vars_need_reset_ = true;
}
void ExpressionEngine::forceResetFunVars() {
fun_vars_.refresh_fun_vars(true, &mm_vars_);
fun_vars_need_reset_ = false;
}
void ExpressionEngine::refreshFromMemory(
const TimePoint& now_time,
mix_cc::time_range_t& query_time_range) {
ensureVarCache(var_cache_, m_tags_.size());
for (size_t i = 0; i < m_tags_.size(); i++) {
double current =
SingletonTemplate<GlobaltemSharedMemory>::GetInstance()[m_tags_[i]];
// pN = 旧 tagN
mm_vars_[var_cache_.p_keys[i]] = mm_vars_[var_cache_.tag_keys[i]];
// tagN = 当前值
mm_vars_[var_cache_.tag_keys[i]] = current;
// pvN 历史移位
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;
}
mm_vars_["now"] = std::chrono::duration_cast<std::chrono::milliseconds>(
now_time.time_since_epoch())
.count();
fun_vars_.refresh_fun_vars(false, &mm_vars_);
query_time_range.set_right(now_time);
refreshHoldVars();
}
void ExpressionEngine::refreshFromIhdRow(
int row,
const Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic>& queried_data,
const std::vector<TimePoint>& queried_time,
TimePoint& now_time,
mix_cc::time_range_t& query_time_range) {
if (queried_data.rows() == 0 || queried_data.cols() == 0) return;
if (row < 0 || row >= queried_data.rows()) return;
if (static_cast<int>(m_tags_.size()) > queried_data.cols()) return;
ensureVarCache(var_cache_, m_tags_.size());
for (size_t i = 0; i < m_tags_.size(); i++) {
// pN = 旧 tagN
mm_vars_[var_cache_.p_keys[i]] = mm_vars_[var_cache_.tag_keys[i]];
// tagN = 当前行数据
mm_vars_[var_cache_.tag_keys[i]] = queried_data(row, i);
// pvN 历史移位
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]] =
mm_vars_[var_cache_.tag_keys[i]];
}
mm_vars_["now"] =
mix_cc::mix_time_t(queried_time[row]).to_milliseconds();
now_time = queried_time[row];
query_time_range.set_right(queried_time[row]);
fun_vars_.refresh_fun_vars(false, &mm_vars_);
refreshHoldVars();
}
void ExpressionEngine::refreshHoldVars() {
for (auto& kv : hold_times_) {
mm_vars_[kv.first] =
static_cast<double>(kv.second->update_value(mm_vars_[kv.second->tagi]));
}
}
// ========== 调试 ==========
void ExpressionEngine::printVars(const std::string& exp_str) {
// 简单调试输出:遍历所有变量
// 调用方可使用 LOG 宏自行格式化
}
int ExpressionEngine::firstFill(int data_source, TimePoint& now_time,
mix_cc::time_range_t& query_time_range) {
// 惰性初始化 VarsCache
ensureVarCache(var_cache_, m_tags_.size());
for (size_t i = 0; i < m_tags_.size(); i++) {
auto idx = std::to_string(i + 1);
double value = 0.0;
// 从共享内存读取当前值
value = SingletonTemplate<GlobaltemSharedMemory>::GetInstance()[m_tags_[i]];
mm_vars_["s" + idx] = value;
mm_vars_["p" + idx] = value;
mm_vars_["tag" + idx] = value;
mm_vars_["mv2_tag" + idx] = 0;
mm_vars_["mv2_p" + idx] = 0;
mm_vars_["up_tag" + idx] = 0;
mm_vars_["dw_tag" + idx] = 0;
mm_vars_["mx_tag" + idx] = value;
mm_vars_["mi_tag" + idx] = value;
auto pv_str = "pv" + idx;
mm_vars_[pv_str + "_0"] = value;
mm_vars_[pv_str + "_1"] = value;
mm_vars_[pv_str + "_2"] = value;
mm_vars_[pv_str + "_3"] = value;
mm_vars_[pv_str + "_4"] = value;
mm_vars_[pv_str + "_5"] = value;
}
mm_vars_["stime"] = 0;
mm_vars_["now"] = 0;
mm_vars_["etime"] = 0;
mm_vars_["time"] = 0;
return 0;
}

View File

@ -0,0 +1,119 @@
// eqpalg/utility/expression_engine.h
#pragma once
#include <chrono>
#include <map>
#include <memory>
#include <string>
#include <vector>
#include <Eigen/Dense>
#include <eqpalg/utility/HoldTime.h>
#include <eqpalg/utility/StatExp.hpp>
#include <eqpalg/utility/VarsCache.hpp>
#include <mix_cc/matheval/matheval.hpp>
#include <mix_cc/type/mix_time.h>
using MExp = mix_cc::matheval::Expression;
using TimePoint = std::chrono::system_clock::time_point;
/**
* @brief
*
* FunVars
* /IHDBhold
*
* ExpModule + ExpBase is_exp_alg_
*/
class ExpressionEngine {
public:
/**
* @param mm_vars AlgBase
* @param m_tags tag AlgBase
*/
ExpressionEngine(std::map<std::string, double>& mm_vars,
std::vector<std::string>& m_tags);
~ExpressionEngine() = default;
// 禁止拷贝/移动
ExpressionEngine(const ExpressionEngine&) = delete;
ExpressionEngine& operator=(const ExpressionEngine&) = delete;
// ========== 表达式注册 ==========
/**
* @brief FunVars
* @param name ("act", "feedback", "result", "pre_result", ...)
* @param raw_exp_str KeepC(tag1,1)
* @return 0 -1
*/
int registerExpression(const std::string& name, const std::string& raw_exp_str);
/**
* @brief FunVars
*/
int registerRawExpression(const std::string& name, const std::string& raw_exp_str);
// ========== 表达式求值 ==========
double evaluate(const std::string& name);
bool evaluateBool(const std::string& name);
/**
* @brief map
*/
std::map<std::string, double>& vars() { return mm_vars_; }
// ========== 每周期数据刷新 ==========
void refreshFromMemory(const TimePoint& now_time,
mix_cc::time_range_t& query_time_range);
void refreshFromIhdRow(int row,
const Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic>& queried_data,
const std::vector<TimePoint>& queried_time,
TimePoint& now_time,
mix_cc::time_range_t& query_time_range);
int firstFill(int data_source, TimePoint& now_time,
mix_cc::time_range_t& query_time_range);
// ========== FunVars 控制 ==========
void autoResetFunVars();
void markFunVarsNeedReset();
void forceResetFunVars();
// ========== hold 变量 ==========
void refreshHoldVars();
// ========== 调试 ==========
void printVars(const std::string& exp_str = "");
private:
std::map<std::string, double>& mm_vars_;
std::vector<std::string>& m_tags_;
StatExp::FunVars fun_vars_;
bool fun_vars_need_reset_ = false;
// 表达式注册表
struct ExpEntry {
std::string raw_str;
std::string processed_str;
std::unique_ptr<MExp> ptr;
};
std::map<std::string, ExpEntry> exps_;
// hold(n,T) 变量
std::map<std::string, std::unique_ptr<HoldTime>> hold_times_;
VarsCache var_cache_;
static constexpr size_t PV_NUM = 6;
// 内部辅助
int initHoldExpStr(const std::string& exp_str);
};

View File

@ -0,0 +1,226 @@
// eqpalg/utility/fb_state_machine.cpp
#include <eqpalg/utility/fb_state_machine.h>
#include <cmath>
#include <cfloat>
#include <string>
// ========== 配置 ==========
void FbStateMachine::configure(bool keepMode, TimeDur timeout) {
keep_mode_ = keepMode;
timeout_ = timeout;
state_ = FbState::Idle;
}
// ========== 每周期状态更新 ==========
FbUpdateResult FbStateMachine::update(bool actTriggered, TimePoint now,
std::map<std::string, double>& mm_vars,
size_t tag_count) {
FbUpdateResult result;
result.funVarsNeedReset = false;
switch (state_) {
case FbState::Idle:
if (actTriggered) {
// → Started快照变量
snapshotActionStart(now, mm_vars, tag_count);
state_ = FbState::Started;
start_time_ = now;
result.state = FbState::Started;
} else {
result.state = FbState::Idle;
result.funVarsNeedReset = true; // 空闲时重置 fun_vars
}
break;
case FbState::Started:
// 更新累积变量(首周期也需要更新)
updateActionVars(now, mm_vars, tag_count);
if (!actTriggered && keep_mode_) {
state_ = FbState::Idle;
result.state = FbState::NotHold;
result.funVarsNeedReset = true;
} else if (isAccumulatorNearOverflow(mm_vars, tag_count)) {
state_ = FbState::Idle;
result.state = FbState::Timeout;
result.funVarsNeedReset = true;
} else {
state_ = FbState::InProgress;
result.state = FbState::Started; // 返回 Started调用方跳过反馈检查
}
break;
case FbState::InProgress:
// 先更新累积变量
updateActionVars(now, mm_vars, tag_count);
if (!actTriggered && keep_mode_) {
state_ = FbState::Idle;
result.state = FbState::NotHold;
result.funVarsNeedReset = true;
} else if (isAccumulatorNearOverflow(mm_vars, tag_count)) {
clearActionAccumulators(mm_vars, tag_count);
state_ = FbState::Idle;
result.state = FbState::Timeout;
result.funVarsNeedReset = true;
} else if (timeout_ != TimeDur(-32768) &&
(now - start_time_) > timeout_) {
clearActionAccumulators(mm_vars, tag_count);
state_ = FbState::Idle;
result.state = FbState::Timeout;
result.funVarsNeedReset = true;
} else {
result.state = FbState::InProgress;
}
break;
// 终端状态 → 下周期自动回 Idle
default:
state_ = FbState::Idle;
result.state = FbState::Idle;
result.funVarsNeedReset = true;
break;
}
return result;
}
// ========== 反馈条件检查 ==========
bool FbStateMachine::checkFeedback(bool fbCondition, TimePoint now,
std::map<std::string, double>& mm_vars) {
if (state_ != FbState::InProgress) {
return false;
}
if (fbCondition) {
recordActionEnd(now, mm_vars);
state_ = FbState::Idle; // 下周期回到 Idle
return true;
}
return false;
}
// ========== 查询 ==========
bool FbStateMachine::isActive() const {
return state_ == FbState::Started || state_ == FbState::InProgress;
}
// ========== 强制重置 ==========
void FbStateMachine::forceReset() {
state_ = FbState::Idle;
start_time_ = TimePoint{};
}
// ========== 内部变量操作 ==========
void FbStateMachine::snapshotActionStart(
TimePoint now,
std::map<std::string, double>& mm_vars,
size_t tag_count) {
mm_vars["stime"] = std::chrono::duration_cast<std::chrono::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];
double p_val = mm_vars["p" + idx];
// s[n] = tag[n] 在动作开始时刻的快照值
mm_vars["s" + idx] = tag_val;
// 极值初始化为当前值
mm_vars["mx_tag" + idx] = tag_val;
mm_vars["mi_tag" + idx] = tag_val;
// 累积器初始化为 0
mm_vars["mv2_tag" + idx] = 0;
mm_vars["mv2_p" + idx] = 0;
// 上升沿/下降沿初值
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;
// 若当前已为 1初始计数为 1
if (tag_val == 1) {
mm_vars["mv2_tag" + idx] = 1;
}
if (p_val == 1) {
mm_vars["mv2_p" + idx] = 1;
}
}
}
void FbStateMachine::updateActionVars(
TimePoint now,
std::map<std::string, double>& mm_vars,
size_t tag_count) {
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]);
// 累积 1 的计数
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 FbStateMachine::recordActionEnd(
TimePoint now,
std::map<std::string, double>& mm_vars) {
mm_vars["etime"] = mm_vars["now"];
}
void FbStateMachine::clearActionAccumulators(
std::map<std::string, double>& mm_vars,
size_t tag_count) {
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 FbStateMachine::isAccumulatorNearOverflow(
const std::map<std::string, double>& mm_vars,
size_t tag_count) const {
for (size_t i = 0; i < tag_count; i++) {
std::string key = "mv2_tag" + std::to_string(i + 1);
auto it = mm_vars.find(key);
if (it != mm_vars.end()) {
if (std::abs(it->second - DBL_MAX) < 2.0) {
return true;
}
}
}
return false;
}

View File

@ -0,0 +1,122 @@
// eqpalg/utility/fb_state_machine.h
#pragma once
#include <chrono>
#include <map>
#include <string>
using TimePoint = std::chrono::system_clock::time_point;
using TimeDur = std::chrono::milliseconds;
/**
* @brief
*/
enum class FbState {
Idle, // 空闲(无活动动作)
Started, // 刚启动(首周期检测到触发条件,下次检查反馈)
InProgress, // 进行中(等待反馈条件满足或超时)
Done, // 反馈条件满足,动作完成
NotHold, // keep_mode 下触发条件丢失
Timeout // 超时
};
/**
* @brief FbStateMachine::update()
*/
struct FbUpdateResult {
FbState state;
bool funVarsNeedReset; // 本周期是否需要标记 fun_vars 重置
};
/**
* @brief
*
* ExpBase 4 act_start_done/act_not_hold/act_done/act_timeout
* 4 act_started_/act_triggered_/feedback_triggered_/feedback_done_
*
*
* Idle Started InProgress Done/NotHold/Timeout Idle
*
* VarManager
*/
class FbStateMachine {
public:
FbStateMachine() = default;
~FbStateMachine() = default;
// ========== 配置 ==========
/**
* @param keepMode 退
* @param timeout -32768ms
*/
void configure(bool keepMode, TimeDur timeout);
// ========== 每周期调用 ==========
/**
* @brief mon
*
* @param actTriggered exp_act_->evaluate()
* @param now
* @param mm_vars /
* @param tag_count tag
*
* @return FbUpdateResult + fun_vars
*/
FbUpdateResult update(bool actTriggered, TimePoint now,
std::map<std::string, double>& mm_vars,
size_t tag_count);
/**
* @brief update() InProgress
*
* @param fbCondition exp_feedback_->evaluate()
* @param now
* @param mm_vars
*
* @return true Done
* @return false InProgress
*/
bool checkFeedback(bool fbCondition, TimePoint now,
std::map<std::string, double>& mm_vars);
// ========== 查询 ==========
bool isActive() const; // Started 或 InProgress
bool isKeepMode() const { return keep_mode_; }
TimePoint actionStartTime() const { return start_time_; }
FbState currentState() const { return state_; }
// ========== 外部强制重置PRR=false 时调用)==========
void forceReset();
// ========== 获取 m_timemode供外部判断报警消息格式==========
bool isTimeMode() const { return time_mode_; }
void setTimeMode(bool v) { time_mode_ = v; }
private:
bool keep_mode_ = false;
TimeDur timeout_ = std::chrono::minutes(10);
FbState state_ = FbState::Idle;
TimePoint start_time_;
bool time_mode_ = false; // 原 ExpBase::m_timemode
// ========== 内部变量操作(替代分散在 act_* 方法中的变量更新)==========
void snapshotActionStart(TimePoint now,
std::map<std::string, double>& mm_vars,
size_t tag_count);
void updateActionVars(TimePoint now,
std::map<std::string, double>& mm_vars,
size_t tag_count);
void recordActionEnd(TimePoint now,
std::map<std::string, double>& mm_vars);
void clearActionAccumulators(std::map<std::string, double>& mm_vars,
size_t tag_count);
bool isAccumulatorNearOverflow(
const std::map<std::string, double>& mm_vars,
size_t tag_count) const;
};

View File

@ -0,0 +1,112 @@
// eqpalg/utility/stat_collector.cpp
#include <eqpalg/utility/stat_collector.h>
#include <eqpalg/feature_extraction/daa.h>
#include <eqpalg/define/public.h>
#include <algorithm>
#include <log4cplus/LOG.h>
StatCollector::~StatCollector() = default;
void StatCollector::configure(const std::string& ruleId,
const std::string& ruleName,
int distMode, bool isLearning) {
rule_id_ = ruleId;
rule_name_ = ruleName;
dist_mode_ = distMode;
is_learning_ = isLearning;
configured_ = true;
}
int StatCollector::processCron(const std::vector<double>& statValues,
TimePoint now, int cronDelayHours,
TimePoint& lastLoadTime) {
if (statValues.empty()) return 0;
int size_data = static_cast<int>(statValues.size());
if (sta_ptr_ == nullptr) {
sta_ptr_ = std::make_unique<DAA::STA>(rule_id_, rule_name_);
sta_ptr_->update_ci_dist();
lastLoadTime = now;
}
if (now - lastLoadTime > std::chrono::hours(cronDelayHours)) {
sta_ptr_->update_ci_dist();
lastLoadTime = now;
}
if (sta_ptr_->is_init()) {
for (int i = 0; i < size_data; i++) {
sta_ptr_->dist_add(statValues[i]);
}
} else {
double max_data = *std::max_element(statValues.begin(), statValues.end());
double min_data = *std::min_element(statValues.begin(), statValues.end());
double range = (max_data - min_data) / static_cast<double>(DAA::STA_SIZE_MIN);
if (range < 0.1) range = 0.1;
if (sta_ptr_->init(range, min_data)) {
for (int i = 0; i < size_data; i++) {
sta_ptr_->dist_add(statValues[i]);
}
}
}
sta_ptr_->store_db2();
return size_data;
}
bool StatCollector::reloadCiDist(double& limitDown, double& limitUp,
TimePoint now, TimePoint& lastLoadTime) {
if (dist_mode_ == 0) return false; // 手动模式,不自动加载
lastLoadTime = now;
mix_cc::float_range_t dist_range;
if (dist_mode_ == DistMode::Online) {
dist_range = DAA::STA::select_from_t_rule_feature(rule_id_);
} else if (dist_mode_ == DistMode::Offline) {
dist_range = DAA::STA::select_from_t_sample_mag(rule_id_);
} else {
return false;
}
if (dist_range.get_distance() == 0 ||
dist_range.get_left() > dist_range.get_right()) {
return false;
}
limitDown = dist_range.get_left();
limitUp = dist_range.get_right();
return true;
}
void StatCollector::reset(const std::string& ruleId) {
DAA::STA::delete_statistics_data(ruleId);
sta_ptr_.reset();
}
void StatCollector::ensureInitialized() {
if (sta_ptr_ == nullptr) {
sta_ptr_ = std::make_unique<DAA::STA>(rule_id_, rule_name_);
}
}
void StatCollector::resetData() {
sta_ptr_->reset_data();
}
void StatCollector::distAdd(double value) {
sta_ptr_->dist_add(value);
}
bool StatCollector::taskStoreDb2(const std::string& sampleId) {
return sta_ptr_->task_store_db2(sampleId);
}
std::string StatCollector::getSampleStatStr() {
return sta_ptr_->get_sample_stat_str();
}
void StatCollector::runningStatAdd(double value) {
sta_ptr_->running_stat_add(value);
}

View File

@ -0,0 +1,94 @@
// eqpalg/utility/stat_collector.h
#pragma once
#include <chrono>
#include <memory>
#include <string>
#include <vector>
// 前向声明
namespace DAA { class STA; }
using TimePoint = std::chrono::system_clock::time_point;
/**
* @brief
*
* DAA::STA
* - dist_add
* - DB2
* -
*/
class StatCollector {
public:
StatCollector() = default;
~StatCollector();
/**
* @brief
* @param ruleId ID
* @param ruleName
* @param distMode 0=, 1=线, 2=线
* @param isLearning
*/
void configure(const std::string& ruleId, const std::string& ruleName,
int distMode, bool isLearning);
/**
* @brief cron
* @param statValues
* @param now
* @param cronDelayHours cron
* @param lastLoadTime [in/out]
* @return
*/
int processCron(const std::vector<double>& statValues,
TimePoint now, int cronDelayHours,
TimePoint& lastLoadTime);
/**
* @brief
* @param limitDown [out]
* @param limitUp [out]
* @param now
* @param lastLoadTime [out]
* @return true
*/
bool reloadCiDist(double& limitDown, double& limitUp,
TimePoint now, TimePoint& lastLoadTime);
/**
* @brief
*/
void reset(const std::string& ruleId);
bool isConfigured() const { return configured_; }
// ---- task 进程专用接口 ----
/** @brief 确保 STA 实例已初始化(惰性初始化) */
void ensureInitialized();
/** @brief 重置累积数据,供新 task 使用 */
void resetData();
/** @brief 向分布添加单个值 */
void distAdd(double value);
/** @brief 将 task 结果存储到 DB2 (T_SAMPLE_STAT) */
bool taskStoreDb2(const std::string& sampleId);
/** @brief 获取样本统计字符串,用于 T_SAMPLE_MAG */
std::string getSampleStatStr();
/** @brief 向运行统计中添加值 */
void runningStatAdd(double value);
private:
std::unique_ptr<DAA::STA> sta_ptr_;
std::string rule_id_;
std::string rule_name_;
int dist_mode_ = 0;
bool is_learning_ = true;
bool configured_ = false;
};