Docs: repeat-detection findings and interval backlog notes

This commit is contained in:
4aiman 2026-09-21 12:42:57 +03:00
commit d6139a99f7

View file

@ -351,3 +351,299 @@ Aeropod). Трубка, направленная вперёд, плюс датч
Готовый код обоих методов варианта 1 -- в `tools/track/track.py`
(функция `wind_by_passes` и секция «ветер» в `report`), выше описан
алгоритмически.
---
# БЭКЛОГ: автоматический разбор повторов внутри одной поездки
Заказано пользователем 17 сентября 2026.
## Задача
Пользователь катает интервалы: приезжает к подъёму, проезжает его 5 раз,
скатываясь вниз между заездами, потом уезжает домой. Сейчас, чтобы разобрать
такую тренировку, приходится либо останавливать и заново стартовать трек
на каждом повторе (пользователь называет это бредом), либо разбирать
вручную задним числом.
Нужно: **один трек на всю тренировку, повторы находятся сами.** Пользователь
готов на отдельный режим «велотренировка», если так проще.
## Что должно получиться на выходе
Таблица повторов: номер, время, средняя скорость, каденс, мощность.
И главное число, по которому принимается решение о завтрашнем дне:
отношение последнего повтора к первому
Порог, выведенный из данных (11 и 17 сентября, по 5 повторов):
- последний быстрее или равен первому -- не на пределе, можно повторить завтра
- последний медленнее на 5-10% -- нормальная тяжёлая тренировка
- последний медленнее больше чем на 10% -- два дня спокойно
## Алгоритм, проверен вручную на этих данных
Повторы ищутся по самоподобию трека, без опорных точек от пользователя.
1. **Разложить точки поездки по ячейкам** ~10 м (сетка как в `heightmap.py`).
2. **Найти ячейки, посещённые больше одного раза**, и отбросить те, где
направление движения (`bearing_deg`) при посещениях различается больше
чем на 60 градусов: проезд вниз по той же дороге повтором не считается.
**Без проверки направления половина повторов теряется или считается
задом наперёд** -- проверено, это главная ловушка.
3. **Собрать самую длинную цепочку** таких ячеек, идущую подряд по ходу
движения. Это и есть повторяемый участок. Требовать длину не меньше
300 м и не меньше 3 проходов, иначе это перекрёсток у дома, а не подъём.
4. **Разметить проходы:** для каждого прохода записать вход и выход
по цепочке, время, длину.
5. **Нормировать время на общую длину:** у пользователя GPS даёт от 1138
до 1199 м на одном и том же подъёме, то есть 5% разброса. Без нормировки
сравнение повторов между собой врёт.
## Чего делать НЕ надо
**Не просить пользователя ставить метки.** Он специально сказал, что
останавливать и перезапускать трек не будет.
**Не опираться на высоту отдельного проезда.** Профиль повторяемого участка
брать из карты высот по многим проездам (см. соседний раздел): у одиночного
проезда GPS даёт ступеньки вида 0, 0, 0, 0, 8.5 м на ровном месте.
**Не считать мощность усреднением по точкам.** Усреднять по времени.
При усреднении по точкам отрезки чуть разной длины дают разный вес
медленным кускам, и на одинаковой скорости выходят разные ватты
(проверено: 17.9 км/ч -> 173 Вт и 18.1 км/ч -> 209 Вт на одном подъёме).
## Готовый код
Ручная версия детектора по двум опорным точкам -- в истории этой сессии,
в `tools/track/`. Она находит проходы правильно, но опорные точки задаются
руками. Автоматизировать надо шаги 1-3.
---
# БЭКЛОГ: массовое уточнение высот по всем трекам
Заказано пользователем 17 сентября 2026. Приоритет свой, не срочный.
## Что нужно
Пункт меню, который уточняет высоты **пакетно**: идёт от последнего трека
назад по времени и обрабатывает те, где `altitude_srtm_m` ещё не заполнена.
Пользователь запускает его вручную, когда сидит на проводном интернете.
Требования к исполнению, из общих правил проекта:
- показывать прогресс числами: сколько треков обработано, сколько осталось
- прерываемость: в поле связь рвут РЭБ и РКН, процесс должен переживать
обрыв и продолжаться с того места, где встал, а не начинать заново
- уже уточнённые треки не трогать повторно
## Зачем, с измерениями
Аудит всех 83 велопоездок показал, что **высота за май и июнь непригодна**:
май-июнь: набор в БД 0-23 м, расчётный по сглаженной высоте 50-207 м
июль-сентябрь: набор в БД и расчётный сходятся (168/138, 251/275, 227/235)
До 17 мая запись шла раз в 3-4 секунды при шуме высотомера 0.8 м между
соседними точками. Пример 10 мая (session_id 3): сырая высота скачет
`36 20 23 20 20 36 36 35 35 35 34 34 33 20 23 20 19 32 32 32 31`, то есть
приёмник бегает между двумя уровнями с размахом 16 м. Медианный фильтр
и скользящее среднее это приглаживают, но остаток даёт уклоны до 12.8%
на треке, где реального набора 14 м на 13 км.
Практическое следствие: **любой анализ по уклонам за май и июнь даёт
фальшивые результаты**, причём правдоподобные на вид. Построенная по этим
данным кривая «лучший удержанный момент на шатунах» показала май лучшим
месяцем сезона, хотя по мощности за 20 минут рост с мая по сентябрь
составил 1.12 -> 1.75 Вт/кг.
Отдельно найдены отказы высотомера внутри годных треков: 2 сентября
(session_id 1788361994788) после точки 4194 значение падает на 10 м
и **замерзает** -- 26.8, 26.9, 26.9, 26.9 шестьдесят точек подряд,
а после перерыва записи прыгает на 64.8 м с точностью 22 м. Уклоны
на этом участке выдуманы целиком.
## Признаки негодной высоты, годятся как автотест
Все три считаются по уже записанным данным, без сети:
1. `elevation_gain_m` в сессии и набор, пересчитанный по точкам, расходятся
больше чем вдвое в любую сторону
2. медиана модуля разности высоты между соседними точками выше 1.5 м
3. доля точек с шагом записи больше 8 секунд выше 15%
По этим признакам из 83 поездок отсеиваются 31. После уточнения высот
через SRTM проверку надо прогнать повторно: она же покажет, помогло ли.
## Объём работы, посчитан по базе
всего точек 418 465
уже уточнено 47 420 (11.3%, 12 сессий из 170)
осталось 371 045
запросов по 100 3 711
Размер пачки 100 задан в `frontend/js/main.js` около строки 3085 и совпадает
с потолком OpenTopoData.
## Что надо поправить ДО массового прогона
**Откат по цепочке источников менять не надо.** Он сделан намеренно:
лучше получить хоть какую-то высоту, чем никакой. Проблема не в откате,
а в том, что после него не остаётся следа, кто именно ответил.
**1. Нет колонки источника.** В `gps_points` есть `altitude_srtm_m`, но нет
поля, откуда это значение пришло. При работающем откате база неизбежно
становится смесью источников, и без этой колонки отличить их потом нельзя
никак. Нужна колонка (ALTER TABLE, без пересоздания), заполняемая при записи.
Это единственное обязательное изменение.
**2. Откат должен быть виден пользователю.** Сейчас он сообщается только
строкой в консоли (`[gpxz] http 403`). Достаточно показать в конце итог
вида «уточнено 2314 точек: GPXZ 0, open-elevation 0, open-meteo 2314».
**3. Обработка 429 и 402 отдельно от сетевой ошибки.** По лимиту
останавливаться до завтра, по обрыву повторять. Иначе при массовом прогоне
упёршийся лимит молча отправит весь остаток базы к запасному источнику.
## Чего ждать и чего не ждать
Copernicus GLO-30 это сетка 30 м. На подъём длиной 200-400 м приходится
7-13 отсчётов, этого хватает. На короткий рамп 60-100 м приходится 2-3
отсчёта, форму по ним не восстановить. То есть массовое уточнение закроет
затяжные подъёмы и общий набор, но НЕ закроет короткие рампы, на которых
пользователь срывается. Для них нужен либо барометр, либо разовый замер
профиля конкретного подъёма угломером.
## Лимиты GPXZ против объёма
Тарифы с их сайта: Evaluation бесплатно и 100 запросов в сутки (помечен
«testing only»), Small 99 USD/мес и 2500 в сутки, Large 249 USD/мес и 7500.
3711 запросов при 100/сутки = 37 дней
3711 запросов при 2500/сутки = 1.5 дня
Отдельно: одна поездка это медиана 19 запросов, но максимум 133. То есть
**длинная поездка не влезает в бесплатный суточный лимит даже одна.**
Если массовый прогон делать на бесплатном ключе, он обязан быть
возобновляемым и считать свой суточный расход сам.
## Вариант без лимитов: свой Open Topo Data
GPXZ на своём сайте прямо пишет, что открытый форк их API-сервера
называется Open Topo Data. Размер пачки 100, уже зашитый в коде,
взят как раз из его потолка, то есть **протокол совместим и менять
в клиенте нужен только базовый URL** (сейчас `https://api.gpxz.io/v1/...`
захардкожен в `frontend/js/main.js` около строки 3091).
Считано по базе: все треки пользователя укрываются **двумя градусными
тайлами 1x1**. Для GLO-30 это порядка сотни мегабайт на диске, разово
скачивается по проводу.
Что это даёт против платного тарифа:
- лимита нет вообще, весь бэклог прогоняется за один вечер
- в поле сеть не нужна: у пользователя её глушат, и это постоянная
проблема всех сетевых зависимостей приложения
- тот же софт и тот же набор данных, значит результаты сопоставимы
с уже уточнёнными 12 сессиями, если там был GPXZ
Минус: нужен хост, на котором это крутится, и разовая возня с датасетом.
## Но своего хоста может и не понадобиться
Автор Open Topo Data держит бесплатный публичный инстанс
`api.opentopodata.org`. Его ограничения:
100 точек на запрос -- ровно тот размер пачки, что уже в коде
1 запрос в секунду -- нужен троттлинг, сейчас пауз между пачками нет
1000 запросов в сутки -- в десять раз больше бесплатного GPXZ
весь бэклог 3711 запросов -> 3.7 суток, 62 минуты чистого времени,
то есть ~15 минут работы в день
Сравнение по срокам на тот же объём:
GPXZ Evaluation (бесплатно) 37.1 дней
Open Topo Data публичный 3.7 дней
GPXZ Small (99 USD/мес) 1.5 дня
**Ключ не нужен вообще**, значит отпадает и вопрос, укладывается ли
массовый прогон личного архива в пометку «testing only» бесплатного
тарифа GPXZ.
### Выбор датасета, и почему это не мелочь
На публичном инстансе **нет Copernicus GLO-30**, на котором работает GPXZ.
Из глобальных 30-метровых доступны `mapzen`, `aster30m`, `srtm30m`.
`eudem25m` (25 м, лучшая точность) покрывает только страны EEA, регион
пользователя в его extent не входит.
Отсюда два следствия:
1. Датасет будет **не тот**, которым уточнены нынешние 12 сессий.
Смешивать нельзя, и колонка источника из пункта 2 выше становится
не пожеланием, а обязательной: без неё база превратится в винегрет
из трёх рельефов.
2. Разумный выбор по умолчанию `srtm30m` (USGS): у `aster30m` известны
шумы и артефакты, `mapzen` это композит из нескольких источников,
то есть внутри одного датасета точность неоднородна.
Хорошая проверка после прогона: 12 уже уточнённых сессий уточнить заново
новым датасетом в отдельную колонку и сравнить набор высоты. Разойдутся
сильно - значит выбор датасета важнее, чем кажется, и это надо знать
до того, как на этих высотах строить разбор поездок.
## ПРОВЕРЕНО: в базе лежит не то, что все думали
Проверка сделана, и она важнее всего остального в этом разделе.
**Ключ GPXZ не работает и, судя по всему, не работал никогда.** Запрос
к `api.gpxz.io` возвращает HTTP 403 с телом `error code: 1010`
и заголовком `Server: cloudflare`. Ответ **одинаковый с ключом и без
ключа**, то есть запрос режет Cloudflare по подписи клиента, до всякой
проверки ключа. Ключ при этом валидный на вид: 28 символов, префикс `ak_`.
**Те 12 «уточнённых» сессий уточнены open-meteo.** Это третий, последний
запасной источник в цепочке `GPXZ -> open-elevation -> open-meteo`.
Доказательство: опорные целые значения в `altitude_srtm_m` совпадают
с ответом open-meteo точка в точку и расходятся с `srtm30m` на 2-3 м.
**Дробные значения в базе создаёт приложение, а не источник.** Записанная
последовательность `39.000, 38.857, 38.714, 38.571, 38.429, 38.286,
38.143, 38.000` это `39 - k/7`, линейная интерполяция между двумя целыми
отсчётами. Все внешние DEM (srtm30m, aster30m, mapzen, srtm90m,
open-elevation, open-meteo) отдают **целые метры**. То есть дроби
в колонке выглядят как высокая точность, а на деле это прямая, натянутая
между узлами сетки в 30-90 м.
Практическое следствие для любого будущего разбора: **нельзя считать
уклон по `altitude_srtm_m` на коротких кусках.** Интерполяция даёт
гладкий правдоподобный профиль там, где данных нет вообще.
### Что из этого следует для задачи
1. Тихий откат (пункт 1 выше) не гипотетическая опасность, а уже
случившийся факт: база заполнена последним источником в цепочке,
и об этом никто не знал.
2. Колонка источника (пункт 2) обязательна.
3. Отказ надо показывать пользователю, а не писать в консоль.
### Покупать ли GPXZ: нет
С карты покрытия на их сайте: лидар 0.5-2 м у GPXZ есть только в США,
западной и северной Европе и Новой Зеландии. Регион пользователя
в жёлтой зоне, где по их же легенде 30 м, а по их тексту база
из Copernicus 30m. То есть платный GPXZ дал бы Copernicus 30m вместо
бесплатного SRTM 30m: другая вертикальная точность, **та же
горизонтальная сетка 30 м**.
А упирается всё именно в горизонтальную: подъём длиной 60-100 м, на
котором пользователь срывается, это 2-3 отсчёта на любой тридцатиметровой
сетке. Ни один DEM его не разрешит, ни платный, ни бесплатный.
Измерено на одном подъёме (1153 м, 229 точек, первый заезд 16 сентября):
источники расходятся между собой до 2-3% уклона на сотню метров, что
того же порядка, что шум GPS. `mapzen` при этом отдаёт числа, побитово
совпадающие с `srtm30m`.
Вывод: брать бесплатный `srtm30m` с публичного Open Topo Data.