Расширить обмен по шинам, CAN-адаптеры и обработку графиков
Добавить очереди операций PBUS/SBUS, общую консоль, настройки Candle и SLCAN, декодеры DSView и обработку плотных графиков. Обновить протокол Бальзам и проверки общих компонентов.
This commit is contained in:
@@ -1,19 +1,31 @@
|
||||
"""Pixel-column envelope for drawing; measurement samples are never changed."""
|
||||
import math
|
||||
from bisect import bisect_left, bisect_right
|
||||
from .background_work import work_items # Фоновая подготовка длинных временных массивов должна уступать GIL во время заполнения и проверки порядка.
|
||||
|
||||
|
||||
class PointTimes: # Представление координат X для bisect без параметра key, отсутствующего в Python 3.8 сборки Win7.
|
||||
def __init__(self, points): # Принимаем индексируемые пары (X, Y), порядок которых проверяется вызывающим кодом.
|
||||
self.points = points # Храним ссылку на исходные точки, чтобы не копировать большой ряд при каждом изменении окна.
|
||||
|
||||
def __len__(self): # Предоставляем bisect длину исходного ряда для определения границ двоичного поиска.
|
||||
return len(self.points) # Получаем размер за постоянное время без перебора измерений.
|
||||
|
||||
def __getitem__(self, index): # Извлекаем одну координату по индексу, выбранному алгоритмом двоичного поиска.
|
||||
return self.points[index][0] # В паре (X, Y) индекс 0 обозначает время или частоту; значения Y не участвуют в сравнении.
|
||||
|
||||
|
||||
class IndexedTimes(list):
|
||||
"""Random-access timestamps, with ordering validated once when loaded."""
|
||||
def __init__(self, points):
|
||||
super().__init__(points)
|
||||
self.ordered = all(math.isfinite(x) and (i == 0 or self[i-1] <= x)
|
||||
for i, x in enumerate(self))
|
||||
def __init__(self, points, checkpoint=None): # Необязательная контрольная точка включает кооперативную подготовку только для фоновых файловых заданий.
|
||||
super().__init__(work_items(points, checkpoint)) # Заполняем полный массив, регулярно освобождая GIL при наличии фонового бюджета.
|
||||
self.ordered = all(math.isfinite(x) and (i == 0 or self[i-1] <= x) # Конечность и порядок проверяются по всем исходным меткам без прореживания.
|
||||
for i, x in work_items(enumerate(self), checkpoint)) # Второй длинный проход также уступает интерфейсу и реагирует на отмену.
|
||||
|
||||
|
||||
def viewport_samples(timestamps, values, left, right, neighbours=False, budget=None):
|
||||
"""Select only the viewport in logarithmic time for indexed records."""
|
||||
if isinstance(timestamps, (list, tuple, range)) and getattr(timestamps, "ordered", True):
|
||||
if getattr(timestamps, "ordered", isinstance(timestamps, (list, tuple, range))): # Проверенные индексируемые представления со смещением X также допускают двоичный поиск без копирования меток времени.
|
||||
start = bisect_left(timestamps, left)
|
||||
stop = bisect_right(timestamps, right)
|
||||
if neighbours:
|
||||
@@ -37,6 +49,10 @@ def pixel_envelope(timestamps, values, left, right, width):
|
||||
and zooming retain lines crossing the viewport boundaries.
|
||||
"""
|
||||
width = max(1, int(width))
|
||||
index = getattr(values, "envelope_index", None) # Подготовленный файловый ряд содержит индекс точных экстремумов для быстрого выбора экранных столбцов.
|
||||
if index is not None and index.count == len(values) and getattr(timestamps, "ordered", isinstance(timestamps, (list, tuple, range))): # Индекс применим к неизменённым значениям и проверенным координатам, включая ленивое смещение короткой развёртки.
|
||||
yield from index.points(timestamps, left, right, width) # Выдаём не более четырёх конечных точек на пиксель плюс соседей для отсечения краёв.
|
||||
return # Исключаем повторное линейное сканирование миллионов измерений при каждом изменении масштаба.
|
||||
scale = width / (right - left) if right > left else 1.
|
||||
bucket = None
|
||||
first = low = high = last = None
|
||||
@@ -73,6 +89,61 @@ def pixel_envelope(timestamps, values, left, right, width):
|
||||
yield from emit()
|
||||
|
||||
|
||||
def render_envelope(timestamps, values, left, right, width): # Экранная огибающая ограничивает плотный упорядоченный ряд двумя экстремумами на логический пиксель, сохраняя исходные измерения отдельно.
|
||||
width = max(1, int(width)) # Вызывающий виджет передаёт логическую ширину области: увеличение DPI не должно умножать число почти совпадающих вершин.
|
||||
ordered = getattr(timestamps, 'ordered', isinstance(timestamps, (list, tuple, range))) # Двоичный поиск и экранные столбцы применимы только к проверенному порядку временных координат.
|
||||
if not ordered: # Неупорядоченный поток нельзя безопасно переставлять по экранным столбцам или обрезать двоичным поиском.
|
||||
yield from pixel_envelope(timestamps, values, left, right, width) # Сохраняем прежнюю последовательность и разрывы для нестандартного источника без предположения об упорядоченности.
|
||||
return # Новый ограниченный экранный режим не меняет семантику неупорядоченных данных.
|
||||
count = min(len(timestamps), len(values)) # Общая длина исключает обращения за границу при несовпадающих размерах координат и амплитуд.
|
||||
start, stop = bisect_left(timestamps, left, 0, count), bisect_right(timestamps, right, 0, count) # Находим точное число исходных отсчётов видимого окна без полного обхода файла.
|
||||
if stop - start <= width: # При сильном увеличении все измерения помещаются в экранный бюджет и не требуют никакого сокращения.
|
||||
for position in range(max(0, start - 1), min(count, stop + 1)): # Сохраняем по одному соседу для линий, пересекающих края видимого окна.
|
||||
stamp, value = timestamps[position], values[position] # Извлекаем точную исходную пару, включая нечисловые амплитуды между конечными отсчётами.
|
||||
yield (stamp, value) if math.isfinite(stamp) and math.isfinite(value) else None # Все конечные точки остаются без изменений, а NaN и бесконечности разрывают путь.
|
||||
return # Разреженный зум не должен проходить через экстремумы столбцов и терять близкие исходные точки.
|
||||
index = getattr(values, 'envelope_index', None) # Подготовленный файловый ряд позволяет выбирать экстремумы без повторного линейного обхода миллионов значений.
|
||||
if index is not None and index.count == len(values): # Индекс допустим только для исходной неизменённой длины ряда.
|
||||
yield from index.points(timestamps, left, right, width, extrema_only=True) # Сохраняем максимум два экстремума на столбец, два конца окна и два внешних соседа.
|
||||
return # Быстрый запрос не должен дополнительно запускать линейный запасной путь.
|
||||
columns = {} # Для неиндексированного ряда храним лишь пару экстремумов на экранный столбец, а не полную запись.
|
||||
first_visible = last_visible = None # Дополнительно сохраняем два конца видимой линии для правильного соединения с внешними соседями.
|
||||
segment = 0 # Каждый нечисловой отсчёт увеличивает номер непрерывного участка; разные номера запрещают соединять выбранные точки.
|
||||
seen = False # Полностью нечисловое окно должно вернуть один разрыв, а пустая запись не создаёт искусственных точек.
|
||||
scale = width / (right - left) if right > left else 1. # Масштаб переводит время в столбцы; совпадающие временные координаты не допускают деления на ноль.
|
||||
for ordinal, (stamp, value) in enumerate(viewport_samples(timestamps, values, left, right, neighbours=True)): # Запасной путь читает каждый исходный отсчёт окна ровно один раз и поэтому также сохраняет одиночные пики.
|
||||
seen = True # Фиксируем наличие хотя бы одного конечного либо нечислового исходного измерения.
|
||||
if not math.isfinite(stamp) or not math.isfinite(value): # Пропуски не участвуют в экстремумах, но должны разделять любые выбранные точки по разные стороны.
|
||||
segment += 1 # Номер участка переносит информацию о пропуске без хранения каждого маркера плотного ряда.
|
||||
continue # Нечисловая амплитуда не имеет экранной координаты кривой.
|
||||
column = -1 if stamp < left else width if stamp > right else min(width - 1, int((stamp - left) * scale)) # Соседи вынесены в два отдельных столбца за пределами видимой области.
|
||||
point = (ordinal, stamp, value, segment) # Исходный порядок и номер участка позволяют вернуть экстремумы без ложных соединений через NaN.
|
||||
pair = columns.get(column) # В каждом экранном столбце сохраняются только точные минимум и максимум.
|
||||
if pair is None: # Первый конечный отсчёт одновременно задаёт обе роли нового столбца.
|
||||
columns[column] = [point, point] # Два совпадающих индекса позднее объединяются в одну вершину.
|
||||
else: # Дальнейшие измерения меняют только действительно достигнутые экстремумы.
|
||||
if value < pair[0][2]: # Строгое сравнение сохраняет первое вхождение равного минимума.
|
||||
pair[0] = point # Одиночный отрицательный выброс остаётся в экранной огибающей.
|
||||
if value > pair[1][2]: # Строгое сравнение сохраняет первое вхождение равного максимума.
|
||||
pair[1] = point # Одиночный положительный выброс также остаётся видимым при любом сжатии.
|
||||
if left <= stamp <= right: # Внешние соседи не подменяют начало и конец собственно видимых измерений.
|
||||
first_visible = point if first_visible is None else first_visible # Первый конечный видимый отсчёт сохраняется один раз независимо от числа столбцов.
|
||||
last_visible = point # Последний конечный видимый отсчёт обновляется до правого края окна.
|
||||
selected = {point[0]: point for pair in columns.values() for point in pair} # Уникальные исходные индексы устраняют совпадение ролей без изменения времени или амплитуды.
|
||||
for point in (first_visible, last_visible): # Всего две дополнительные конечные точки сохраняют соединения на краях видимого окна.
|
||||
if point is not None: # Полностью нечисловое окно не имеет конечных краевых измерений.
|
||||
selected[point[0]] = point # Повторное добавление совпавшего экстремума не увеличивает экранный бюджет.
|
||||
previous_segment = 0 # Нулевой участок соответствует началу исходной последовательности до первого NaN.
|
||||
for ordinal in sorted(selected): # Возвращаем выбранные точки в исходном порядке, включая совпадающие метки времени.
|
||||
_, stamp, value, current_segment = selected[ordinal] # Экранная вершина сохраняет исходные координаты и номер непрерывного участка.
|
||||
if current_segment != previous_segment: # Различные номера подтверждают хотя бы один NaN между выбранными конечными отсчётами.
|
||||
yield None # Маркер разрыва запрещает рисовать линию через отсутствующие измерения.
|
||||
yield stamp, value # Амплитуда и время возвращаются точно, без усреднения и сдвига коротких пиков.
|
||||
previous_segment = current_segment # Следующая вершина сравнивается с участком предыдущего выбранного измерения.
|
||||
if seen and (not selected or previous_segment != segment): # Сохраняем полностью нечисловое окно и разрыв после последней конечной точки.
|
||||
yield None # Длинная последовательность хвостовых пропусков ограничивается одним экранным маркером.
|
||||
|
||||
|
||||
def preview_window_count(requested, preview_count, original_count):
|
||||
"""Translate a raw-sample window to a display-envelope window."""
|
||||
if not original_count or original_count <= preview_count:
|
||||
|
||||
Reference in New Issue
Block a user