Repeat analysis: pick the longest corridor traversal as reference (fix short segment)

This commit is contained in:
4aiman 2026-09-17 22:50:24 +03:00
commit 0d8b5f97b9
2 changed files with 44 additions and 19 deletions

View file

@ -43,6 +43,20 @@
совпадает с харнессом, вердикт зелёный 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 (аддитивная мощность вместо max(физика,MET);
п.1 про Crr уже был сделан ранее), массовое уточнение высот, ветер (Часть 2 - там вопросы).

View file

@ -130,26 +130,37 @@ GPS.detectRepeats = function(points, opts) {
}
let isHot = (k) => k != null && (visits.get(k) || 0) >= minPasses;
// Reference segment = ONE clean traversal of the corridor: start at the first hot
// cell, add each new hot cell in order, and STOP at the first revisit (that means the
// rider turned back / started the next lap). Gluing several laps into the reference
// (a long hot run) inflates its length and scrambles the order — measured, gives
// absurd 9-second "passes" over a 4.4 km "segment". One traversal fixes both.
let s0 = 0;
while (s0 < n && !isHot(key[s0])) s0++;
if (s0 >= n) return { found: false };
let seq = [], order = new Map(), s1 = s0;
for (let k = s0, gap = 0; k < n; k++) {
let c = key[k];
if (c == null || !isHot(c)) { gap++; if (gap > 5) break; continue; } // left the corridor
gap = 0;
if (seq.length && c === seq[seq.length - 1]) { s1 = k; continue; } // still in the same cell
if (order.has(c)) break; // revisit of an earlier cell → first traversal complete
order.set(c, seq.length); seq.push(c); s1 = k;
// Reference segment = the LONGEST clean traversal of the corridor. One traversal =
// start at a cell, add each NEW hot cell in order, STOP at the first revisit (turned
// back / next lap) or a gap out of the corridor. Gluing laps together inflates length
// and scrambles order (measured: absurd 9 s "passes" over 4.4 km); a single traversal
// fixes that. But picking the FIRST hot cell is also wrong — a ride can START inside a
// hot cell (its approach overlaps the corridor), giving a short gap-truncated seq
// (measured: 729 m of a 1572 m climb). So try a traversal from every hot-run start and
// keep the longest.
let buildTraversal = (start) => {
let seq = [], order = new Map(), end = start;
for (let k = start, gap = 0; k < n; k++) {
let c = key[k];
if (c == null || !isHot(c)) { gap++; if (gap > 5) break; continue; } // left the corridor
gap = 0;
if (seq.length && c === seq[seq.length - 1]) { end = k; continue; } // still in the same cell
if (order.has(c)) break; // revisit of an earlier cell → traversal complete
order.set(c, seq.length); seq.push(c); end = k;
}
let len = 0;
for (let k = start + 1; k <= end; k++)
len += GPS.distance(points[k - 1].lat, points[k - 1].lon, points[k].lat, points[k].lon);
return { seq, order, len };
};
let best = null;
for (let k = 0; k < n; k++) {
if (!isHot(key[k]) || (k > 0 && isHot(key[k - 1]))) continue; // only at a hot-run start
let t = buildTraversal(k);
if (!best || t.len > best.len) best = t;
}
let segLength = 0;
for (let k = s0 + 1; k <= s1; k++)
segLength += GPS.distance(points[k - 1].lat, points[k - 1].lon, points[k].lat, points[k].lon);
if (!best) return { found: false };
let seq = best.seq, order = best.order, segLength = best.len;
if (seq.length < 3 || segLength < minLenM) return { found: false };
// Passes: scan the ride; a pass is a run where position along seq rises from the