#ifndef __FORMULA_EVAL_HPP__ #define __FORMULA_EVAL_HPP__ // 「公式映射」的表达式编译与求值(信号样式区的 `Y = …` 输入框)。 // // 语法(刻意保持极小,避免引入脚本引擎与额外的体积): // expr := term (('+' | '-') term)* // term := unary (('*' | '/') unary)* // unary := ('+' | '-') unary | power // power := primary ('^' unary)? —— 右结合:2^3^2 = 2^(3^2) // primary := number | '(' expr ')' | variable | constant // number := 十进制小数(支持 e/E 指数) // constant:= `e`(自然常数)| `pi`(圆周率)—— 大小写均接受,保存前统一为小写 // variable:= `X` / `x` / `f(t)` —— 都表示"当前采样点的值"(**不支持单独的 `f`**) // // ⚠️ 求值走 `thread_local` 的单条缓存(按 `std::wstring` 的 data 指针 + 内容判定): // 绘制循环里同一条信号的样式对象是稳定的,因此每个信号只需编译一次; // **绝不能**每个采样点都重新解析(千万级采样会慢到不可用)。 #include #include #include #include #include #include namespace jdi { /** * @brief 编译后的一条公式指令(逆波兰式)。 * * `NUMBER` 的操作数存放在 `value`,`VARIABLE` 表示当前采样值; * 其余为一元 / 二元运算符(二元运算从求值栈弹出两个值、压回一个)。 */ struct FormulaInstruction { enum class Kind : std::uint8_t { NUMBER = 0, /**< 常量。 */ VARIABLE, /**< 自变量(当前采样值)。 */ ADD, SUBTRACT, MULTIPLY, DIVIDE, POWER, /**< 幂(`a ^ b`)。 */ NEGATE, /**< 一元负号。 */ SQUARE, /**< 平方(编译期把 `x^2` 改写而来,弹一压一)。 */ CUBE, /**< 立方(编译期把 `x^3` 改写而来)。 */ SQRT /**< 平方根(编译期把 `x^0.5` 改写而来)。 */ }; Kind kind = Kind::NUMBER; double value = 0.0; }; /** 公式求值栈的深度上限(与编译期允许的嵌套深度匹配)。 */ inline constexpr std::size_t kFormulaMaxStack = 64; /** * @brief 可复用的公式程序:**编译一次**,之后逐点 `Evaluate` 只做纯粹的栈运算。 * * ⚠️⚠️ **批量路径必须用它**(`DataSet::FormulaTransformFor`、公式包络构建): * `EvaluateFormula()` 每次调用都要做一次编译缓存查找(`std::wstring` 内容比较), * 千万级采样时这点开销会放大成**秒级**卡顿。实测 2580 万采样: * `EvaluateFormula` 逐点 ≈ 1.6 s,`FormulaProgram::Evaluate` ≈ 0.27 s(≈6 倍)。 * * 用法: * @code * jdi::FormulaProgram program(style.formula); * if (program.Valid()) { for (...) value = program.Evaluate(values[i]); } * @endcode */ class FormulaProgram { public: FormulaProgram() = default; /** * @brief 编译公式文本。 * @param [IN] text 公式正文(不含 `Y =` 前缀)。空串或非法表达式 ⇒ `Valid()` 为 false。 */ explicit FormulaProgram(const std::wstring& text); /** @brief 公式是否可用(非空且语法合法)。 */ bool Valid() const { return m_valid; } /** @brief 已编译的指令序列(只读,主要供调试 / 测试)。 */ const std::vector& Instructions() const { return m_instructions; } /** * @brief 在给定采样值处求值(**必须**先确认 `Valid()`)。 * * 结果非有限(如 `1/0`)时返回 NaN,与 `EvaluateFormula()` 一致。 * * ⚠️⚠️ **没有运行期栈深检查**:栈深在编译期(构造函数里)已校验过 —— * 超过 `kFormulaMaxStack` 的表达式会直接 `Valid() == false`(由调用方回退到 * 带检查的 `EvaluateFormula()`)。批量路径每次调用都把检查加上去, * 在 2580 万点上是纯粹的浪费。 * ⚠️ `always_inline` 是**性能必需**:Release 用 `-Oz`(体积优先)不会自动内联, * 而每点一次未内联调用(512B 栈帧)实测让一次全扫从 0.28s 涨到 0.85s。 * * @param [IN] x 采样值。 * @return 映射后的值。 */ __attribute__((always_inline)) inline double Evaluate(double x) const { double stack[kFormulaMaxStack]; std::size_t depth = 0; for (const FormulaInstruction& instruction : m_instructions) { switch (instruction.kind) { case FormulaInstruction::Kind::NUMBER: stack[depth++] = instruction.value; break; case FormulaInstruction::Kind::VARIABLE: stack[depth++] = x; break; case FormulaInstruction::Kind::NEGATE: stack[depth - 1] = -stack[depth - 1]; break; case FormulaInstruction::Kind::SQUARE: stack[depth - 1] = stack[depth - 1] * stack[depth - 1]; break; case FormulaInstruction::Kind::CUBE: { const double base = stack[depth - 1]; stack[depth - 1] = base * base * base; break; } case FormulaInstruction::Kind::SQRT: stack[depth - 1] = std::sqrt(stack[depth - 1]); break; case FormulaInstruction::Kind::POWER: --depth; stack[depth - 1] = std::pow(stack[depth - 1], stack[depth]); break; case FormulaInstruction::Kind::DIVIDE: --depth; stack[depth - 1] = stack[depth - 1] / stack[depth]; break; default: { // ADD / SUBTRACT / MULTIPLY --depth; double left = stack[depth - 1]; const double right = stack[depth]; if (instruction.kind == FormulaInstruction::Kind::ADD) { left += right; } else if (instruction.kind == FormulaInstruction::Kind::SUBTRACT) { left -= right; } else { left *= right; } stack[depth - 1] = left; break; } } } if (depth != 1) { return std::numeric_limits::quiet_NaN(); } return std::isfinite(stack[0]) ? stack[0] : std::numeric_limits::quiet_NaN(); } private: std::vector m_instructions; bool m_valid = false; }; /** * @brief 校验公式文本是否可以解析。 * * 空文本是合法输入(表示"不做映射")。 * * @param [IN] text 公式文本(不含 `Y =` 前缀)。 * @param [OUT] error 解析失败时的原因(成功时清空),允许为空。 * @return 语法合法返回 true。 */ bool ValidateFormula(const std::wstring& text, std::wstring* error); /** * @brief 计算公式在给定采样值处的取值。 * * 文本为空或无法解析时**原样返回** `x`(保证绘制不会因为非法公式而消失)。 * 计算结果非有限(如 1/0)时返回 NaN(保持断线语义)。 * * @param [IN] text 公式文本。 * @param [IN] x 采样值(公式中的 `f(t)` / `X`)。 * @return 映射后的值。 */ double EvaluateFormula(const std::wstring& text, double x); /** * @brief 把用户输入里的自变量写法统一成 `f(t)`、常量统一为小写(保存前调用)。 * * 规则: * * 把独立的 `X` / `x` / `f(t)`(含 `F(T)`)记号改写为 `f(t)`; * * 把 `E` / `Pi` / `PI` 等写法改写为小写 `e` / `pi`; * * **单独的 `f` 不改写**(它不是合法写法,留给校验报错)。 * * 只做记号级替换(不会动标识符中间或数字里的字符,如 `1e5` 保持不变)。 * * @param [IN] text 用户输入的公式。 * @return 归一化后的文本。 */ std::wstring NormalizeFormulaText(const std::wstring& text); } // namespace jdi #endif // __FORMULA_EVAL_HPP__