# Тонкий адаптер реконструкции сигнала и преобразования напряжений в коды ЦАП. Массивы и # рабочая память подготавливаются в Python, а численный алгоритм и проверки диапазонов # выполняет общее C-ядро. """Thin host adapter for the shared C99 reconstruction/DAC algorithms.""" from __future__ import annotations import ctypes as C from dataclasses import dataclass from functools import lru_cache from protocan.native import NativeProtocol from time import perf_counter # Измеряем полное время обработки запроса, включая преобразование входных и выходных буферов. MAX_OUTPUT_POINTS = 1_000_000 BACKENDS = {"native": "Текущий МНК", "eigen": "Eigen QR"} # Стабильные ключи сохраняются в запросах и рецептах, а подписи показываются в GUI. METHODS = {"polynomial": "Аппроксимация: полином МНК", "linear": "Интерполяция: линейная", "pchip": "Интерполяция: PCHIP", "spline": "Восстановление: кубический сплайн"} @dataclass class Reconstruction: points: list input_count: int unique_count: int rmse: float pieces: tuple = () backend: str = "native" # Сохраняем выбранный вариант расчёта для сравнения и подписей результатов. elapsed_ms: float = 0.0 # Время полного вызова в миллисекундах позволяет оценивать стоимость преобразований вместе с решателем. @lru_cache(maxsize=1) def library(): try: lib = NativeProtocol().lib pointer = C.POINTER(C.c_double) lib.set_signal_reconstruct.argtypes = [pointer, pointer, C.c_size_t, C.c_int, C.c_uint, C.c_size_t, C.c_int, pointer, pointer, pointer, pointer, C.c_size_t] lib.set_signal_reconstruct.restype = C.c_int lib.set_signal_dac12.argtypes = [pointer, C.c_size_t, C.c_double, C.POINTER(C.c_uint16)] lib.set_signal_dac12.restype = C.c_int return lib except (AttributeError, OSError, RuntimeError) as error: raise ValueError("Пересоберите SETProtocol с set_signal.c и set_wavegen.c") from error def reconstruct(points, method="pchip", output_count=1000, degree=2, *, endpoint=True, with_model=False, backend="native"): # Новый именованный параметр сохраняет все существующие позиционные вызовы. started = perf_counter() # Начинаем замер до подготовки ctypes-массивов, одинаковой для двух решателей. if backend not in BACKENDS or (backend == "eigen" and method != "polynomial"): # Eigen предлагается только для полиномиального МНК, а неизвестные ключи отклоняются явно. raise ValueError("Eigen QR доступен только для полинома МНК; выберите корректный вариант расчёта") # Исключаем скрытое использование другого метода при сравнении. if method not in METHODS or type(output_count) is not int or not 2 <= output_count <= MAX_OUTPUT_POINTS: raise ValueError(f"Неизвестный метод или число выходных точек вне 2…{MAX_OUTPUT_POINTS}") count = len(points) if not 2 <= count <= 100000 or type(degree) is not int or not 1 <= degree <= 5: raise ValueError("Нужно 2…100000 исходных точек, степень 1…5") x, y = (C.c_double * count)(*(p[0] for p in points)), (C.c_double * count)(*(p[1] for p in points)) ox, oy, meta = (C.c_double * output_count)(), (C.c_double * output_count)(), (C.c_double * 3)() work = (C.c_double * (14 * count + 128))() core = library() # Существующая библиотека остаётся источником базового алгоритма и каталога установленного модуля. operation = core.set_signal_reconstruct # По умолчанию сохраняем исходный C99-решатель и его бинарный контракт. if backend == "eigen": # Загружаем дополнительный компонент только после явного выбора пользователя. from .signal_eigen import library as eigen_library # Ленивый импорт не требует наличия Eigen при обычной работе и в прошивках. operation = eigen_library(core._name).set_signal_reconstruct_eigen # Соседняя DLL реализует тот же контракт входов, выходов и work. code = operation(x, y, count, list(METHODS).index(method), degree, output_count, int(endpoint), ox, oy, meta, work, len(work)) # Оба решателя получают одинаковые данные и настройки сетки. if code: raise ValueError({1: "Недостаточно точек для выбранной степени или неверные параметры", 2: "Нужны конечные значения и минимум две различные временные точки", 3: "Неустойчивая аппроксимация: уменьшите степень", 5: "Недостаточно памяти или ошибка решателя Eigen"}.get(code, "Ошибка расчёта")) # Дополнительная настольная ошибка не меняет прежние коды C99. pieces = () if with_model: from .plot_formula import reconstruction_pieces if method == 'polynomial' and output_count <= degree: pieces = reconstruct(points, method, degree + 1, degree, endpoint=endpoint, with_model=True, backend=backend).pieces # Короткий выход требует отдельной сетки для формулы; сохраняем решатель и режим конечной точки. else: pieces = reconstruction_pieces(work, count, int(meta[1]), method, degree, oy, endpoint) result_points = list(zip(ox, oy)) # Включаем формирование выходных Python-точек в отображаемое время обработки. return Reconstruction(result_points, int(meta[0]), int(meta[1]), meta[2], pieces, backend, (perf_counter() - started) * 1000) # Переводим секунды монотонного таймера в миллисекунды без влияния изменения системных часов. def dac12(volts, vref=3.3): data = (C.c_double * len(volts))(*volts) codes = (C.c_uint16 * len(volts))() if library().set_signal_dac12(data, len(data), vref, codes): raise ValueError("Расчётная кривая выходит за диапазон ЦАП 0…Vref; измените точки или метод") return tuple(codes)