baldav/docs/session-logs/2026-09-17_repeat-intervals.md

12 KiB
Raw Permalink Blame History

Сессия 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, но эталонный проход находится сам:
    1. сетка ~10м (формула из wind_by_passes/heightmap.py);
    2. горячие ячейки = >= minPasses (3) входов (заход = смена ключа ячейки);
    3. эталон = ОДИН чистый проход: от первой горячей ячейки, добавляя новые, до первого возврата к более ранней (склейка кругов давала абсурд - см. баги);
    4. проходы = порт passes(): позиция в seq растёт от старта (допуск назад 3, гэп 25), покрытие >= 80%; старт-гвард pos<=15% не даёт спуску открыть заезд;
    5. нормировка: сравнение по L_seq / T_прохода (снимает 5% разброса длины GPS). Направление без явной проверки bearing: seq хранит первую (подъёмную) позицию ячейки, спуск роняет позицию и закрывает заезд.
  • 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) в разметке проходов - мост через короткое прерывание рядом с коридором (bridgeSec 90с, bridgeM 60м) + сохранена терпимость к нулевым пропускам (gap<=25); время простоя остаётся в длительности заезда -> честный штраф скорости. (2) Порог покрытия minFrac 0.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-индекс, допуск вбок projTolM 30м) -> прогресс в метрах. Проход = прогресс растёт от низа до >= 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. Перекрёстки отсекаются длиной (minLenM 300м), не счётчиком; обычная поездка «туда-обратно» (коммьют) не ловится, т.к. проходы считаются монотонно в ОДНУ сторону (спуск/возврат start-guard отсекает).
  • Проверка: 2-заездный -> 2×; 16 сен 4×, 11/17 по 5× (без изменений); 5 обычных поездок (включая туда-обратно) - 0 ложных. Признаки из разбора агента (возвратный участок, время, высота) не понадобились - длина+направление уже отсекают мусор. Снос вбок и дырки в цепочке уже покрыты (проекция 30м в разметке проходов; gap>5 в эталонном проходе).

Не в этой задаче

Из того же файла НЕ делалось: поправка модели п.2 (аддитивная мощность вместо max(физика,MET); п.1 про Crr уже был сделан ранее), массовое уточнение высот, ветер (Часть 2 - там вопросы).