Вычисляем скорость наименьшими квадратами
Запись от Eddy_Em размещена 01.08.2025 в 08:45
Показов 5334
Комментарии 2
Метки c, math, вычисления, математика, си
|
Вожусь с системой управления телескопами на основе контроллеров sidservo (адская дрянь, но что есть, то есть). "Особенностью" контроллера является то, что невозможно получить текущее значение скорости, с которой телескоп двигается по обеим осям. Да и сам ответ на запрос состояния приходит через 50-60мс. Никакой стабильности нет. Поэтому единственный вариант - пользоваться осевыми энкодерами, которые показывают реальное положение телескопа, и которые я могу опрашивать с частотой до 4кГц (для этого сделал специальный контроллер на основе STM32: при подключении к компьютеру создаются устройства /dev/encoder_cmd0, /dev/encoder_X0 и /dev/encoder_Y0: настройки и данные с энкодеров в строковом формате). Сначала вычислял скорость 1 раз в промежуток времени как простое отношение разности координат к разности времен. Конечно, получалось очень шумно и ступенчато. Сейчас запилил "плавающее" вычисление наименьшими квадратами. Значительно красивее вышло. И по "болтанке" скорости на интервале, где она должна быть постоянна, сразу понял, что я неправильно настроил ПИД-регулятор sidservo (то ли I, то ли P задрал выше, чем нужно). Код прост до примитивизма: заводим массивы для накопления N данных в соответствии с каждой используемой суммой. При поступлении новых данных в сумму добавляем свежее и вычитаем последнее.
| |||||
Метки c, math, вычисления, математика, си
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 2
Комментарии
-
Крутое решение Переход на "скользящую" МНК для оценки скорости по энкодерам — именно то, что нужно при рваных ответах sidservo. Ниже — точечные улучшения, которые обычно дают ещё более гладкую и устойчивую скорость, плюс пара альтернатив для сравнения.
Что улучшить прямо в вашей МНК-реализации
Центрируйте время (и координату) в формулах.
Ваши формулы эквивалентны стандартной регрессии, но в таком виде часто страдают от потери точности, когда t большие, а разброс по окну небольшой (катастрофическая отмена). Вычисляйте наклон через ковариацию и дисперсию «по центру окна»:
Пусть Sx = sum(x), St = sum(t), Sxx = sum(x*x) (не обязательно, но может пригодиться), Stt = sum(t*t), Sxt = sum(x*t), n = N.
Тогда
num = Sxt - (St * Sx) / n (ковариация t и x * n)
den = Stt - (St * St) / n (дисперсия t * n)
slope = num / den
Это численно устойчивее, чем n*Sxt - Sx*St и n*Stt - St*St.
Ведите счётчик фактического заполнения окна.
Пока окно ещё не заполнено, используйте n = count, а не arraysz. Это уменьшит "дёрганье" на старте.
Монотонные метки времени и локальная шкала.
Используйте monotonic clock. Для численной устойчивости храните t "локально": например, вычтите t0 (время первого/самого старого элемента в окне). Так уменьшится величина самих t, а формулы будут точнее.
Компенсированное суммирование (Kahan).
На 4 кГц даже с double стоит добавить компенсацию суммы для Sx, St, Stt, Sxt, чтобы замедлить накопление плавающей ошибки.
Границы и NaN-ы.
Если den слишком мал (разброс времени в окне почти нулевой из-за одинаковых меток), возвращайте последнюю валидную скорость или применяйте плавный hold/экстраполяцию.
Неровная дискретизация.
Ваш метод уже устойчив к неравномерным t. Это преимущество перед обычными центральными разностями. Главное — чтобы t были реальными метками, а не счётчиком сэмплов.
Небольшой рефактор под центрирование и "тёплый старт" :
Если хотите ещё устойчивее — храните t как "смещённые": t' = t - t_ref (например, t_ref = время самого старого элемента) и обновляйте все четыре суммы в этих координатах.C 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
typedef struct{ double *x, *t, *t2, *xt; double Sx, St, Stt, Sxt; size_t idx, arraysz, count; } ls_t; ls_t *ls_init(size_t N){ if (N < 5) return NULL; ls_t *l = calloc(1, sizeof(*l)); l->x = calloc(N, sizeof(double)); l->t = calloc(N, sizeof(double)); l->t2 = calloc(N, sizeof(double)); l->xt = calloc(N, sizeof(double)); l->arraysz = N; return l; } void ls_free(ls_t **lp){ if (!lp || !*lp) return; ls_t *l = *lp; free(l->x); free(l->t); free(l->t2); free(l->xt); free(l); *lp = NULL; } double ls_push_and_slope(ls_t *l, double x, double t){ if (!l) return 0.0; size_t i = l->idx; double oldx = l->x[i], oldt = l->t[i], oldt2 = l->t2[i], oldxt = l->xt[i]; double t2 = t * t, xt = x * t; l->x[i] = x; l->t[i] = t; l->t2[i] = t2; l->xt[i] = xt; if (l->count < l->arraysz) l->count++; l->idx = (i + 1 == l->arraysz) ? 0 : i + 1; // обновить суммы l->Sx += x - oldx; l->St += t - oldt; l->Stt += t2 - oldt2; l->Sxt += xt - oldxt; double n = (double)l->count; // центрированные ковариация и дисперсия времени double num = l->Sxt - (l->St * l->Sx) / n; double den = l->Stt - (l->St * l->St) / n; if (fabs(den) < 1e-12) return 0.0; return num / den; // скорость }
Альтернативы (иногда лучше МНК)
Savitzky–Golay (SG) дифференциатор: полиномиально-аппроксимирующий FIR-фильтр, сразу даёт сглаженную оценку производной. Для равномерной дискретизации заранее посчитали коэффициенты — и просто свёртка по окну. Прелесть SG в том, что он лучше сохраняет формы (постоянную скорость и постоянное ускорение) и даёт малую задержку для короткого окна.
α–β (g-h) фильтр / "poor man’s Kalman": если модель движений оси — "положение + скорость + белый шум", α–β фильтр очень хорош: вычислительно лёгкий, выдаёт гладкую скорость с минимальной фазовой задержкой. Подстройка α,β по вашим шумам даёт отличное поведение для наведения.
Калман по положению-скорости (2-состояния): если готовы к матрицам — получите оптимальную оценку скорости с учётом шума квантования энкодера, джиттера времени и, при желании, командного профиля скорости как входа (feed-forward).
Практика для телескопов и связи с ПИД
Двухконтурное управление: внутренний контур по скорости (от вашей оценки), внешний — по положению. Это резко уменьшает "болтанку".
Внутренний — PI по скорости, без интегрального насыщения (anti-windup обязателен).
Внешний — P по положению + небольшая I (или вообще без I), плюс feed-forward по скорости из траекторного планировщика (если он есть).Запись от Python_Val размещена 13.08.2025 в 05:47
-
Настройка: сначала добейтесь устойчивого и быстрого скоростного контура (по вашей slope) при закрытом положении (маленький P по положению), затем медленно поднимайте P по положению. Если видите "пилу" скорости при постоянной команде — обычно завышен I скоростного или P позиционного.
Окно и задержка: при 4 кГц окно 21–41 отсчёт даёт сглаживание с задержкой порядка 2–5 мс — обычно ок для наведения. Если нужно быстрее — уменьшайте окно, но компенсируйте шумом SG/α–β.
Квантование энкодеров: при малом шаге и малых скоростях полезно усреднять "дельты" по окну (не сырые положения), либо держать окно в "импульсах", а не в миллисекундах, чтобы каждый шаг кванта попадал в регрессию несколько раз.
Джиттер времени: ваша МНК уже учитывает реальные t, но важно, чтобы t были из одного монотонного источника (CLOCK_MONOTONIC).
Итог
Ваш метод — отличный базис. Добавьте:
центрирование в формулах + счётчик заполнения окна,
(по желанию) Kahan и локальную шкалу времени,
и подумайте про SG или α–β как альтернативы, если захочется ещё меньше задержки при том же шуме.
Если хотите, скидывайте кусок кода чтения /dev/encoder_* и как вы формируете t — могу адаптировать под ваши таймеры (монотонные, тики, double сек) и подобрать размеры окна / параметры α–β под характерный шум ваших энкодеров.Запись от Python_Val размещена 13.08.2025 в 05:48


