Добавить очереди операций PBUS/SBUS, общую консоль, настройки Candle и SLCAN, декодеры DSView и обработку плотных графиков. Обновить протокол Бальзам и проверки общих компонентов.
175 lines
31 KiB
Python
175 lines
31 KiB
Python
import math # Проверяем конечность измерений, чтобы пропуски не соединялись линиями и не участвовали в экстремумах.
|
|
from array import array # Храним индексы в компактных массивах вместо отдельных объектов Python для каждого узла дерева.
|
|
from bisect import bisect_left, bisect_right # Находим границы пиксельных столбцов двоичным поиском по упорядоченным меткам времени.
|
|
|
|
|
|
class EnvelopeIndex: # Индекс неизменяемого ряда позволяет выбирать точные экстремумы без повторного обхода миллионов отсчётов.
|
|
BLOCK_SIZE = 32 # Лист дерева содержит 32 отсчёта: крайние неполные блоки малы, а индекс занимает около 2,5 байта на отсчёт.
|
|
EMPTY = (-1, -1, -1, -1, 0) # Первые четыре поля обозначают индексы первого, минимального, максимального и последнего отсчёта; пятое считает пропуски.
|
|
|
|
def __init__(self, values, checkpoint=None): # Необязательная контрольная точка сохраняет отзывчивость Qt при первоначальном построении большого индекса.
|
|
self.values = values # Сохраняем исходные значения по ссылке; сам индекс не сокращает и не изменяет измерения.
|
|
self.count = len(values) # Запоминаем размер снимка для проверки пригодности индекса после очистки графика.
|
|
blocks = (self.count + self.BLOCK_SIZE - 1) // self.BLOCK_SIZE # Округление вверх включает неполный последний блок записи.
|
|
self.leaves = 1 # Для обычного двоичного дерева число листьев должно быть степенью двух.
|
|
while self.leaves < blocks: # Добавляем пустые листья только до ближайшей степени двух, не удваивая размер без необходимости.
|
|
self.leaves *= 2 # Два дочерних узла соответствуют объединённому диапазону родителя.
|
|
self.nodes = tuple(array('q', [field]) * (2 * self.leaves) for field in self.EMPTY) # Пять знаковых 64-битных массивов содержат индексы, включая признак отсутствия -1.
|
|
for block in range(blocks): # Полный обход исходных значений нужен только при первоначальном построении индекса.
|
|
if checkpoint is not None and block % 32 == 0: # Тридцать два листа по 32 отсчёта дают контроль примерно через 1024 исходных значения.
|
|
checkpoint() # Отпускаем GIL и проверяем отмену между ограниченными порциями подготовки экстремумов.
|
|
start = block * self.BLOCK_SIZE # Переводим номер блока в начальный индекс исходного отсчёта.
|
|
self._store(self.leaves + block, self._scan(start, min(self.count, start + self.BLOCK_SIZE))) # Сводка листа сохраняет точные экстремумы и число нечисловых отсчётов.
|
|
for node in range(self.leaves - 1, 0, -1): # Строим родителей снизу вверх, когда сводки обоих детей уже готовы.
|
|
if checkpoint is not None and node % 1024 == 0: # Объединение большого дерева также делится на порции по 1024 узла.
|
|
checkpoint() # Не допускаем непрерывного удержания GIL после завершения чтения исходных измерений.
|
|
self._store(node, self._merge(self._load(2 * node), self._load(2 * node + 1))) # Родитель объединяет соседние диапазоны с сохранением временного порядка.
|
|
|
|
def _store(self, node, summary): # Записываем одну сводку в параллельные компактные массивы дерева.
|
|
for column, field in zip(self.nodes, summary): # Каждое поле имеет одинаковый номер узла во всех пяти массивах.
|
|
column[node] = field # Индексы ссылаются на исходный ряд, поэтому значения Y не дублируются в памяти.
|
|
|
|
def _load(self, node): # Читаем сводку готового узла без обращения ко всем его исходным отсчётам.
|
|
first, low, high, last, gaps = self.nodes # Разбираем пять массивов один раз, исключая создание генератора при каждом посещении узла дерева.
|
|
return first[node], low[node], high[node], last[node], gaps[node] # Возвращаем те же пять полей напрямую; тысячи запросов кадра не создают промежуточных итераторов.
|
|
|
|
def _scan(self, start, stop): # Сканируем полуинтервал только при построении листа либо на неполных краях запроса.
|
|
first = low = high = last = -1 # Отрицательный индекс означает, что конечное значение ещё не найдено.
|
|
gaps = 0 # Подсчёт пропусков позволяет проверять разрыв линии между выбранными экстремумами.
|
|
values, isfinite = self.values, math.isfinite # Локальные ссылки исключают повторный поиск атрибутов для каждого отсчёта неполного края блока.
|
|
low_value = high_value = 0. # Значения обоих экстремумов устанавливаются первым конечным отсчётом; ноль не участвует в сравнении до этого момента.
|
|
for index in range(start, stop): # Полуинтервал исключает stop и совпадает с правилами срезов Python.
|
|
value = values[index] # Читаем исходное измерение ровно один раз, сохраняя значение для обоих сравнений экстремумов.
|
|
if not isfinite(value): # NaN и бесконечности должны разрывать путь, а не менять масштаб или экстремумы.
|
|
gaps += 1 # Учитываем пропуск даже в диапазоне, где совсем нет конечных значений.
|
|
continue # Исключаем нечисловой отсчёт из поиска первого, последнего, минимального и максимального значений.
|
|
if first < 0: # Первый конечный отсчёт одновременно задаёт оба начальных экстремума.
|
|
first = low = high = index # Все три индекса указывают на одно измерение до появления следующих точек.
|
|
low_value = high_value = value # Сохраняем амплитуды начальных экстремумов, чтобы следующие отсчёты не читали их повторно из большого ряда.
|
|
else: # Для остальных отсчётов обновляем только реально изменившиеся экстремумы.
|
|
if value < low_value: # Строгое сравнение с сохранённой амплитудой сохраняет первый по времени минимум при равных значениях.
|
|
low = index # Запоминаем точный индекс минимального исходного значения.
|
|
low_value = value # Обновляем амплитуду вместе с индексом, исключая отдельное чтение минимума на следующей итерации.
|
|
if value > high_value: # Первый по времени максимум также остаётся стабильным при равенстве значений.
|
|
high = index # Запоминаем точный индекс максимального исходного значения.
|
|
high_value = value # Сохраняем амплитуду нового максимума для дальнейших сравнений без обращения к исходному контейнеру.
|
|
last = index # Последний конечный отсчёт нужен для правильного соединения соседних пиксельных столбцов.
|
|
return first, low, high, last, gaps # Сводка содержит только индексы и счётчик; измерения остаются в исходном ряду.
|
|
|
|
def _merge(self, before, after): # Объединяем сводки соседних диапазонов, расположенных в исходном временном порядке.
|
|
gaps = before[4] + after[4] # Число пропусков аддитивно и не зависит от расположения экстремумов.
|
|
if before[0] < 0: # Левый диапазон может быть пустым либо полностью состоять из пропусков.
|
|
return (*after[:4], gaps) # Сохраняем конечные точки правого диапазона и пропуски обоих диапазонов.
|
|
if after[0] < 0: # Пустой правый диапазон не меняет конечные точки левого диапазона.
|
|
return (*before[:4], gaps) # Добавляем только число пропусков справа.
|
|
low = after[1] if self.values[after[1]] < self.values[before[1]] else before[1] # При равных минимумах сохраняем более ранний отсчёт.
|
|
high = after[2] if self.values[after[2]] > self.values[before[2]] else before[2] # При равных максимумах также сохраняем более ранний отсчёт.
|
|
return before[0], low, high, after[3], gaps # Крайние точки берём из внешних границ объединённого диапазона.
|
|
|
|
def between(self, start, stop): # Возвращаем точную сводку произвольного диапазона индексов без полного сканирования ряда.
|
|
start, stop = max(0, start), min(self.count, stop) # Ограничиваем запрос существующими измерениями снимка.
|
|
if stop <= start: # Пустое или обратное окно не содержит ни точек, ни пропусков.
|
|
return self.EMPTY # Единый пустой результат упрощает объединение диапазонов на границах записи.
|
|
if not self.nodes[4][1] and isinstance(self.values, (list, tuple, array)): # Нулевой счётчик пропусков корня доказывает конечность всего неизменяемого ряда; обычные контейнеры допускают короткие срезы краёв.
|
|
return self._finite_between(start, stop) # Плотные записи без NaN обходятся без пяти-польных сводок и проверок конечности на каждом кадре.
|
|
first_block = (start + self.BLOCK_SIZE - 1) // self.BLOCK_SIZE # Находим первый полностью включённый блок после левого неполного края.
|
|
last_block = stop // self.BLOCK_SIZE # Этот блок уже не входит целиком в запрос и задаёт начало правого края.
|
|
if first_block >= last_block: # Короткий запрос дешевле прочитать напрямую, чем обходить дерево.
|
|
return self._scan(start, stop) # Здесь читается не более двух неполных блоков исходных отсчётов.
|
|
before = self._scan(start, first_block * self.BLOCK_SIZE) # Получаем сводку левого неполного края, максимум из 31 отсчёта.
|
|
after = self._scan(last_block * self.BLOCK_SIZE, stop) # Получаем сводку правого неполного края, также максимум из 31 отсчёта.
|
|
left, right = self.leaves + first_block, self.leaves + last_block # Переводим границы полных блоков в номера листьев дерева.
|
|
while left < right: # За логарифмическое число шагов выбираем узлы, полностью покрывающие внутренний диапазон.
|
|
if left % 2: # Правый ребёнок слева включается отдельно, чтобы не захватить отсчёты перед запросом.
|
|
before = self._merge(before, self._load(left)) # Добавляем очередной диапазон после уже выбранной левой части.
|
|
left += 1 # Переходим к следующему соседнему узлу перед подъёмом к родителю.
|
|
if right % 2: # Правая исключённая граница позволяет включить её левого соседа отдельно.
|
|
right -= 1 # Перемещаем границу к последнему целиком включённому узлу текущего уровня.
|
|
after = self._merge(self._load(right), after) # Добавляем узел перед уже выбранной правой частью, сохраняя порядок.
|
|
left //= 2 # Поднимаем левую границу на уровень объединённых диапазонов.
|
|
right //= 2 # Правая граница поднимается на тот же уровень дерева.
|
|
return self._merge(before, after) # Соединяем выбранные части в одну точную сводку запрошенного полуинтервала.
|
|
|
|
def _finite_between(self, start, stop): # Быстрый запрос применяется только после проверки отсутствия NaN и бесконечностей во всём исходном ряду.
|
|
values = self.values # Сохраняем локальную ссылку на полные исходные амплитуды, не создавая их копию.
|
|
first_block = (start + self.BLOCK_SIZE - 1) // self.BLOCK_SIZE # Левый неполный край заканчивается перед первым целиком включённым блоком.
|
|
last_block = stop // self.BLOCK_SIZE # Последний целый блок предшествует правому неполному краю запроса.
|
|
if first_block >= last_block: # Короткий диапазон занимает не более двух блоков по 32 отсчёта и не требует обхода дерева.
|
|
chunk = values[start:stop] # Временный срез содержит максимум 63 исходных амплитуды, независимо от полного размера лога.
|
|
return start, start + chunk.index(min(chunk)), start + chunk.index(max(chunk)), stop - 1, 0 # Встроенные операции находят точные первые экстремумы на C; первый, последний и нулевые пропуски уже известны.
|
|
low = high = start # Любой непустой конечный диапазон имеет допустимое начальное измерение на левой границе.
|
|
low_value = high_value = values[start] # Амплитуды экстремумов храним локально, чтобы не перечитывать их из большого массива после каждого узла.
|
|
for begin, end in ((start, first_block * self.BLOCK_SIZE), (last_block * self.BLOCK_SIZE, stop)): # Сканируем только два неполных края, каждый не длиннее 31 исходного отсчёта.
|
|
if begin < end: # Совпадение с границей блока исключает создание пустого среза и вызов min/max без элементов.
|
|
chunk = values[begin:end] # Короткий временный срез позволяет выполнить поиск экстремумов встроенными операциями C.
|
|
minimum, maximum = min(chunk), max(chunk) # Исходные конечные амплитуды сравниваются без повторной проверки math.isfinite.
|
|
if minimum < low_value: # Края идут слева направо, поэтому при равенстве оставляем уже найденный более ранний минимум.
|
|
low, low_value = begin + chunk.index(minimum), minimum # index возвращает первое вхождение точного минимума внутри неполного края.
|
|
if maximum > high_value: # При равенстве максимумов также сохраняем более раннее измерение.
|
|
high, high_value = begin + chunk.index(maximum), maximum # Индекс исходной амплитуды вычисляется смещением от начала короткого среза.
|
|
lows, highs = self.nodes[1], self.nodes[2] # Для конечного ряда нужны только индексы минимумов и максимумов; остальные три массива не читаются.
|
|
left, right = self.leaves + first_block, self.leaves + last_block # Переводим полуинтервал целых блоков в номера листьев компактного дерева.
|
|
while left < right: # Как и общий алгоритм, выбираем лишь логарифмическое число готовых сводок диапазона.
|
|
if left % 2: # Отдельный правый ребёнок слева входит в запрос целиком, а его сосед остаётся за границей.
|
|
candidate = lows[left] # Готовый индекс минимума узла ссылается непосредственно на исходный ряд.
|
|
value = values[candidate] # Читаем только амплитуду найденного в узле минимума.
|
|
if value < low_value or (value == low_value and candidate < low): # Узлы посещаются не строго слева направо, поэтому равные значения сравниваем и по исходному индексу.
|
|
low, low_value = candidate, value # Обновляем точное первое вхождение минимума без создания промежуточной сводки.
|
|
candidate = highs[left] # Второй готовый индекс узла соответствует точному исходному максимуму.
|
|
value = values[candidate] # Амплитуда максимума также читается один раз на выбранный узел.
|
|
if value > high_value or (value == high_value and candidate < high): # Равные максимумы должны сохранять прежний выбор самого раннего отсчёта.
|
|
high, high_value = candidate, value # Обновляем максимум и его амплитуду вместе для следующего сравнения.
|
|
left += 1 # Исключаем уже обработанный узел перед подъёмом обеих границ на следующий уровень.
|
|
if right % 2: # Левый сосед исключённой правой границы входит в диапазон полностью.
|
|
right -= 1 # Получаем номер последнего целиком включённого узла текущего уровня дерева.
|
|
candidate = lows[right] # Читаем индекс готового минимума правого узла без распаковки пяти полей сводки.
|
|
value = values[candidate] # Только выбранный минимум требует обращения к исходной амплитуде.
|
|
if value < low_value or (value == low_value and candidate < low): # Порядок обхода не влияет на первый по времени минимум при равенстве амплитуд.
|
|
low, low_value = candidate, value # Сохраняем лучший исходный минимум для объединённого запроса.
|
|
candidate = highs[right] # Индекс максимума берём из второго массива готовых экстремумов.
|
|
value = values[candidate] # Читаем точную амплитуду исходного максимума правого узла.
|
|
if value > high_value or (value == high_value and candidate < high): # При одинаковых амплитудах выбираем наименьший исходный индекс.
|
|
high, high_value = candidate, value # Объединяем максимум напрямую, исключая вызовы _load и _merge на каждом узле.
|
|
left //= 2 # Переходим от листьев или текущих узлов к покрывающим их родителям.
|
|
right //= 2 # Правая исключённая граница поднимается на тот же уровень дерева.
|
|
return start, low, high, stop - 1, 0 # Все точки конечны, поэтому внешние отсчёты и отсутствие пропусков не требуют отдельного вычисления.
|
|
|
|
def points(self, timestamps, left, right, width, *, extrema_only=False): # Обычная огибающая сохраняет четыре роли на столбец, а отдельный экранный режим оставляет только точные минимум и максимум.
|
|
count = min(len(timestamps), self.count) # Общий размер защищает от несовпадающих длин координат и значений.
|
|
start, stop = bisect_left(timestamps, left, 0, count), bisect_right(timestamps, right, 0, count) # Оба края видимого временного окна включаются полностью.
|
|
ranges = [] # Список содержит только диапазоны пикселей и по одному соседнему отсчёту для отсечения на краях.
|
|
if start: # Сосед слева сохраняет линию, пересекающую левую границу без отсчёта непосредственно внутри окна.
|
|
ranges.append((start - 1, start)) # Только один внешний отсчёт нужен для правильного геометрического отсечения.
|
|
if right <= left: # Совпадающие границы X могут содержать много отсчётов с одной меткой времени, но не допускают деления на ширину окна.
|
|
if stop > start: # Пустое окно не создаёт искусственный диапазон измерений.
|
|
ranges.append((start, stop)) # Экстремумы всех совпадающих меток времени ограничены четырьмя точками одного столбца.
|
|
else: # Любое окно делим по экранным координатам, чтобы неравномерные и повторяющиеся метки также соблюдали бюджет на столбец.
|
|
edge = start # Первый пиксель начинается с первого отсчёта внутри видимого окна.
|
|
for column in range(width): # Число итераций зависит только от ширины изображения в физических пикселях.
|
|
boundary = left + (right - left) * ((column + 1) / width) # Правая граница столбца задана в единицах исходной оси X.
|
|
end = stop if column == width - 1 else bisect_left(timestamps, boundary, edge, stop) # Последний столбец включает отсчёты точно на правой границе окна.
|
|
if end > edge: # Пустые пиксели не создают искусственных измерений.
|
|
ranges.append((edge, end)) # Запоминаем исходный диапазон для выбора первого, минимального, максимального и последнего отсчётов.
|
|
edge = end # Следующий столбец продолжает ряд без потерь и повторного включения границы.
|
|
if stop < count: # Сосед справа сохраняет линию, пересекающую правую границу окна.
|
|
ranges.append((stop, stop + 1)) # Внешний сосед добавляет не более одной конечной точки к пиксельному бюджету.
|
|
has_gaps = bool(self.nodes[4][1]) # Нулевой счётчик корня доказывает отсутствие пропусков во всём ряду и исключает лишние запросы между компактными экстремумами.
|
|
previous = None # Индекс предыдущей выбранной конечной точки нужен для проверки разрыва между столбцами.
|
|
for begin, end in ranges: # Обрабатываем только экранные диапазоны и двух возможных внешних соседей.
|
|
summary = self.between(begin, end) # Получаем точные экстремумы через индекс, читая лишь неполные края блоков.
|
|
roles = summary[1:3] if extrema_only else summary[:4] # Новый экранный режим уменьшает число вершин, не изменяя исходные экстремумы или прежний публичный результат.
|
|
if extrema_only and begin == start: # Первый видимый столбец сохраняет также начало линии, чтобы она правильно пересекала левый край окна.
|
|
roles += (summary[0],) # Дополнительная роль нужна только одному столбцу, а не каждому пикселю.
|
|
if extrema_only and end == stop: # Последний видимый столбец сохраняет конец линии для правильного пересечения правого края.
|
|
roles += (summary[3],) # Два внешних конца увеличивают общий бюджет только на две конечные точки.
|
|
selected = sorted({index for index in roles if index >= 0}) # Убираем совпадающие роли и восстанавливаем исходный временной порядок выбранных экстремумов.
|
|
if not selected: # Полностью нечисловой диапазон разрывает линию один раз независимо от числа NaN внутри пикселя.
|
|
yield None # Маркер разрыва не является измерением и обрабатывается обоими движками отрисовки.
|
|
previous = None # Следующая конечная точка начинает новый путь.
|
|
continue # В пустом пикселе нет конечных экстремумов для отрисовки.
|
|
for index in selected: # На один непустой пиксель приходится не более четырёх исходных измерений.
|
|
if has_gaps and previous is not None and previous + 1 < index and (summary[4] or previous < begin) and self.between(previous + 1, index)[4]: # Проверяем разрывы только при наличии NaN в записи; соседние индексы и доказанно конечные ряды не требуют дополнительных запросов дерева.
|
|
yield None # Разрыв сохраняет пропуски между выбранными экстремумами без повторного запроса того же диапазона дерева.
|
|
yield timestamps[index], self.values[index] # Возвращаем исходные координаты и значения без усреднения или изменения амплитуды.
|
|
previous = index # Запоминаем конец текущего пути для следующей выбранной точки.
|