Files
templates/tools/dsview/tests/pwm_envelope.cpp
T

109 lines
17 KiB
C++

#include "pwm_waveform.h" // Проверяем тот же индекс, который использует нативная строка DSView.
#include <chrono> // Монотонные часы измеряют стоимость обзорной перерисовки длинной записи.
#include <iostream> // Результат содержит проверяемые размеры геометрии и измеренное время.
#include <random> // Детерминированный шум проверяет сохранение нерегулярных экстремумов.
#include <stdexcept> // Исключения завершают тест с ненулевым кодом даже в сборке NDEBUG.
static void check(bool condition, const char *message) { // Проверки не отключаются оптимизациями компилятора.
if (!condition) throw std::runtime_error(message); // Ошибка сообщает, какое свойство графика нарушено.
} // Успешная проверка не изменяет состояние индекса.
int main() { // Тест работает без прибора, окна и цикла событий Qt.
set_pwm::Envelope envelope; // Один индекс соответствует одной графической строке PWM.
std::vector<set_pwm::Line> original, actual; // Эталон хранит полные отрезки для независимого сравнения.
envelope.lines(actual, 0, 100, 10); // Запрос до поступления данных должен быть безопасным.
check(actual.empty(), "empty envelope"); // Пустая запись не должна рисовать выдуманный сигнал.
std::mt19937 random(7351); // Постоянное зерно делает случайные экстремумы воспроизводимыми.
double previous = 0; // Начальное значение общей непрерывной кривой равно заполнению 50 процентов.
for (size_t index = 0; index < 4097; ++index) { // Нечётная длина проверяет незавершённые узлы пирамиды.
const double next = double(random() % 20001) / 10000 - 1; // Значения покрывают нормированный диапазон с шагом 0,0001.
envelope.append(index * 10, (index + 1) * 10, previous, next); // Каждый линейный сегмент занимает десять отсчётов.
original.push_back({double(index * 10), double((index + 1) * 10), previous, next}); // Эталон сохраняет координаты без использования индекса.
previous = next; // Соседние сегменты соединяются точно по своим концам.
if (index == 2 || index == 1024 || index == 4096) { // Запросы во время поступления данных проверяют обновление неполного дерева.
actual.clear(); // Каждый запрос получает отдельный выходной вектор, как при перерисовке DSView.
envelope.lines(actual, 0, (index + 1) * 10, 1); // Вся текущая запись сворачивается в единственный пиксель.
double low = 1, high = -1; // Полный перебор эталона независимо находит оба экстремума.
for (const auto &line : original) { low = std::min(low, std::min(line.a, line.b)); high = std::max(high, std::max(line.a, line.b)); } // Эталон не использует дерево или его правила разбиения.
check(actual.size() == 1 && actual[0].a == low && actual[0].b == high, "streaming extrema"); // Даже незавершённый блок должен сохранять все пики.
} // Следующая итерация продолжает пополнять уже опрошенный индекс.
} // Готовая нерегулярная запись используется для проверки разных областей и масштабов.
for (size_t pixels : {size_t(1), size_t(7), size_t(31), size_t(512)}) { // Ширины охватывают одноточечный обзор и обычное экранное окно.
const uint64_t start = 17, end = 40953; // Обе границы проходят внутри сегментов и требуют интерполяции.
actual.clear(); // Результат не должен включать геометрию предыдущего масштаба.
envelope.lines(actual, start, end, pixels); // Запрашиваем плотное представление с обрезкой краёв.
check(actual.size() <= pixels * 2, "bounded geometry"); // Число вызовов рисования не должно расти с количеством периодов.
size_t column = 0; // Каждая вертикаль соответствует очередной непустой колонке.
for (const auto &line : actual) { // Соединительные отрезки не участвуют в сравнении вертикальных экстремумов.
if (line.start != line.end) continue; // Наклонные соединения обеспечивают непрерывность, а не диапазон колонки.
const double left = start + double(end - start) * column / pixels; // Эталон использует физические границы пикселя.
const double right = start + double(end - start) * (column + 1) / pixels; // Правая граница не округляется до номера периода.
double low = 1, high = -1; // Диапазон вычисляется полным перебором всех исходных сегментов.
for (const auto &source : original) { // Независимый алгоритм намеренно медленный, чтобы проверить индекс.
const double begin = std::max(source.start, left), finish = std::min(source.end, right); // Находим реальное пересечение сегмента с колонкой.
if (finish <= begin) continue; // Сегменты вне колонки не могут расширять её экстремумы.
const double a = source.a + (source.b - source.a) * (begin - source.start) / (source.end - source.start); // Левый конец обрезается аналитически.
const double b = source.a + (source.b - source.a) * (finish - source.start) / (source.end - source.start); // Правый конец обрезается независимо от индекса.
low = std::min(low, std::min(a, b)); // Сохраняем самый низкий из всех видимых концов.
high = std::max(high, std::max(a, b)); // Сохраняем самый высокий из всех видимых концов.
} // Полный перебор образует эталон без прореживания.
check(std::abs(line.a - low) < 1e-12 && std::abs(line.b - high) < 1e-12, "clipped pixel extrema"); // Пирамида должна давать тот же диапазон с точностью double.
++column; // Следующая вертикаль относится к следующему пикселю.
} // Проверены все вертикали текущего масштаба.
check(column == pixels, "all visible columns"); // Непрерывная запись должна покрывать каждую колонку.
} // Изменение ширины окна не должно менять измеренные данные.
actual.clear(); // Увеличенный фрагмент проверяется отдельно от обзорного графика.
envelope.lines(actual, 20000, 20100, 1000); // На крупном масштабе десять сегментов занимают тысячу пикселей.
check(actual.size() == 10, "zoom restores original segments"); // Исходные периоды не должны исчезать после обзорной отрисовки.
for (size_t index = 0; index < actual.size(); ++index) { // Сравниваем каждый восстановленный отрезок с полным эталоном.
const auto &expected = original[2000 + index]; // Номер исходного периода следует из координаты 20000 / 10.
check(actual[index].start == expected.start && actual[index].end == expected.end && actual[index].a == expected.a && actual[index].b == expected.b, "exact zoom values"); // Увеличение должно вернуть точные координаты и значения.
} // Проверка исключает разрушительное прореживание исходных аннотаций.
envelope.clear(); // Новый захват должен сбросить старые сегменты и все уровни индекса.
actual.clear(); // Удаляем геометрию предыдущей записи.
envelope.lines(actual, 0, 40970, 100); // Те же координаты после очистки больше не содержат сигнала.
check(actual.empty(), "clear removes stale extrema"); // Старые выбросы не должны переходить в новый анализ.
for (uint64_t index = 0; index < 100; ++index) { // Два участка разделены настоящим отсутствием данных.
envelope.append(index, index + 1, 0, 0); // Первая часть записи имеет нулевую модуляцию.
envelope.append(index, index, 1, 1); // Пустой сегмент не должен нарушать поиск или вызывать деление на ноль.
} // Первый участок занимает отсчёты от нуля до ста.
for (uint64_t index = 300; index < 400; ++index) envelope.append(index, index + 1, 1, 1); // Второй участок начинается после разрыва в двести отсчётов.
actual.clear(); // Проверяем плотную геометрию разрывной записи.
envelope.lines(actual, 0, 400, 4); // Разрыв занимает две полные экранные колонки.
check(actual.size() == 2 && actual[0].a == 0 && actual[1].a == 1, "gaps remain disconnected"); // Между участками не должно появиться искусственной наклонной линии.
for (double direction : {1.0, -1.0}) { // Угол может вращаться в обе стороны, поэтому проверяем сброс 360→0 и обратный переход 0→360.
envelope.clear(); // Каждый вариант вращения проверяется независимо от предыдущей записи.
envelope.append(0, 50, direction * 0.9, direction); // Первый участок заканчивается точно на одном краю нормированной шкалы θ.
envelope.append(50, 100, -direction, -direction * 0.9); // Смежный участок начинается на противоположном краю в тот же отсчёт.
actual.clear(); // Запрашиваем полную геометрию одного циклического перехода.
envelope.lines(actual, 0, 100, 1000); // Крупный масштаб должен сохранить точный момент сброса на отсчёте 50.
check(actual.size() == 3 && actual[1].start == 50 && actual[1].end == 50 && actual[1].a == direction && actual[1].b == -direction, "theta wrap has exact vertical edge"); // Между двумя пилообразными участками требуется вертикаль без ложной диагонали.
actual.clear(); // Повторный запрос начинается непосредственно с момента циклического перехода.
envelope.lines(actual, 50, 100, 1000); // Левая граница окна не должна скрывать вертикаль θ.
check(actual.size() == 2 && actual[0].start == 50 && actual[0].end == 50, "theta edge survives left viewport boundary"); // Соседний невидимый сегмент нужен только для восстановления фронта на границе.
envelope.clear(); // Отдельно отличаем циклический сброс от действительно неизвестного интервала.
envelope.append(0, 50, direction * 0.9, direction); // Первый известный участок остаётся тем же.
envelope.append(51, 100, -direction, -direction * 0.9); // Один пропущенный отсчёт обозначает отсутствие данных, а не мгновенный переход.
actual.clear(); // Старые вертикали не должны переноситься в новый запрос.
envelope.lines(actual, 0, 100, 1000); // Проверяем сохранность разрыва даже при большом увеличении.
check(actual.size() == 2 && actual[0].end == 50 && actual[1].start == 51, "unknown theta interval stays disconnected"); // Новый соединитель не имеет права достраивать неизвестные значения угла.
} // Проверка касается геометрии и не изменяет сами рассчитанные углы или выбор направления вращения.
envelope.clear(); // Длинный нагрузочный сценарий начинается с чистого состояния.
const auto build_start = std::chrono::steady_clock::now(); // Измеряем создание индекса отдельно от перерисовки.
for (uint64_t index = 0; index < 1000000; ++index) { // Миллион периодов соответствует 50 секундам при несущей 20 кГц.
envelope.append(index * 50, (index + 1) * 50, index == 500000 ? -1 : 0, index == 500001 ? 1 : 0); // Одиночные отрицательный и положительный выбросы должны пережить обзор.
} // Длинная запись содержит оба пика среди почти постоянного сигнала.
const auto query_start = std::chrono::steady_clock::now(); // Стоимость создания индекса не включается в задержку интерфейса.
for (int frame = 0; frame < 100; ++frame) { // Сто последовательных кадров выявляют случайный полный обход в перерисовке.
actual.clear(); // Размер геометрии измеряется для каждого кадра отдельно.
envelope.lines(actual, 0, 50000000, 1920); // Ширина 1920 пикселей моделирует полный экран с 50-секундным захватом.
check(actual.size() <= 3840, "million periods bounded"); // Даже миллион периодов выдаёт не более двух отрезков на пиксель.
} // Повторная отрисовка использует уже готовый индекс без разбора строк PWM1.
double low = 1, high = -1; // Проверяем, что нагрузочное уплотнение не скрывает одиночные выбросы.
for (const auto &line : actual) { low = std::min(low, std::min(line.a, line.b)); high = std::max(high, std::max(line.a, line.b)); } // Учитываем все видимые отрезки конечного кадра.
check(low == -1 && high == 1, "single period spikes preserved"); // Редкие пики должны оставаться видимыми среди миллиона периодов.
const auto finish = std::chrono::steady_clock::now(); // Фиксируем итог после всех запросов геометрии.
std::cout << "periods=1000000 lines=" << actual.size() << " build_ms=" << std::chrono::duration<double, std::milli>(query_start - build_start).count() << " frame_ms=" << std::chrono::duration<double, std::milli>(finish - query_start).count() / 100 << std::endl; // Отчёт позволяет сравнить задержку до и после исправления.
return 0; // Нулевой код означает прохождение функциональных и нагрузочных проверок.
} // Все ресурсы индекса освобождаются автоматически после завершения теста.