12 KiB
12 KiB
Сессия 2026-09-17 - автоматический разбор повторов внутри поездки (интервалы)
Контекст
Заказ из docs/gps-model-findings.md (раздел «БЭКЛОГ: автоматический разбор повторов»).
Пользователь катает интервалы: один подъём 5 раз за поездку, один трек, без стоп/старт.
Нужно: повторы находятся сами по самоподобию, таблица заездов и число «последний/первый».
Действовали по утверждённому плану ~/.claude/plans/merry-discovering-dream.md
(в docs/plans не копировал - план короткий и одноразовый; здесь ссылка).
Сделано
GPS.detectRepeats(points, opts)(gps.js) - портtools/track/pers.py, но эталонный проход находится сам:- сетка ~10м (формула из
wind_by_passes/heightmap.py); - горячие ячейки = >= minPasses (3) входов (заход = смена ключа ячейки);
- эталон = ОДИН чистый проход: от первой горячей ячейки, добавляя новые, до первого возврата к более ранней (склейка кругов давала абсурд - см. баги);
- проходы = порт
passes(): позиция в seq растёт от старта (допуск назад 3, гэп 25), покрытие >= 80%; старт-гвардpos<=15%не даёт спуску открыть заезд; - нормировка: сравнение по
L_seq / T_прохода(снимает 5% разброса длины GPS). Направление без явной проверки bearing: seq хранит первую (подъёмную) позицию ячейки, спуск роняет позицию и закрывает заезд.
- сетка ~10м (формула из
avgPowerв возвратGPS.computeStats(накоплениеpower*dtв вело-ветке / время педалирования) - для колонки «Вт».- UI: секция-аккордеон «Разбор повторов» в
showSessionDetail(только вело), таблица (№/время/км-ч норм/каденс/Вт), вердикт последний/первый (зелёный >=100%, жёлтый >=90%, красный <90%), подсветка участка на карте (бирюза#00e5ff). CSS в styles.css.
Баги по пути (и фиксы)
- Эталон как «самый длинный горячий ран» склеивал 5 кругов -> segLength 4406м и заезды по 9с (норм. скорость 1762 км/ч). Фикс: эталон = один проход до первого возврата.
- Следующая точка в ТОЙ ЖЕ ячейке считалась «возвратом» и сразу обрывала seq (seq=1).
Фикс:
c===seq[last]-> продолжить, не обрывать.
Проверка
- Node-харнесс на
tools/track/baldav.db(реальные данные):- 11 сен (3214 тчк): участок 1577м, 5 заездов, норм. скорости 15.0/17.1/17.8/17.9/17.5, последний/первый 117%.
- 17 сен (3079 тчк): участок 1630м, 5 заездов, 16.9/15.8/17.3/16.4/17.7, 105%. Совпадает с находками («11 и 17 сентября, по 5 повторов»).
- Контроль: 5 обычных вело-поездок (8-23 км) + 2 короткие -> «повторов не найдено» (0 ложных).
- Десктоп (Xvfb): временно подсажена сессия 17 сен в дев-БД (бэкап + точечное удаление после, БД байт-в-байт восстановлена). Секция рисуется, аккордеон работает, таблица 5 строк совпадает с харнессом, вердикт зелёный 105%, участок подсвечен бирюзой на карте. Скрин.
- Далее: сборка APK + проверка на телефоне (реальные интервалы там же).
Фикс после проверки на теле (трек 16 сен)
- Симптом (пользователь): на 16 сен в повторы попала только часть горки, 729м вместо 1200+, на карте видно прямую, что идёт дальше.
- Причина: эталон брался с ПЕРВОЙ горячей ячейки. Поездка 16 сен стартует прямо в горячей ячейке (подъезд частично совпал с коридором), проход с индекса 0 обрывался гэпом на 143й точке -> 729м, хотя горячих ячеек ~183 (~1830м).
- Фикс: эталон = САМЫЙ ДЛИННЫЙ чистый проход (перебор всех стартов горячих ранов,
buildTraversal), не первый. Дёшево (стартов немного). - Проверка: харнесс 16 сен -> участок 1572м (было 729), 3 полных заезда. 11/17 сен без изменений (по 5). Контроль (5 обычных) - 0 ложных. Десктоп (Xvfb, подсажена 16 сен, бэкап+удаление): подсветка на карте покрывает весь верхний коридор, таблица 1.57км/3 заезда, вердикт 104%. По коридору 4 траверса, но первый 74% (начат выше низа) - отсечён правилом 80% (как pers.py MIN_FRAC), иначе нормировка длина/время завысила бы его скорость.
Фикс 2: остановка внутри заезда (седло) + сшивка (трек 16 сен)
- Пользователь: первый заезд 16 сен был полным, но разорвался остановкой (опущенное седло, ноги не крутили) - такие стопы <~минуты сшивать в один заезд, время стопа оставлять в заезде как штраф.
- Проверка данных: остановка ЕСТЬ - idx 310-319, 89 секунд сдвиг <15м. По reported speed её не видно (fused показывал 8 км/ч), нашлась по СМЕЩЕНИЮ. Плюс у вершины трек первого заезда уходит с общей линии в негорячие ячейки (GPS-разброс на 20 км/ч), из-за чего покрытие вышло 74%.
- Сделано: (1) в разметке проходов - мост через короткое прерывание рядом с коридором
(
bridgeSec90с,bridgeM60м) + сохранена терпимость к нулевым пропускам (gap<=25); время простоя остаётся в длительности заезда -> честный штраф скорости. (2) Порог покрытияminFrac0.8 -> 0.7: полный заезд, у которого GPS сполз с линии у вершины, засчитывается. - Проверка (харнесс): 16 сен -> 4 заезда (был 3), норм.скор [19.6, 21.3, 21.2, 23.8] - первый самый медленный (штраф за 89с стоп), вердикт 121%. 11/17 сен без изменений (по 5), контроль 5 обычных - 0 ложных при 0.7. Десктоп: 16 сен подсажен, таблица 4 строки.
Фикс 3: проекция на опорную линию + время только в движении
- Пользователь: снос вбок не случайность (седло падало, подкручивал) - проецировать на опорную линию, чтобы боковой снос не резал покрытие; остановки на краях заездов не считать во время.
- Сделано: детект проходов переписан с «точного совпадения ячеек» на ПРОЕКЦИЮ. Опорная линия =
точки лучшего прохода с накопленной дистанцией; каждая точка поездки проецируется на неё
(ближайшая вершина через cell-индекс, допуск вбок
projTolM30м) -> прогресс в метрах. Проход = прогресс растёт от низа до >= minFrac. Снос на 30м больше не роняет покрытие, поэтомуminFracвернул к строгому 0.8 (как pers.py). - Время заезда = только СЕКУНДЫ В ДВИЖЕНИИ (
movingSec, по фактическому смещению >=0.8 м/с), стопы на краях и в середине вырезаются. Нормировка норм.скор = длина/движ.время. - Проверка (харнесс, minFrac 0.8): 16 сен -> 4 заезда, движ.время [281,268,271,252]с (89с стоп вырезан из первого), норм.скор [20.1,21.1,20.9,22.5], посл/перв 112%. 11 сен 5× (16.6..18.8, 113%), 17 сен 5× (101%), контроль 5 обычных - 0 ложных, короткие - нет.
Фикс 4: детекция при 2 заездах (порог 3 -> 2)
- Симптом (пользователь + разбор другого агента): при двух проездах туда-обратно детекции нет по построению.
- Проверка по коду + харнесс:
minPasses(по умолч. 3) работал И как порог «горячей» ячейки (>=3 входов), И как финальный порог числа проходов. Урезанный до 2 заездов трек 16 сен: порог 3 -> НЕ НАЙДЕНО, порог 2 -> найдено (2 заезда, 1555м). Подтверждено. - Фикс:
minPassesпо умолчанию 2. Перекрёстки отсекаются длиной (minLenM300м), не счётчиком; обычная поездка «туда-обратно» (коммьют) не ловится, т.к. проходы считаются монотонно в ОДНУ сторону (спуск/возврат start-guard отсекает). - Проверка: 2-заездный -> 2×; 16 сен 4×, 11/17 по 5× (без изменений); 5 обычных поездок (включая туда-обратно) - 0 ложных. Признаки из разбора агента (возвратный участок, время, высота) не понадобились - длина+направление уже отсекают мусор. Снос вбок и дырки в цепочке уже покрыты (проекция 30м в разметке проходов; gap>5 в эталонном проходе).
Не в этой задаче
Из того же файла НЕ делалось: поправка модели п.2 (аддитивная мощность вместо max(физика,MET); п.1 про Crr уже был сделан ранее), массовое уточнение высот, ветер (Часть 2 - там вопросы).