/** * @file DataModelTests.cpp * @brief `Utilities/DataModel` 的纯数据层语义回归(不创建窗口、不碰 GUI)。 * * 重点用例: * * `dataset.formula_transform_cache` —— 第 32 轮引入的「公式绘制值缓存」 * (`DataSet::FormulaTransformFor`)必须与"逐点 `ApplyDrawTransform` 后统计"**逐位一致**, * 并且命中缓存时返回同一指针、样式变化后失效重建。 * 这条是"公式信号不再逐帧全扫"的正确性底线。 * * `filter.moving_average` —— 多阶等价性(1 阶 == 单次、2 阶 == 链式)。 * * `transform.*` —— 变换顺序(公式 → 系数 → 绝对值 → 垂直镜像 → Y 偏移)与区间映射。 */ #include #include #include #include #include #include #include "DataModel.hpp" #include "UnitTestFramework.hpp" namespace { /** @brief 取 double 的位模式。 */ std::uint64_t BitsOf(const double value) { std::uint64_t bits = 0; __builtin_memcpy(&bits, &value, sizeof(bits)); return bits; } /** @brief 构造一条合成信号分组(一条单调时间轴 + 一条信号)。 */ jdi::SignalGroup MakeGroup(const std::vector& values, const wchar_t* name) { jdi::SignalGroup group; group.name = L"synthetic"; jdi::TimeAxis axis; axis.name = L"time"; axis.time.resize(values.size()); for (std::size_t index = 0; index < values.size(); ++index) { axis.time[index] = static_cast(index) * 0.001; } axis.monotonic = true; axis.minTime = 0.0; axis.maxTime = values.empty() ? 0.0 : axis.time.back(); axis.finiteCount = axis.time.size(); group.axes.push_back(std::move(axis)); jdi::SignalData signal; signal.name = name; signal.axisIndex = 0; signal.values = values; group.signals.push_back(std::move(signal)); return group; } } // namespace JDI_TEST(transform_apply_order) { jdi::SignalStyle style; style.gain = 2.0f; style.absolute = true; style.mirrorVertical = true; style.offsetY = 1.0f; // -1.5 → ×2 = -3 → |·| = 3 → 垂直镜像 = -3 → +1 = -2 JDI_CHECK_NEAR(jdi::ApplyDrawTransform(style, -1.5), -2.0, 0.0); // 公式最先结算:f(t)=x*10,再 ×2、|·|、镜像、+1 ⇒ 2.5 → 25 → 50 → 50 → -50 → -49 style.formula = L"f(t)*10"; JDI_CHECK_NEAR(jdi::ApplyDrawTransform(style, 2.5), -49.0, 1e-9); // NaN 必须保持 NaN(断线语义) JDI_CHECK(std::isnan(jdi::ApplyDrawTransform(style, std::numeric_limits::quiet_NaN()))); // 水平镜像不改变取值(只改下标),`SourceIndexFor` 负责 jdi::SignalStyle mirrored; mirrored.mirrorHorizontal = true; JDI_CHECK_NEAR(jdi::ApplyDrawTransform(mirrored, 7.0), 7.0, 0.0); JDI_CHECK_EQ(jdi::SourceIndexFor(mirrored, 10, 0), 9); JDI_CHECK_EQ(jdi::SourceIndexFor(mirrored, 10, 9), 0); } JDI_TEST(transform_value_range_and_order_preserving) { jdi::SignalStyle style; style.gain = 2.0f; style.absolute = true; style.mirrorVertical = true; style.offsetY = 1.0f; double low = -2.0; double high = 3.0; jdi::TransformValueRange(style, low, high); // [-2,3] → ×2 = [-4,6] → |·| = [0,6] → 镜像 = [-6,0] → +1 = [-5,1] JDI_CHECK_NEAR(low, -5.0, 0.0); JDI_CHECK_NEAR(high, 1.0, 0.0); jdi::SignalStyle plain; JDI_CHECK(jdi::DrawTransformOrderPreserving(plain)); plain.gain = -1.0f; JDI_CHECK(!jdi::DrawTransformOrderPreserving(plain)); plain.gain = 1.0f; plain.absolute = true; JDI_CHECK(!jdi::DrawTransformOrderPreserving(plain)); plain.absolute = false; plain.mirrorVertical = true; JDI_CHECK(!jdi::DrawTransformOrderPreserving(plain)); plain.mirrorVertical = false; plain.formula = L"f(t)*f(t)"; JDI_CHECK(!jdi::DrawTransformOrderPreserving(plain)); plain.formula = L"f(t)"; JDI_CHECK(jdi::DrawTransformOrderPreserving(plain)); } JDI_TEST(filter_moving_average_equivalence) { std::vector values; values.reserve(400); for (int index = 0; index < 400; ++index) { values.push_back(std::sin(static_cast(index) * 0.07) * 100.0 + (index % 7)); } values[10] = std::numeric_limits::quiet_NaN(); values[200] = std::numeric_limits::quiet_NaN(); std::vector single; std::vector singleCopy; jdi::ComputeMovingAverage(values, 3, single); // 1 阶 == 单次平均(逐位) jdi::ComputeMovingAverageIterated(values, 3, 1, singleCopy); JDI_CHECK_EQ(single.size(), singleCopy.size()); bool identical = (single.size() == singleCopy.size()); for (std::size_t index = 0; identical && index < single.size(); ++index) { identical = (BitsOf(single[index]) == BitsOf(singleCopy[index])); } JDI_CHECK(identical); // 2 阶 == 对 1 阶结果再做一次(逐位) std::vector firstPass(single.begin(), single.end()); std::vector chained; jdi::ComputeMovingAverage(firstPass, 3, chained); std::vector iterated; jdi::ComputeMovingAverageIterated(values, 3, 2, iterated); JDI_CHECK_EQ(chained.size(), iterated.size()); bool chainIdentical = (chained.size() == iterated.size()); for (std::size_t index = 0; chainIdentical && index < chained.size(); ++index) { chainIdentical = (BitsOf(chained[index]) == BitsOf(iterated[index])); } JDI_CHECK(chainIdentical); // 常数信号经任意阶数保持不变 std::vector constant(500, 7.5); std::vector filtered; jdi::ComputeMovingAverageIterated(constant, 5, 3, filtered); bool constantHeld = (filtered.size() == constant.size()); for (std::size_t index = 0; constantHeld && index < filtered.size(); ++index) { constantHeld = (std::fabs(static_cast(filtered[index]) - 7.5) < 1e-5); } JDI_CHECK(constantHeld); } JDI_TEST(dataset_group_access) { jdi::DataSet dataSet; int firstIndex = -1; JDI_CHECK(dataSet.AddGroup(MakeGroup({1.0, 2.0, 3.0}, L"a"), firstIndex)); JDI_CHECK_EQ(firstIndex, 0); JDI_CHECK_EQ(dataSet.GroupCount(), 1); const jdi::SignalGroup* group = dataSet.GroupAt(0); JDI_CHECK(group != nullptr); if (group != nullptr) { JDI_CHECK_EQ(group->signals.size(), 1); JDI_CHECK_WIDE_EQ(group->signals[0].name, std::wstring(L"a")); JDI_CHECK_EQ(group->axes.size(), 1); } // 外观属于信号本身:通过 `Style(ref)` 读写 const jdi::SignalRef ref{0, 0}; jdi::SignalStyle* style = dataSet.Style(ref); JDI_CHECK(style != nullptr); if (style != nullptr) { style->lineWidth = 3.5f; const jdi::SignalStyle* readBack = dataSet.Style(ref); JDI_CHECK(readBack != nullptr && readBack->lineWidth == 3.5f); } JDI_CHECK(dataSet.GroupAt(5) == nullptr); } JDI_TEST(dataset_formula_transform_cache) { std::vector values; values.reserve(5000); for (int index = 0; index < 5000; ++index) { values.push_back(std::sin(static_cast(index) * 0.013) * 50.0 + (index % 11) * 0.5); } values[17] = std::numeric_limits::quiet_NaN(); values[4999] = std::numeric_limits::quiet_NaN(); jdi::DataSet dataSet; int groupIndex = -1; JDI_CHECK(dataSet.AddGroup(MakeGroup(values, L"formula"), groupIndex)); const jdi::SignalGroup* group = dataSet.GroupAt(0); JDI_CHECK(group != nullptr); if (group == nullptr) { return; } const jdi::SignalData& signal = group->signals[0]; jdi::SignalStyle& style = *dataSet.Style(jdi::SignalRef{0, 0}); // 恒等 / 空公式 ⇒ 不建立缓存(调用方走 O(1) 保序快路径) JDI_CHECK(dataSet.FormulaTransformFor(signal, style) == nullptr); style.formula = L"f(t)"; JDI_CHECK(dataSet.FormulaTransformFor(signal, style) == nullptr); // 真公式 ⇒ 建立一次缓存 style.formula = L"f(t)*2"; style.gain = 1.5f; style.absolute = true; style.mirrorVertical = false; style.offsetY = -3.0f; const jdi::DataSet::FormulaTransformCache* entry = dataSet.FormulaTransformFor(signal, style); JDI_CHECK(entry != nullptr); if (entry == nullptr) { return; } JDI_CHECK(entry->ready); // 参考实现:逐点走 `ApplyDrawTransform`(同一套语义),按缓存给出的 stride 分组 double referenceLow = std::numeric_limits::infinity(); double referenceHigh = -std::numeric_limits::infinity(); std::vector referenceMin(entry->minimums.size(), std::numeric_limits::quiet_NaN()); std::vector referenceMax(entry->maximums.size(), std::numeric_limits::quiet_NaN()); const std::size_t stride = entry->stride; JDI_CHECK(stride > 0); for (std::size_t index = 0; index < signal.values.size(); ++index) { const double mapped = jdi::ApplyDrawTransform(style, signal.values[index]); if (!std::isfinite(mapped)) { continue; } const std::size_t bucket = index / stride; if (bucket >= referenceMin.size()) { continue; } if (mapped < referenceMin[bucket] || referenceMin[bucket] != referenceMin[bucket]) { referenceMin[bucket] = mapped; } if (mapped > referenceMax[bucket] || referenceMax[bucket] != referenceMax[bucket]) { referenceMax[bucket] = mapped; } referenceLow = std::min(referenceLow, mapped); referenceHigh = std::max(referenceHigh, mapped); } JDI_CHECK_BIT_EQUAL(entry->low, referenceLow); JDI_CHECK_BIT_EQUAL(entry->high, referenceHigh); JDI_CHECK_EQ(entry->minimums.size(), referenceMin.size()); bool bucketsMatch = (entry->minimums.size() == referenceMin.size()); for (std::size_t bucket = 0; bucketsMatch && bucket < referenceMin.size(); ++bucket) { const bool emptyReference = (referenceMin[bucket] != referenceMin[bucket]); const bool emptyEntry = (entry->minimums[bucket] != entry->minimums[bucket]); if (emptyReference != emptyEntry) { bucketsMatch = false; break; } if (emptyReference) { continue; } bucketsMatch = BitsOf(entry->minimums[bucket]) == BitsOf(referenceMin[bucket]) && BitsOf(entry->maximums[bucket]) == BitsOf(referenceMax[bucket]); } JDI_CHECK(bucketsMatch); // 命中缓存:同一信号 + 同一样式 ⇒ 返回同一指针(不再重扫) const jdi::DataSet::FormulaTransformCache* again = dataSet.FormulaTransformFor(signal, style); JDI_CHECK(again == entry); // 样式变化 ⇒ 失效重建,且新值域按新公式计算 style.formula = L"f(t)*3"; const jdi::DataSet::FormulaTransformCache* rebuilt = dataSet.FormulaTransformFor(signal, style); JDI_CHECK(rebuilt != nullptr); if (rebuilt != nullptr) { JDI_CHECK(rebuilt != entry); double expectedLow = std::numeric_limits::infinity(); double expectedHigh = -std::numeric_limits::infinity(); for (const double value : signal.values) { const double mapped = jdi::ApplyDrawTransform(style, value); if (!std::isfinite(mapped)) { continue; } expectedLow = std::min(expectedLow, mapped); expectedHigh = std::max(expectedHigh, mapped); } JDI_CHECK_BIT_EQUAL(rebuilt->low, expectedLow); JDI_CHECK_BIT_EQUAL(rebuilt->high, expectedHigh); } }