#include "pwm_waveform.h" // Проверяем тот же индекс, который использует нативная строка DSView. #include // Монотонные часы измеряют стоимость обзорной перерисовки длинной записи. #include // Результат содержит проверяемые размеры геометрии и измеренное время. #include // Детерминированный шум проверяет сохранение нерегулярных экстремумов. #include // Исключения завершают тест с ненулевым кодом даже в сборке 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 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(query_start - build_start).count() << " frame_ms=" << std::chrono::duration(finish - query_start).count() / 100 << std::endl; // Отчёт позволяет сравнить задержку до и после исправления. return 0; // Нулевой код означает прохождение функциональных и нагрузочных проверок. } // Все ресурсы индекса освобождаются автоматически после завершения теста.