Compare commits

...

7 commits

Author SHA1 Message Date
Andrew Pimenov
2604cfc302 Revert "switch license to PolyForm Noncommercial 1.0.0"
This reverts commit cbc88abe5a.
2026-08-07 18:52:55 +03:00
Andrew Pimenov
cbc88abe5a switch license to PolyForm Noncommercial 1.0.0 2026-08-07 17:51:54 +03:00
Andrew Pimenov
15bb552166 rename: sort by game — races/F1 22|ACC/<track>/<session>/
read_metadata now returns game_raw; _game_folder maps it to 'F1 22' or 'ACC';
dest_dir gains a game subfolder between races_dir and track
2026-06-19 16:40:02 +03:00
Andrew Pimenov
c1ee2f82e4 add ACC (GT3/GT4) support
- game_of() + load_prompt() with acc→f1 fallback in report_common
- technique/setup/race: skip ERS/setup sections for ACC; compare: routes prompt
- --game {auto,f1,acc} CLI flag
- 8 ACC prompt files (ru/en × 4 modes)
- GUI: ttk.Notebook F1 22/ACC tabs, isolated state, game-mismatch warning
- rename: normalise ACC full-word events to compact tokens (Practice→P, Qualifying→Q, Race→R, Hotlap→HL, Hotstint→HS)
- tests: ACC fixture + 9 new tests (87 total); all F1 regressions green
- docs: README + ARCHITECTURE bilingual updates
2026-06-19 16:29:52 +03:00
Andrew Pimenov
22ca7cc24f document profile mode, new flags, and tunable constants 2026-06-18 03:47:42 +03:00
Andrew Pimenov
0661ff4322 add profile mode, optional prompt, report cache, compare verdict
profile: cheap cross-lap corner tendencies + auto-picked laps, per tyre compound
--no-prompt flag and GUI checkbox omit the embedded prompt; --force ignores the cache
compare: deterministic fuel-corrected verdict section (fuel as a range, not a point)
corner merge gap now scales with track length; race dedup keeps laps with no Lap field
2026-06-18 03:47:42 +03:00
Andrew Pimenov
917a66c224 update docs: recursive rename with folder sorting
README: explain rename behavior with races_dir (recursion, auto-sort,
move misplaced files, skip in-place files)

ARCHITECTURE: update rename.py description and gotchas section to
document races_dir parameter, os.walk, and file movement logic
2026-06-18 02:17:39 +03:00
26 changed files with 1811 additions and 229 deletions

View file

@ -2,7 +2,7 @@
# Architecture
A telemetry CSV becomes a Markdown report through a small linear pipeline. Everything is stdlib Python; there is no framework and no global state.
A telemetry CSV (F1 22 or ACC) becomes a Markdown report through a small linear pipeline. Everything is stdlib Python; there is no framework and no global state.
## Files
@ -13,14 +13,16 @@ telemetry/
segments.py Lap -> corners & straights. Zone detection + numbering.
corner_metrics.py Per-zone number crunching: braking point, lock/spin, coast, temps.
resample.py Adaptive distance grid + channel interpolation for the trace.
report_common.py Header block, legend strings, md_table, token_estimate, load_prompt, atomic write.
report_common.py Header block, legend strings, md_table, token_estimate, game_of, load_prompt, atomic write.
report_technique.py Driving report (corners, straights, cost, trace).
report_setup.py Car report (setup, balance, tyres, brakes, suspension, phases).
report_compare.py Two+ laps side by side, per-corner and cumulative delta.
report_race.py A folder of laps: lap times (with position), stints, wear, thermals, ERS.
rename.py Insert session type + lap number (e.g. P1_L7) into filenames; skip processed.
gui.py tkinter window; per-mode browse memory; runs generation/rename on a worker thread.
report_race.py A folder of laps: lap times (with position), stints, wear, thermals, ERS. Dedups duplicate lap numbers.
report_profile.py A folder of laps: cheap cross-lap corner tendencies (median per corner, per dominant compound) + auto-picked deep-dive laps. No trace.
rename.py Insert session type + lap number (P1_L7) into filenames; with races_dir, sort into races/<game>/<track>/<session>/ and recurse subdirectories.
gui.py tkinter window with two tabs (F1 22 / ACC); per-mode browse memory; runs generation/rename on a worker thread. Warns (non-blocking) when the detected game doesn't match the active tab.
prompts/ LLM analysis prompts, ru/ and en/ subdirectories, one file per mode. Auto-appended to every report.
ACC-specific overrides live in prompts/<lang>/acc/<mode>.md; missing files fall back to the F1 prompt.
tests/ pytest suite + fixture laps.
```
@ -30,7 +32,7 @@ tests/ pytest suite + fixture laps.
2. **`parser.load_lap`** reads the file as UTF-8 and splits it into four meta blocks (session, track, setup) plus the 108-channel telemetry table. Channel names are normalised by stripping the ` [unit]` suffix. Every column that is entirely zero is dropped — the game writes many dead channels (see Gotchas).
3. **`segments.detect_corners`** walks the samples, marks each as "cornering" when `|Steer| > STEER_ON` or `Brake > BRAKE_ON`, merges adjacent zones closer than `MERGE_GAP_M`, drops zones shorter than `MIN_LEN_M`, and numbers what's left T1..Tn by distance. For each zone it calls **`corner_metrics.corner_from_zone`**, which extracts the braking point, apex, gear, full-throttle point, peak brake/gLat, relative lock%/spin%, coast distance, and rear tyre temperature peak. `straights` fills the gaps between corners.
4. **`resample.adaptive_points`** builds a distance grid that is dense inside (and on approach to) corners and sparse on straights — the step sizes default to the lap's own median sample spacing via `auto_steps`, so denser raw telemetry yields a denser trace. **`sample_at`** interpolates each channel onto that grid (nearest-sample for discrete channels like Gear/DRS).
5. The **`report_<mode>.generate(…, lang)`** function assembles Markdown from `report_common` helpers (`header_block`, `md_table`, `legend`) and the data above. It then calls **`load_prompt(mode, lang)`** to read the matching prompt from `prompts/<lang>/<mode>.md` and appends it after a `---` separator. Finally **`write_report`** writes atomically (temp file + `os.replace`) and returns `(abs_path, tokens)`.
5. The **`report_<mode>.generate(…, lang, include_prompt=True)`** function assembles Markdown from `report_common` helpers (`header_block`, `md_table`, `legend`) and the data above. **`game_of(lap)`** inspects the `Game` session field and returns `'acc'` (if the field starts with `"ACC"`) or `'f1'`; the report generators use this to suppress DRS/ERS/hybrid sections for ACC and to replace the setup parameter table with a note when all setup values are zero (as ACC always exports). When `include_prompt` is set it calls **`load_prompt(mode, lang, game)`** which first tries `prompts/<lang>/acc/<mode>.md` for ACC and falls back to `prompts/<lang>/<mode>.md` if absent, then appends the result after a `---` separator (CLI `--no-prompt` / GUI checkbox turn this off). Finally **`write_report`** writes atomically (temp file + `os.replace`) and returns `(abs_path, tokens)`. Before generating, `__main__` skips the run via **`is_fresh(report, sources)`** when an up-to-date report exists (override with `--force`).
6. **`__main__`** prints that path and `~N tokens (budget 60k)`. The **GUI** shows the same tuple in its log instead of printing.
## Adding things
@ -41,6 +43,23 @@ tests/ pytest suite + fixture laps.
**A new corner metric.** Add a field to the `Corner` dataclass in `segments.py` and compute it in `corner_metrics.corner_from_zone`. Because `Corner` is frozen, set it through the constructor.
## Assumptions & tunables
Two kinds of constants. **Detection** thresholds are in telemetry units and uniform across F1 22 — they decide *where* a boundary is, not the conclusion, so they are not track-tuned. **Interpretation** constants encode racing judgment that genuinely varies; reports always print the raw number next to any flag/verdict, so a debatable threshold never hides the data.
| Constant | File | Kind | Basis / sensitivity |
|----------|------|------|---------------------|
| `STEER_ON 6`, `BRAKE_ON 5`, `BRAKE_MARK 20`, `FULL_THROTTLE 98` | `segments` / `corner_metrics` | detection | percent of input range, game-uniform |
| `MERGE_GAP_M 40` + `merge_gap_for` (clamp 28–60) | `segments` | detection | **adaptive**: scales with `Tracklen` around the Zandvoort anchor so chicanes/long circuits self-tune |
| `MIN_LEN_M 15`, `_SAFE_SPEED_FLOOR 30`, `0.3 s` throttle window | `segments` / `corner_metrics` | detection | geometry / divide guard / debounce |
| `resample` dense=median, sparse=8×median | `resample` | detection | self-tunes to sample rate |
| `LOCKUP −10 %`, `SPIN +8 %` | `segments` | interpretation | severe-only flags; raw `lock%`/`spin%` always shown |
| `FUEL 0.03–0.05 s/kg` | `report_compare` | interpretation | track-varying; emitted as a **range** with the rate printed |
| `diff ±0.5` US/OS | `report_setup` | interpretation | relative to the lap's slip baseline; raw front/rear slip shown |
| profile `*_REL` multipliers + `*_FLOOR` / `TR` margins | `report_profile` | interpretation | multipliers are **relative to the session median**; floors are noise gates; medians shown in the table |
If a future track needs a different corner grouping, `merge_gap_for` already adapts by length; only a genuinely unusual layout would want a manual nudge there.
## Gotchas
**Files are UTF-8, the OS default here is cp1251.** `parser.load_lap` always passes `encoding='utf-8'`; opening a telemetry file with the default codec raises `UnicodeDecodeError` on the first non-ASCII byte. `rename.read_lap_number` reads only the first five lines but must use the same encoding.
@ -53,6 +72,12 @@ tests/ pytest suite + fixture laps.
**The setup row has a trailing comma** (an empty `FuelLoad` tail field in some exports). The parser drops trailing empties so the setup dict has no blank-string key.
**Rename idempotency hinges on TWO tokens.** A file is considered processed only when both a session token (`P1`, `Q2`, `R`, …, matched by `_SESSION_PAT`) and a lap token (`L<digits>`) are present in the stem. Both `session_token_present` and `lap_token_present` are the joint source of truth — changing either token format means updating both the insert and the presence-check functions together.
**Rename with `races_dir` sorts into folders and walks subdirectories.** `rename_unprocessed(targets, races_dir)` when `races_dir` is set: (1) recursively scans all subdirectories via `os.walk`, (2) reads metadata from each CSV (`read_metadata` → lap, event, track, game), (3) inserts tokens if absent, (4) creates `races/F1 22|ACC/<track_safe>/Practice|Qualifying|Race|Sprint/` and moves the file there, (5) even already-renamed files are moved if they're in the wrong folder, (6) only skips files that are already in the right place with the right name. Without `races_dir` it works as before — renames in-place in the original folder.
**Reports go next to the input, not next to the program.** `report_path` derives the `reports/` directory from the *input CSV's* folder. `race` mode puts the report inside the race session folder (`<race_dir>/reports/`). Moving the tool does not move where reports land.
**Game is derived from the `Game` metadata field and threaded through report generation.** `game_of(lap)` reads `lap.session['Game']`; the field starts with `"ACC"` for Assetto Corsa Competizione and is something else for F1 22. Every report generator receives the lap and calls `game_of` to decide which sections to emit. The CLI `--game` flag can override detection by patching the session field before dispatch.
**ACC setup is all zeros and is intentionally not rendered as a parameter table.** ACC's CSV export always writes zeros for every setup field. `report_setup` checks whether all setup values are zero and, when true, replaces the parameter table with a single explanatory note rather than printing a table of meaningless zeros.
**ACC session events use full words; F1 22 uses short codes.** F1 22 writes session codes like `P1`, `Q1`, `R` directly. ACC writes `Practice`, `Qualifying`, `Race`. The rename module normalises ACC full-word events to compact canonical codes (Practice→P, Qualifying→Q, Race→R) before inserting them into the filename and choosing the destination folder. The inserted token is the same compact code that the session-pattern recogniser expects, so re-running rename on already-processed ACC files is idempotent (they are skipped, not double-tagged).

View file

@ -2,7 +2,7 @@
# Архитектура
CSV-телеметрия превращается в Markdown-отчёт через короткий линейный конвейер. Всё на стандартной библиотеке Python; ни фреймворка, ни глобального состояния.
CSV-телеметрия (F1 22 или ACC) превращается в Markdown-отчёт через короткий линейный конвейер. Всё на стандартной библиотеке Python; ни фреймворка, ни глобального состояния.
## Файлы
@ -13,14 +13,16 @@ telemetry/
segments.py Lap -> повороты и прямые. Детекция зон + нумерация.
corner_metrics.py Подсчёт по зоне: точка торможения, lock/spin, накат, температуры.
resample.py Адаптивная сетка по дистанции + интерполяция каналов для трассы.
report_common.py Шапка, строки легенды, md_table, token_estimate, load_prompt, атомарная запись.
report_common.py Шапка, строки легенды, md_table, token_estimate, game_of, load_prompt, атомарная запись.
report_technique.py Отчёт о пилотировании (повороты, прямые, расход, трасса).
report_setup.py Отчёт о машине (сетап, баланс, шины, тормоза, подвеска, фазы).
report_compare.py Два+ круга рядом, дельта по поворотам и накопленная.
report_race.py Папка кругов: лаптаймы (с позицией), стинты, износ, тепло, ERS.
rename.py Вставка типа сессии + номера круга (P1_L7); пропуск обработанных.
gui.py Окно tkinter; память последней папки по режиму; генерация/переименование в потоке.
report_race.py Папка кругов: лаптаймы (с позицией), стинты, износ, тепло, ERS. Дедуп дублей номеров кругов.
report_profile.py Папка кругов: дешёвые тенденции по поворотам (медиана на поворот, по доминирующему составу) + авто-выбор кругов для разбора. Без трассы.
rename.py Вставка типа сессии + номера круга (P1_L7) в имя; с races_dir — сортировка в races/<game>/<track>/<session>/ и рекурсия по подпапкам.
gui.py Окно tkinter с двумя вкладками (F1 22 / ACC); память последней папки по режиму; генерация/переименование в потоке. Предупреждает (без блокировки), если определённая игра не совпадает с активной вкладкой.
prompts/ Промпты LLM, подпапки ru/ и en/, по одному файлу на режим. Добавляются в конец каждого отчёта.
Переопределения для ACC лежат в prompts/<lang>/acc/<mode>.md; при отсутствии — откат к F1-промпту.
tests/ Набор pytest + фикстуры-круги.
```
@ -30,7 +32,7 @@ tests/ Набор pytest + фикстуры-круги.
2. **`parser.load_lap`** читает файл как UTF-8 и делит на четыре мета-блока (session, track, setup) плюс таблицу телеметрии на 108 каналов. Имена каналов нормализуются срезанием суффикса ` [unit]`. Каждая колонка, целиком состоящая из нулей, отбрасывается — игра пишет много мёртвых каналов (см. «Подводные камни»).
3. **`segments.detect_corners`** проходит по сэмплам, помечает сэмпл как «в повороте», если `|Steer| > STEER_ON` или `Brake > BRAKE_ON`, сливает соседние зоны с зазором меньше `MERGE_GAP_M`, отбрасывает зоны короче `MIN_LEN_M` и нумерует оставшиеся T1..Tn по дистанции. Для каждой зоны вызывается **`corner_metrics.corner_from_zone`**, которая извлекает точку торможения, апекс, передачу, точку полного газа, пик тормоза/gLat, относительные lock%/spin%, дистанцию наката и пик температуры задней шины. `straights` заполняет промежутки между поворотами.
4. **`resample.adaptive_points`** строит сетку по дистанции — плотную внутри поворотов (и на подходе) и редкую на прямых; шаги по умолчанию берутся из медианного интервала сэмплов круга через `auto_steps`, так что более плотная сырая телеметрия даёт более плотную трассу. **`sample_at`** интерполирует каждый канал на эту сетку (ближайший сэмпл для дискретных вроде Gear/DRS).
5. Функция **`report_<режим>.generate(…, lang)`** собирает Markdown из помощников `report_common` (`header_block`, `md_table`, `legend`) и данных выше. Затем вызывает **`load_prompt(mode, lang)`**, которая читает промпт из `prompts/<lang>/<mode>.md` и добавляет его после разделителя `---`. Финальный текст пишется через **`write_report`** атомарно (временный файл + `os.replace`), возвращается `(abs_path, tokens)`.
5. Функция **`report_<режим>.generate(…, lang, include_prompt=True)`** собирает Markdown из помощников `report_common` (`header_block`, `md_table`, `legend`) и данных выше. **`game_of(lap)`** читает поле `Game` из сессии и возвращает `'acc'` (если значение начинается с `"ACC"`) или `'f1'`; генераторы отчётов используют это, чтобы убрать разделы DRS/ERS/гибрид для ACC и заменить таблицу сетапа пометкой, когда все значения нулевые (ACC всегда так экспортирует). При `include_prompt` вызывает **`load_prompt(mode, lang, game)`**, которая для ACC сначала ищет `prompts/<lang>/acc/<mode>.md` и откатывается к `prompts/<lang>/<mode>.md` при его отсутствии, затем добавляет результат после разделителя `---` (CLI `--no-prompt` / чекбокс GUI это отключают). Финальный текст пишется через **`write_report`** атомарно (временный файл + `os.replace`), возвращается `(abs_path, tokens)`. Перед генерацией `__main__` пропускает прогон через **`is_fresh(report, sources)`**, если свежий отчёт уже есть (override — `--force`).
6. **`__main__`** печатает путь и `~N tokens (budget 60k)`. **GUI** показывает тот же кортеж в логе вместо печати.
## Как добавлять
@ -41,6 +43,23 @@ tests/ Набор pytest + фикстуры-круги.
**Новая метрика поворота.** Добавь поле в dataclass `Corner` в `segments.py` и посчитай его в `corner_metrics.corner_from_zone`. Поскольку `Corner` — frozen, задавай через конструктор.
## Допущения и тюнящиеся константы
Два рода констант. **Детекция** — пороги в единицах телеметрии, одинаковы во всей F1 22: решают, *где* граница, а не вывод, поэтому не тюнингуются под трассу. **Интерпретация** — доменное суждение, которое реально варьируется; в отчётах рядом с любым флагом/вердиктом всегда печатается сырое число, так что спорный порог не прячет данные.
| Константа | Файл | Род | Основа / чувствительность |
|-----------|------|-----|---------------------------|
| `STEER_ON 6`, `BRAKE_ON 5`, `BRAKE_MARK 20`, `FULL_THROTTLE 98` | `segments` / `corner_metrics` | детекция | % от диапазона ввода, одинаково в игре |
| `MERGE_GAP_M 40` + `merge_gap_for` (clamp 28–60) | `segments` | детекция | **адаптивно**: масштабируется по `Tracklen` вокруг якоря Zandvoort — шиканы/длинные трассы самотюнятся |
| `MIN_LEN_M 15`, `_SAFE_SPEED_FLOOR 30`, окно газа `0.3 с` | `segments` / `corner_metrics` | детекция | геометрия / защита деления / дебаунс |
| `resample` dense=median, sparse=8×median | `resample` | детекция | самонастройка под частоту данных |
| `LOCKUP −10 %`, `SPIN +8 %` | `segments` | интерпретация | флаги severe-only; сырые `lock%`/`spin%` всегда показаны |
| `FUEL 0.03–0.05 с/кг` | `report_compare` | интерпретация | зависит от трассы; выдаётся **диапазоном** с печатью ставки |
| `diff ±0.5` US/OS | `report_setup` | интерпретация | относительно baseline скольжения круга; сырое front/rear показано |
| profile `*_REL` множители + `*_FLOOR` / `TR` поля | `report_profile` | интерпретация | множители **относительно медианы сессии**; полы — шумовые гейты; медианы в таблице |
Если будущая трасса потребует другой группировки поворотов — `merge_gap_for` уже адаптируется по длине; ручная правка нужна лишь на совсем нестандартной конфигурации.
## Подводные камни
**Файлы в UTF-8, а кодировка ОС здесь — cp1251.** `parser.load_lap` всегда передаёт `encoding='utf-8'`; открытие файла телеметрии дефолтным кодеком падает с `UnicodeDecodeError` на первом не-ASCII байте. `rename.read_lap_number` читает только первые пять строк, но обязан использовать ту же кодировку.
@ -53,6 +72,12 @@ tests/ Набор pytest + фикстуры-круги.
**Строка сетапа заканчивается лишней запятой** (пустой хвост `FuelLoad` в части экспортов). Парсер отбрасывает пустые хвостовые поля, чтобы в словаре сетапа не было ключа с пустой строкой.
**Идемпотентность rename держится на ДВУХ токенах.** Файл считается обработанным только при наличии обоих: токена типа сессии (`P1`, `Q2`, `R`, … — совпадение по `_SESSION_PAT`) и токена `L<цифры>`. `session_token_present` и `lap_token_present` — совместный источник истины. Смена формата любого токена требует обновить и функцию вставки, и функцию проверки одновременно.
**Rename с `races_dir` распределяет по папкам и рекурсит.** `rename_unprocessed(targets, races_dir)` когда `races_dir` установлен: (1) сканирует всё дерево подпапок (`os.walk`), (2) читает метаданные из CSV (`read_metadata` → lap, event, track, game), (3) вставляет токены если их нет, (4) создаёт `races/F1 22|ACC/<track_safe>/Practice|Qualifying|Race|Sprint/` и перемещает туда файл, (5) даже уже переименованные файлы переносятся если лежат не там где надо, (6) пропускаются только файлы, которые уже находятся в нужной папке с нужным именем. Без `races_dir` работает как раньше — переименовывает в исходной папке.
**Отчёты выходят рядом с входом, а не рядом с программой.** `report_path` выводит папку `reports/` из папки *входного CSV*. Режим `race` кладёт отчёт внутрь папки сессии (`<race_dir>/reports/`). Перемещение инструмента не меняет, куда попадают отчёты.
**Игра определяется из поля `Game` в метаданных и передаётся сквозь всю генерацию отчёта.** `game_of(lap)` читает `lap.session['Game']`; для Assetto Corsa Competizione поле начинается с `"ACC"`, для F1 22 — иначе. Каждый генератор отчёта вызывает `game_of`, чтобы решить, какие секции выводить. Флаг CLI `--game` позволяет переопределить определение, патча поле сессии до диспатча.
**Сетап ACC — всегда нули, и таблица параметров намеренно не рендерится.** CSV-экспорт ACC записывает нули во все поля сетапа. `report_setup` проверяет, все ли значения нулевые, и, если да, заменяет таблицу параметров поясняющей пометкой вместо вывода бессмысленных нулей.
**События сессии в ACC — полные слова; в F1 22 — короткие коды.** F1 22 пишет коды вроде `P1`, `Q1`, `R`. ACC пишет `Practice`, `Qualifying`, `Race`. Модуль переименования нормализует полные слова ACC до компактных канонических кодов (Practice→P, Qualifying→Q, Race→R) перед вставкой в имя файла и выбором папки назначения. Вставляемый токен совпадает с тем, что ожидает распознаватель паттерна сессии, поэтому повторный запуск rename на уже обработанных файлах ACC идемпотентен (они пропускаются, а не получают двойной тег).

View file

@ -1,8 +1,8 @@
[Русский](README.ru.md)
# F1 Telemetry Analyzer
# F1 22 / ACC Telemetry Analyzer
Turns raw F1 22 lap telemetry into compact Markdown reports you can paste into an LLM for analysis. A full raw lap is ~1M tokens; these reports are 1–35k.
Turns raw F1 22 and Assetto Corsa Competizione lap telemetry into compact Markdown reports you can paste into an LLM for analysis. A full raw lap is ~1M tokens; these reports are 1–35k.
```bash
# from the tool/ folder
@ -21,23 +21,37 @@ Download or clone the repository. Keep your telemetry CSVs in the parent folder
## What it does
Five modes. Four produce reports, one renames files.
Six modes. Five produce reports, one renames files. Both F1 22 and ACC (GT3/GT4) CSVs are supported — the game is detected automatically from each file's metadata.
| Mode | Input | Use it to analyse |
|------|-------|-------------------|
| `technique` | one lap | **how** you drive — braking points, apex speeds, throttle application, lockups, resource cost |
| `setup` | one lap | **what** is set — balance (US/OS), tyre temps, brakes, suspension rake & roll |
| `compare` | two+ laps | **where** one lap is faster — per-corner and cumulative delta |
| `race` | a folder of laps | the whole race — pace, stint degradation, tyre wear, ERS per lap |
| `compare` | two+ laps | **where** one lap is faster — per-corner and cumulative delta, plus a deterministic `Verdict` (fuel-corrected) |
| `race` | a folder of laps | the whole race — pace, stint degradation, tyre wear, ERS per lap (F1) |
| `profile` | a folder of laps | **cheap** (~400 tokens) cross-lap driver tendencies per tyre compound (median coast/lock/spin per corner, flagged relative to the session) + auto-picked laps to deep-dive. Track-agnostic: corners are clustered by apex distance, not index |
| `rename` | files/folder | insert session type and lap number into each filename (`..._74.074_...` → `..._P1_L7_74.074_...`) |
Reports include a legend explaining every column and have the analysis prompt auto-appended at the end — just drop the file into an LLM. Prompts come in RU and EN; pick with `--lang en` on the CLI or the language radio in the GUI.
**Flags:** `--no-prompt` omits the embedded analysis prompt (smaller files for agent pipelines; in the GUI it's the **Include prompt** checkbox). `--force` regenerates even when an up-to-date report already exists (runs are otherwise skipped as `cached`). `--game {auto,f1,acc}` overrides the auto-detected game (default `auto`).
> Typical optimized flow: run `profile` first to see chronic tendencies and which laps matter, then generate `technique`/`setup`/`compare` only for those laps with `--no-prompt`.
### ACC vs F1 22 differences
GT3/GT4 cars have no hybrid system, and ACC does not export the car setup:
- `technique` and `race` reports omit the DRS / ERS / MGU-K sections.
- `setup` mode: ACC's in-game setup export is all zeros, so the parameter table is replaced by a note ("ACC does not export setup — analysis derived from telemetry"); the telemetry-derived sections (corner balance, tyres, brakes, suspension) are retained.
- Fuel is in **litres** (GT) rather than kg (F1). Tyres are Pirelli dry/wet compounds, not F1 C1–C5.
### Folder layout
```
Telemetry/
races/
F1 22/
Zandvoort/
Practice/ <- CSVs go here after renaming
zandvoort_P1_L7_74.074_ferrari.csv
@ -49,6 +63,11 @@ Telemetry/
zandvoort_R_L7_74.152_ferrari.csv
reports/
Race_race.md <- race mode report
ACC/
Hungaroring/
Practice/
hungaroring_P_L1_123.702_ktm_gt4.csv
reports/
tool/ <- this repository
telemetry/
prompts/
@ -57,7 +76,7 @@ Telemetry/
## Handy things
**GUI** — double-click `run-gui.bat` (or `run-gui.pyw` for no console window, or run `python -m telemetry gui`). Pick a mode and language (RU/EN), browse to the file/folder, click Generate. The file browser remembers the last used directory per mode. The **Rename unprocessed** button adds the session type and lap number to files that don't have them yet and leaves the rest alone.
**GUI** — double-click `run-gui.bat` (or `run-gui.pyw` for no console window, or run `python -m telemetry gui`). The window has two tabs — **F1 22** and **ACC** — each with its own independent controls and log. Pick a mode and language (RU/EN), browse to the file/folder, click Generate. The file browser remembers the last used directory per mode. The **Rename unprocessed** button adds the session type and lap number to files that don't have them yet and leaves the rest alone. If a file's detected game doesn't match the active tab, the GUI logs a warning but proceeds.
**CLI** — from the `tool/` folder:
@ -66,10 +85,18 @@ python -m telemetry technique races/Zandvoort/Practice/lap.csv
python -m telemetry setup races/Zandvoort/Practice/lap.csv
python -m telemetry compare races/Zandvoort/Practice/lap_a.csv races/Zandvoort/Practice/lap_b.csv
python -m telemetry race races/Zandvoort/Race
python -m telemetry profile races/Zandvoort/Race # cheap cross-lap tendencies + lap picks
python -m telemetry rename races/Zandvoort/Practice # or individual files
# choose prompt language (default: ru)
python -m telemetry technique races/Zandvoort/Practice/lap.csv --lang en
# override game detection (default: auto)
python -m telemetry technique ACC/races/Hungaroring/Practice/lap.csv --game acc
# agent pipeline: drop the prompt, reuse fresh reports
python -m telemetry technique races/Zandvoort/Practice/lap.csv --no-prompt
python -m telemetry technique races/Zandvoort/Practice/lap.csv --force # ignore cache
```
Every report run prints the output path and a token estimate:
@ -79,11 +106,19 @@ C:\...\Telemetry\reports\lap_technique.md
~33834 tokens (budget 60k)
```
**Rename only touches unprocessed files.** A file is "processed" once its name contains both a session token (`P1`, `Q2`, `R`, …) and an `L<number>` token. Re-running rename on the same folder is safe.
**Rename inserts tokens AND sorts into folders.** Point it at a folder containing CSVs (e.g., `D:\Games\Telemetry\Data\lapdata\f1_2022`) — the script:
- Recursively scans all subdirectories
- Reads metadata from each CSV (track, session type)
- Inserts `P1_L7` into filenames that don't have both tokens yet
- Moves files to `races/<track>/Practice|Qualifying|Race/`
- Even already-renamed files are moved if they're not in the right folder
- Only skips files that are already in the correct location with the correct name
Re-running on the same folder is safe — in-place files are left alone.
## Input format
F1 22 UDP telemetry exported to CSV (tested with [F1Laps](https://www.f1laps.com)). UTF-8, ~1800 rows per lap (≈29 Hz), 108 channels. One file = one lap. For `race` mode, drop all of a race's lap files into one folder.
F1 22 UDP telemetry or ACC telemetry, both exported to CSV (F1 22 tested with [F1Laps](https://www.f1laps.com)). UTF-8, 108 channels, one file = one lap (F1 22 is ≈1800 rows/lap at ≈29 Hz; ACC laps are denser, ~14k rows — reports stay compact via adaptive resampling). For `race` mode, drop all of a race's lap files into one folder. The CSV format is identical between F1 22 and ACC; the game is detected from the `Game` metadata field in each file.
## For developers
@ -99,7 +134,7 @@ Adding a mode: drop a `telemetry/report_<mode>.py` exposing `generate(...) -> (a
## Credits
Built for analysing personal F1 22 hotlaps. Telemetry export format from [F1Laps](https://www.f1laps.com).
Built for analysing personal F1 22 and ACC hotlaps. Telemetry export format from [F1Laps](https://www.f1laps.com). In-game telemetry captured with Telemetry Tool by Iasione.
## License

View file

@ -1,8 +1,8 @@
[English](README.md)
# F1 Telemetry Analyzer
# F1 22 / ACC Telemetry Analyzer
Превращает сырую телеметрию круга F1 22 в компактные Markdown-отчёты для анализа через LLM. Сырой круг — около 1М токенов; отчёты — 1–35k.
Превращает сырую телеметрию круга F1 22 и Assetto Corsa Competizione в компактные Markdown-отчёты для анализа через LLM. Сырой круг — около 1М токенов; отчёты — 1–35k.
```bash
# из папки tool/
@ -21,34 +21,53 @@ python -m telemetry technique "races/Zandvoort/Практика/zandvoort_P1_L7_
## Что умеет
Пять режимов. Четыре строят отчёты, один переименовывает файлы.
Шесть режимов. Пять строят отчёты, один переименовывает файлы. Поддерживаются CSV из F1 22 и ACC (GT3/GT4) — игра определяется автоматически из метаданных каждого файла.
| Режим | Вход | Для анализа |
|-------|------|-------------|
| `technique` | один круг | **как** едешь — точки торможения, скорости в апексах, работа газом, блокировки, расход ресурсов |
| `setup` | один круг | **что** настроено — баланс (US/OS), температуры шин, тормоза, rake и крен подвески |
| `compare` | два+ круга | **где** один круг быстрее — дельта по поворотам и накопленная |
| `race` | папка кругов | вся гонка — темп, деградация в стинтах, износ резины, ERS по кругам |
| `compare` | два+ круга | **где** один круг быстрее — дельта по поворотам и накопленная, плюс детерминированный `Verdict` (с топливной поправкой) |
| `race` | папка кругов | вся гонка — темп, деградация в стинтах, износ резины, ERS по кругам (F1) |
| `profile` | папка кругов | **дёшево** (~400 токенов) тенденции пилота по каждому составу (медиана coast/lock/spin на поворот, флаги относительно сессии) + авто-выбор кругов. Универсально: повороты кластеризуются по дистанции апекса, не по индексу |
| `rename` | файлы/папка | вставить тип сессии и номер круга в имя файла (`..._74.074_...` → `..._P1_L7_74.074_...`) |
Отчёты содержат легенду с пояснением каждой колонки, а промпт анализа добавляется в конец файла автоматически — достаточно скинуть файл в LLM. Промпты доступны на RU и EN; выбирается флагом `--lang en` в CLI или переключателем языка в GUI.
**Флаги:** `--no-prompt` убирает встроенный промпт (меньше файлы для агентных конвейеров; в GUI — чекбокс **Включить промпт**). `--force` перегенерирует, даже если свежий отчёт уже есть (иначе прогон пропускается как `cached`). `--game {auto,f1,acc}` переопределяет автоопределение игры (по умолчанию `auto`).
> Оптимальный порядок: сначала `profile` — увидеть хронические тенденции и какие круги важны, затем `technique`/`setup`/`compare` только по этим кругам с `--no-prompt`.
### Отличия ACC от F1 22
Машины GT3/GT4 не имеют гибридной системы, а ACC не экспортирует настройки машины:
- Отчёты `technique` и `race` не содержат разделов DRS / ERS / MGU-K.
- Режим `setup`: экспорт настроек из ACC — все нули, поэтому таблица параметров заменяется пометкой («ACC не экспортирует сетап — анализ построен по телеметрии»); разделы, основанные на телеметрии (баланс в поворотах, шины, тормоза, подвеска), остаются.
- Топливо в **литрах** (GT), а не в кг (F1). Шины — составы Pirelli dry/wet, а не F1 C1–C5.
### Структура папок
```
Telemetry/
races/
F1 22/
Zandvoort/
Практика/ <- CSV-файлы после переименования
Practice/ <- CSV-файлы после переименования
zandvoort_P1_L7_74.074_ferrari.csv
reports/ <- отчёты technique/setup/compare
Квалификация/
Qualifying/
zandvoort_Q1_L3_74.897_ferrari.csv
reports/
Гонка/
Race/
zandvoort_R_L7_74.152_ferrari.csv
reports/
Гонка_race.md <- отчёт режима race
Race_race.md <- отчёт режима race
ACC/
Hungaroring/
Practice/
hungaroring_P_L1_123.702_ktm_gt4.csv
reports/
tool/ <- этот репозиторий
telemetry/
prompts/
@ -57,7 +76,7 @@ Telemetry/
## Полезное
**GUI** — двойной клик по `run-gui.bat` (или `run-gui.pyw` без окна консоли, либо `python -m telemetry gui`). Выбери режим и язык промпта (RU/EN), укажи файл/папку, нажми Generate. Диалог открывается в последней использованной папке для каждого режима отдельно. Кнопка **Rename unprocessed** добавляет тип сессии и номер круга к файлам, у которых их ещё нет, и не трогает остальные.
**GUI** — двойной клик по `run-gui.bat` (или `run-gui.pyw` без окна консоли, либо `python -m telemetry gui`). Окно разделено на две вкладки — **F1 22** и **ACC** — у каждой свои независимые элементы управления и лог. Выбери режим и язык промпта (RU/EN), укажи файл/папку, нажми Generate. Диалог открывается в последней использованной папке для каждого режима отдельно. Кнопка **Rename unprocessed** добавляет тип сессии и номер круга к файлам, у которых их ещё нет, и не трогает остальные. Если игра в файле не совпадает с активной вкладкой, GUI выводит предупреждение, но продолжает работу.
**CLI** — из папки `tool/`:
@ -66,10 +85,18 @@ python -m telemetry technique races/Zandvoort/Практика/lap.csv
python -m telemetry setup races/Zandvoort/Практика/lap.csv
python -m telemetry compare races/Zandvoort/Практика/lap_a.csv races/Zandvoort/Практика/lap_b.csv
python -m telemetry race races/Zandvoort/Гонка
python -m telemetry profile races/Zandvoort/Гонка # дешёвые тенденции по всем кругам + авто-выбор
python -m telemetry rename races/Zandvoort/Практика # или отдельные файлы
# выбор языка промпта (по умолчанию: ru)
python -m telemetry technique races/Zandvoort/Практика/lap.csv --lang en
# переопределить определение игры (по умолчанию: auto)
python -m telemetry technique ACC/races/Hungaroring/Practice/lap.csv --game acc
# агентный конвейер: убрать промпт, переиспользовать свежие отчёты
python -m telemetry technique races/Zandvoort/Практика/lap.csv --no-prompt
python -m telemetry technique races/Zandvoort/Практика/lap.csv --force # игнорировать кэш
```
Каждый прогон печатает путь к отчёту и оценку токенов:
@ -79,11 +106,19 @@ C:\...\Telemetry\reports\lap_technique.md
~33834 tokens (budget 60k)
```
**Rename трогает только необработанные файлы.** Файл считается обработанным, когда в имени есть оба токена: тип сессии (`P1`, `Q2`, `R`, …) и `L<номер>`. Повторный запуск безопасен.
**Rename вставляет токены И распределяет по папкам.** Укажи папку, где лежат CSV (например, `D:\Games\Telemetry\Data\lapdata\f1_2022`) — скрипт:
- Рекурсивно сканирует все подпапки
- Читает метаданные каждого CSV (трасса, тип сессии)
- Вставляет `P1_L7` в имя файла, если там ещё нет обоих токенов
- Перемещает в `races/<трасса>/Practice|Qualifying|Race/`
- Даже уже переименованные файлы, если они лежат не там где надо, перемещаются в нужную папку
- Файл пропускается только если он УЖЕ находится именно в нужной папке
Повторный запуск безопасен — файлы уже на месте не трогаются.
## Входной формат
Телеметрия F1 22 (UDP), экспортированная в CSV (тестировалось на [F1Laps](https://www.f1laps.com)). UTF-8, ~1800 строк на круг (≈29 Гц), 108 каналов. Один файл = один круг. Для режима `race` сложи все круги гонки в одну папку.
Телеметрия F1 22 (UDP) или ACC, экспортированная в CSV (F1 22 тестировалось на [F1Laps](https://www.f1laps.com)). UTF-8, 108 каналов, один файл = один круг (F1 22 — ≈1800 строк/круг при ≈29 Гц; круги ACC плотнее, ~14k строк — отчёты остаются компактными за счёт адаптивного ресэмплинга). Для режима `race` сложи все круги гонки в одну папку. Формат CSV одинаков у F1 22 и ACC; игра определяется из поля `Game` в метаданных каждого файла.
## Для разработчиков
@ -99,7 +134,7 @@ python -m telemetry gui # запустить GUI
## Благодарности
Сделано для разбора личных хотлапов в F1 22. Формат экспорта телеметрии — [F1Laps](https://www.f1laps.com).
Сделано для разбора личных хотлапов в F1 22 и ACC. Формат экспорта телеметрии — [F1Laps](https://www.f1laps.com). Телеметрия в игре записывается через Telemetry Tool от Iasione.
## Лицензия

62
prompts/en/acc/compare.md Normal file
View file

@ -0,0 +1,62 @@
You are a race engineer comparing two laps from the same driver in Assetto Corsa Competizione (GT3/GT4). Lap 1 is the reference. Find where and why one lap is faster than the other and what it reveals about the driving.
## Data format
**Header** — comparison table: laptime, sectors S1/S2/S3, tyre, fuel at lap start [L], tyre wear, weather.
**Corners** — per corner:
- `Vmin L1 / Vmin L2` — minimum apex speed [km/h]
- `time L1 / time L2` — time in corner zone [s]
- `Δt seg` — time delta in this corner (L2 − L1): negative = L2 faster, positive = L1 faster
- `Δt cum` — cumulative delta at end of this corner: shows running score to this point in the lap
**Delta trace** — per sample by distance:
- `dist` — distance [m]
- `spd_L1 / spd_L2` — speed per lap [km/h]
- `Δt` — cumulative delta L2 − L1 [s]: negative = L2 leads, positive = L1 leads
---
## Task
Analyse each point. Back every conclusion with specific Δt numbers, distances, speeds.
### 1. Overview
- Overall time difference and which sector it was built in
- Was the gap steady across the lap or created sharply in one area
- Fuel and tyre wear effect: if laps are from different stints, account for load and degradation
### 2. Corner-by-corner
Per corner with non-zero `Δt seg`:
- Who is faster and by how much
- `Vmin L1 vs L2`: higher apex speed = either later braking or better line
- High Vmin but higher `time` = faster apex but worse exit (traction or trajectory loss)
- Lower Vmin but lower `time` = better exit compensating for the slower apex
- `Δt cum` after corner: does the gap grow, or does the next section recover it
### 3. Delta trace analysis
Working with `Δt` by distance:
- **Where delta builds sharply** (steep kink): critical points — braking or traction where styles diverge
- **Where delta is flat**: both laps equivalent, lower priority zones
- **Braking zones**: Δt shrinking under braking = L2 brakes later (or more effectively)
- **Traction zones**: after apex `spd_L2 > spd_L1` = L2 traction better (earlier throttle or less wheelspin); in GT cars this matters especially at slow-corner exits where mechanical grip is the limiting factor
- **Straights**: speed difference on a straight = result of the preceding corner exit, not the straight itself
### 4. Patterns
- One lap systematically better in slow corners, the other in fast?
- One style risks entries, the other risks exits?
- Any corner where the "slower" lap is actually faster at that specific point — and why?
### 5. Summary and priorities
- **What to take from the faster lap**: specific corners and technique
- **Priority zones to work on**: biggest `|Δt seg|` opportunities
- **What not to copy**: corners where the "faster" lap is still slower than the reference — note them
---
Below is the attached comparison report. Begin analysis.

92
prompts/en/acc/race.md Normal file
View file

@ -0,0 +1,92 @@
You are a race engineer analysing full-race telemetry in Assetto Corsa Competizione (GT3/GT4). The report covers all laps: lap times, stints, tyre wear, and thermals. Perform a detailed race analysis.
## Data format
**Lap times** — per lap:
- `Lap` — lap number
- `Time` — lap time `M:SS.mmm`
- `S1 / S2 / S3` — sector times [s]
- `Tyre` — compound (dry/wet type)
- `Age` — laps on this set (1 = first lap on fresh set)
- `Fuel L` — fuel at lap start [L]
- `Note` — flags: `FAST` = fastest valid lap, `PIT` = in-lap, `INV` = invalidated
**Stint summary** — per stint:
- `Stint` — stint number
- `Laps` — laps in stint
- `Tyre` — compound
- `Laps on tyre` — total laps on this set
- `Avg pace` — average pace (excluding pit laps and invalidated)
- `Best / Worst` — best and worst lap in stint
- `Deg s/lap` — degradation slope per tyre age lap (positive = slowing, negative = improving)
**Tyre wear per lap** — `FL / FR / RL / RR` — wear % at end of each lap
**Thermal summary per lap**:
- `Tf avg°C` — average peak front tyre temperature for the lap
- `Tr avg°C` — rear
- `Brk FL max°C` — peak front-left brake temperature
- `Brk RR max°C` — peak rear-right brake temperature
---
## Task
Analyse each point. Back every conclusion with specific numbers from the tables.
### 1. Race overview
- How did pace evolve through the race: progression or degradation?
- Were there outliers (abnormally slow or fast laps) — name the lap numbers and reason (PIT, INV, safety car?)
- Compare best vs average pace: how large is the gap?
- One-sentence overall race assessment
### 2. Stint analysis
Per stint from `Stint summary`:
- Average pace and relative quality (best/worst stint?)
- `Best–Worst` spread: large spread = inconsistent pace, small = consistent
- Compare stints: where was the driver faster and why (tyre, fuel level, track condition?)
- Was the pit stop timed well: not too early/late given degradation trend
### 3. Tyre degradation
From `Deg s/lap` and `Tyre wear`:
- Which stint has the most/least degradation?
- Compare `Deg s/lap` vs actual wear increment per lap — consistent?
- When did the tyre "die": identify the lap where pace dropped sharply
- Wear evenness: FL/FR or RL/RR asymmetry?
### 4. Fuel effect
GT cars carry fuel in litres; as fuel burns off the car gets lighter.
- For the first (or long) stint: correlate `Fuel L` and `Time` per lap
- Is there a visible correlation between fuel burn and improving pace as the car lightens?
- If early-stint pace is poor — tyre warm-up or a heavy car?
- Check whether pace dynamics are consistent with the expected weight-reduction benefit
### 5. Thermal management
From `Thermal summary`:
- How do tyre temps evolve through each stint: warm-up, stable, overheating?
- Pirelli GT tyre operating window is approximately ~70–90°C; a sharp rise above this with simultaneous pace drop signals overheating
- Brake temps: rising through the race distance or stable? FL vs RR asymmetry?
- Which laps had critical thermal conditions?
### 6. Best and worst laps
- `FAST` lap: which stint, what tyre age, what fuel level?
- Why did the best lap happen when it did — track rubber, clean air, cool conditions?
- Worst valid lap (not INV, not PIT): reason — degradation, traffic, driver error?
- Patterns: "out lap" always slow, "in lap" pace dip?
### 7. Summary and recommendations
- **What worked**: strategy and driving that was executed well
- **Main losses**: specific laps and zones where the most time was lost
- **Strategic recommendations**: pit timing, compound choice, stint length
- **Driving technique**: if degradation is abnormal — what to change in driving style
---
Below is the attached race report. Begin analysis.

80
prompts/en/acc/setup.md Normal file
View file

@ -0,0 +1,80 @@
You are an experienced race engineer. The following is a telemetry report for one lap in Assetto Corsa Competizione (GT3/GT4). Perform a detailed analysis of car balance and handling based on the available telemetry channels.
## Important note
ACC does not export car setup parameters to telemetry — the in-game setup table is all zeros and is omitted from the report. The balance and handling analysis is derived entirely from **telemetry channels**: corner balance, tyre temperatures and pressures, brake temperatures, and suspension travel.
## Data format
**Corner balance** — per corner:
- `gLat_max` — peak lateral G-force [G]
- `steer/g` — steering angle to lateral G ratio: high = steering a lot for the G = understeer
- `front|slip|` / `rear|slip|` — average absolute slip of front/rear axle in the corner zone
- `balance` — outcome: US (front axle slips more), OS (rear), — (neutral)
**Tyres** — per wheel:
- `wearΔ%` — wear over the lap
- `surf min/avg/max °C` — surface temperature
- `carc min/avg/max °C` — carcass temperature
- `pressure` — hot pressure [PSI]
**Brakes** — min/avg/max temperature per wheel [°C]
**Suspension**:
- Rake — average front vs rear ride height [mm]; positive rake = nose higher than tail
- Roll per corner — body roll in each corner [mm]; shows outer-wheel loading
**Lap phases** — average tyre surface temperature across three equal lap segments
---
## Task
Analyse each point. Back every conclusion with numbers from the report.
### 1. Mechanical balance
- Corner balance: systematic US or OS across most corners?
- Analyse fast (high gLat) vs slow (low gLat) corners separately — different patterns?
- High `steer/g` = car demands more steering for the load level = understeer tendency; low `steer/g` with high rear slip = oversteer
- GT cars are sensitive to front/rear spring and anti-roll bar balance: abnormal Roll per corner may indicate a soft spring or weak anti-roll bar on one axle
### 2. Differential and traction
- From `spin%` in the corner table: high rear slip on exit indicates an aggressive on-throttle differential lock or premature throttle application
- If `spin%` is consistently high in slow corners but modest in fast corners — on-throttle differential setting is the likely cause
- Check whether rear slip is symmetric across left and right sides — asymmetry suggests a geometry or tyre pressure issue
### 3. Tyre thermal window
- Pirelli GT tyre operating window is approximately ~70–90°C surface; outside this range grip is compromised
- Are tyres in window; who overheats, who underheats
- Surface vs carcass delta: large gap = tyre overheating on the outside but cold carcass (delamination risk), or vice versa
- Wear: even across 4 wheels — abnormally high wear on one wheel signals load imbalance
- Lap phases: normal warm-up pattern — should rise in phase 1 and stabilise
### 4. Tyre pressures
- Hot pressure (from Tyres): too high = overheating and loss of contact patch; too low = under-temperature, tyre wanders
- F/R pressure balance: asymmetry affects mechanical balance
- L/R pressure difference on the same axle — sign of asymmetric heating or incorrect cold starting pressure
### 5. Brakes
- Temperature range: <200°C = cold (poor braking), 350–800°C = normal GT working range, >900°C = dangerous overheating
- L/R asymmetry: different temperatures on same axle = uneven pad wear or brake balance issue
- If rears are consistently hotter than fronts — bias is too far rearward; if fronts dominate — bias is forward; both reduce braking stability
### 6. Suspension
- Rake: moderate positive rake is standard for GT; very high rake increases rear aerodynamic load but can cause a lazy front on turn-in
- Roll per corner: typical roll 3–8 mm; large roll = soft suspension or weak anti-roll bars; small = stiff, sensitive to bumps
- L/R roll asymmetry in the same corner indicates uneven suspension travel or load distribution
### 7. Recommendations
Per identified issue — what specifically to change (direction of adjustment) and expected effect. Prioritise by lap time impact.
---
Below is the attached report. Begin analysis.

View file

@ -0,0 +1,92 @@
You are an experienced racing engineer. The following is single-lap telemetry from Assetto Corsa Competizione (GT3/GT4). Perform a detailed driving technique analysis.
## Data format
The report contains:
**Corners** — corner table. Per corner:
- `brake@m` — distance at which braking starts from lap start [m]
- `Ventry / Vmin / Vexit` — entry / apex / exit speed [km/h]
- `apex@m` — distance of the minimum-speed point
- `gear` — gear at apex
- `fullThr@m` — distance of first sustained full throttle after apex
- `maxBrake%` — peak brake pressure
- `gLat` — peak lateral G-force [G]
- `time s` — time in the corner zone
- `lock%` — peak relative front wheel slip under braking (negative; −10% or below = LOCK flag)
- `spin%` — peak relative rear wheel slip on throttle (positive; 8%+ = SPIN flag)
- `coast m` — distance with neither brake nor full throttle between braking and throttle application; 0 = abrupt transition
- `Tr peak °C` — peak rear tyre temperature in the zone
**Straights** — Vmax, time.
**Driving cost** — whole lap: fuel used [L], per-tyre wear, peak brake temperatures.
**Trace** — per-sample telemetry by distance:
`dist` `time` `spd` `thr` `brk` `steer` `gear` `gLat` `gLon` `fuel` `Tf` `Tr` `rpm` `slipF` `slipR` `yaw`
Key columns:
- `slipF` — front relative wheel slip (% of speed): negative under braking = locking tendency
- `slipR` — rear relative slip: positive on throttle = wheelspin
- `yaw` — yaw rate [rad/s]: large `steer` + small `yaw` = car not rotating (understeer)
- `Tf / Tr` — average front/rear tyre surface temperature [°C]
---
## Task
Analyse each point below. Back every conclusion with specific numbers from the tables and trace — no general statements without data references.
### 1. Braking
Per corner with non-zero `brake@m`:
- Evaluate the braking point and how fully the brakes are used (`maxBrake%`)
- Analyse `lock%`: how severe is the lockup, isolated or repeated across corners
- Evaluate `coast m`: long coast = cautious transition or trail braking; zero = snap release
- Compare `Ventry` and `Vmin`: large delta = hard braking deep into the corner; small = braking complete before turn-in
### 2. Apexes
Per corner:
- `Vmin` in context of the corner's speed class — over-braked or on the limit?
- `gear` at apex — too low (excess torque, wheelspin risk on exit)?
- `gLat` vs `Vmin`: high gLat at low Vmin = good apex loading; low gLat = missed apex
### 3. Exits
Per corner:
- `fullThr@m` vs `apex@m`: smaller gap = more aggressive exit
- `spin%`: high value = wheelspin on exit — throttle applied too early or too aggressively
- `Vexit`: final result — does it match the corner's speed potential
- From Trace: how `thr` builds after apex — stepped or smooth; any drops after reaching full throttle
### 4. Steering and handling
From Trace (focus on cornering zones):
- `steer`: any sharp sawtooth inputs or excessive corrections
- `steer` + `yaw`: zones where yaw lags far behind steer — specific understeer points
- Corners where the car refuses to rotate (high steer, weak yaw response)
### 5. Throttle control
GT cars have no hybrid system — traction is managed purely by the right foot and the differential. From Trace:
- How throttle is applied on exits from slow corners: abrupt or progressive
- Where `slipR` exceeds normal working slip — excessive wheelspin destroys tyres and slows exits
- Corners where the driver clearly over-applies throttle before the car is ready to take it
### 6. Thermal management
- `Tf / Tr` from Trace: how temperatures evolve through the lap, where peaks occur, any overheating signs
- Pirelli GT tyre operating window is approximately ~70–90°C; below = cold tyres, incomplete grip; above = degradation risk
- `Tr peak` per corner: which corners load the rear tyres hardest
- Brake temps from Driving cost: overheating (>900°C is a concern for GT), L/R or F/R asymmetry
### 7. Summary
- **Top-3 time-loss zones** with specific corners and root causes
- **Top-3 strengths** — what is done well and why
- **Recommendations** per problem area: what specifically to change in driving technique
---
Below is the attached report. Begin analysis.

64
prompts/ru/acc/compare.md Normal file
View file

@ -0,0 +1,64 @@
Ты — гоночный инженер, сравнивающий два круга одного пилота в Assetto Corsa Competizione (GT3/GT4). Первый круг (Lap 1) — эталонный. Найди, где и почему один круг быстрее другого, и что это говорит о пилотировании.
## Формат данных
**Шапка** — сравнительная таблица: laptime, секторы S1/S2/S3, шина, топливо на старте круга [л], износ шин, погода.
**Corners** — по каждому повороту:
- `Vmin L1 / Vmin L2` — минимальная скорость в апексе [км/ч]
- `time L1 / time L2` — время в зоне поворота [с]
- `Δt seg` — разница времён в данном повороте (L2 − L1): отрицательное = L2 быстрее, положительное = L1 быстрее
- `Δt cum` — накопленная дельта к концу этого поворота: показывает общий счёт на данном участке круга
**Delta trace** — построчно по дистанции:
- `dist` — дистанция [м]
- `spd_L1 / spd_L2` — скорость каждого круга [км/ч]
- `Δt` — накопленная дельта L2 − L1 [с]: отрицательное = L2 ведёт, положительное = L1 ведёт
---
## Задача
Разбери каждый пункт. Каждый вывод подкрепляй конкретными числами Δt, дистанциями, скоростями.
### 1. Общая картина
- Итоговая разница времён и в каком секторе она сформировалась
- Был ли разрыв стабильным по кругу или резко возник в одном месте
- Влияние топлива и износа шин: если круги из разных стинтов — учти поправку на загрузку и деградацию
### 2. Разбор по поворотам
По каждому повороту с ненулевым `Δt seg`:
- Кто быстрее и на сколько
- `Vmin L1 vs L2`: кто выше скорость в апексе — тот либо позже тормозил, либо лучше нашёл траекторию
- Если Vmin выше, но `time` больше — апекс быстрее, но выход хуже (потеря на тяге или траектории)
- Если Vmin ниже, но `time` меньше — лучший выход компенсировал потерю в апексе
- `Δt cum` после поворота: нарастает ли отрыв или следующий участок его нивелирует
### 3. Анализ дельта-трассы
Работай с `Δt` по дистанции:
- **Где дельта строится резко** (крутой излом): это критические точки — торможение или тяга, где стили расходятся
- **Где дельта стабильна**: оба круга едут одинаково, эти зоны не приоритет
- **Тормозные зоны**: если Δt уменьшается при торможении — L2 тормозит позже (или эффективнее)
- **Тяговые зоны**: если после апекса `spd_L2 > spd_L1` — L2 лучше тянет (раньше газ или меньше буксовки); в GT это особенно важно на выходах из шпилек, где тяговый контроль — ключ
- **Прямые**: разница скоростей на прямой = результат предыдущего выхода из поворота, а не самой прямой
### 4. Паттерны
- Один круг систематически лучше в медленных поворотах, другой — в быстрых?
- Один стиль рискует на входах, другой — на выходах?
- Есть ли повороты, где «медленный» круг на самом деле быстрее в этом конкретном месте — и почему?
### 5. Итог и приоритеты
- **Что взять из быстрого круга**: конкретные повороты и техника
- **Приоритетные зоны для работы**: где наибольший потенциал (самые большие `|Δt seg|`)
- **Что не копировать**: если в «быстром» круге есть места, где он всё же медленнее — зафиксируй
---
Ниже прикреплён отчёт сравнения. Начинай анализ.

95
prompts/ru/acc/race.md Normal file
View file

@ -0,0 +1,95 @@
Ты — гоночный инженер, анализирующий телеметрию целой гонки в Assetto Corsa Competizione (GT3/GT4). Перед тобой отчёт по всем кругам: времена, стинты, износ шин и тепловой режим. Проведи детальный разбор гонки.
## Формат данных
Отчёт содержит:
**Lap times** — таблица кругов:
- `Lap` — номер круга
- `Time` — время круга в формате `M:SS.mmm`
- `S1 / S2 / S3` — времена секторов [с]
- `Tyre` — тип шины (dry/wet, состав)
- `Age` — количество кругов на данном комплекте (1 = первый круг на новом комплекте)
- `Fuel L` — топливо на старте круга [л]
- `Note` — флаги: `FAST` = быстрейший действительный круг, `PIT` = пит-лэп, `INV` = недействительный круг
**Stint summary** — сводка по стинтам:
- `Stint` — номер стинта
- `Laps` — количество кругов в стинте
- `Tyre` — тип шины
- `Laps on tyre` — всего кругов на комплекте
- `Avg pace` — средний темп (без пит-лэпов и недействительных)
- `Best / Worst` — лучший и худший круг стинта
- `Deg s/lap` — деградация: наклон тренда времён по тирейджу (положительное = замедление, отрицательное = улучшение)
**Tyre wear per lap** — износ по кругам:
- `FL / FR / RL / RR` — процент износа каждой шины (последний замер круга)
**Thermal summary per lap** — тепловой режим:
- `Tf avg°C` — средняя максимальная температура передних шин за круг
- `Tr avg°C` — задних шин
- `Brk FL max°C` — максимальная температура переднего левого тормоза
- `Brk RR max°C` — заднего правого тормоза
---
## Задача
Разбери каждый из пунктов. Каждый вывод подкрепляй конкретными числами из таблиц — не делай общих утверждений без ссылки на данные.
### 1. Общая картина гонки
- Как менялся темп по ходу гонки: прогрессия или деградация?
- Были ли выбросы (аномально медленные или быстрые круги) — назови номера кругов и причину (PIT, INV, safety car?)
- Сравни лучший и средний темп: насколько велик разрыв?
- Итоговая оценка гонки в одном предложении
### 2. Анализ стинтов
По каждому стинту из `Stint summary`:
- Средний темп и его относительное качество (лучший / худший стинт?)
- Разброс `Best–Worst`: большой разброс = непостоянный темп, малый = стабильный
- Сравни стинты между собой: где пилот ехал быстрее и почему (шина, топливо, трек, условия?)
- Был ли питстоп своевременным: не слишком ли рано/поздно по тренду деградации?
### 3. Деградация резины
По `Deg s/lap` и таблице `Tyre wear`:
- В каком стинте деградация наибольшая и наименьшая?
- Сопоставь `Deg s/lap` с фактическим приростом износа по кругам — соответствуют ли друг другу?
- Когда резина «умерла»: определи круг, после которого темп резко упал
- Равномерность износа: есть ли асимметрия FL/FR или RL/RR?
### 4. Топливный эффект
GT-машины заправляются в литрах; по мере выжигания топлива машина становится легче.
- По первому стинту (или длинному стинту): сопоставь `Fuel L` и `Time` по кругам
- Есть ли видимая корреляция между расходом топлива и улучшением темпа по мере облегчения машины?
- Если темп в начале стинта плохой — прогрев шин или тяжёлая машина?
- Оцени, согласуется ли динамика темпа с ожидаемым эффектом снижения веса
### 5. Тепловой менеджмент
По `Thermal summary`:
- Как меняются температуры шин по кругам в каждом стинте: прогрев, стабилизация, перегрев?
- Рабочий диапазон шин Pirelli (GT) ориентировочно ~70–90°C; резкий рост выше с одновременным ухудшением темпа — признак перегрева
- Температуры тормозов: рост по дистанции или стабильные? Асимметрия FL vs RR?
- В каких кругах тепловой режим был критическим?
### 6. Лучшие и худшие круги
- `FAST` круг: в каком стинте, на каком тирейдже, при каком запасе топлива?
- Почему лучший круг получился лучшим — трек rubber? чистый воздух? прохлада?
- Худший действительный круг (не INV, не PIT): причина — деградация, трафик, ошибка?
- Есть ли интересные паттерны: «out lap» всегда медленный, «in lap» провальный по темпу?
### 7. Итог и рекомендации
- **Что сработало:** что в стратегии и пилотировании было сделано хорошо
- **Главные потери:** конкретные круги и зоны, где потеряно больше всего
- **Стратегические рекомендации:** момент питстопа, выбор состава, длина стинтов
- **Техника пилотирования:** если деградация аномальна — что нужно изменить в стиле езды
---
Ниже прикреплён отчёт гонки. Начинай анализ.

80
prompts/ru/acc/setup.md Normal file
View file

@ -0,0 +1,80 @@
Ты — опытный гоночный инженер. Перед тобой телеметрический отчёт за один круг в Assetto Corsa Competizione (GT3/GT4). Проведи детальный разбор баланса и поведения машины на основе доступных телеметрических данных.
## Важное замечание
ACC не экспортирует параметры сетапа в телеметрию — таблица сетапа в игре содержит нули и в отчёт не включается. Анализ баланса и управляемости строится **исключительно по телеметрическим каналам**: угловой баланс, температуры и давления шин, тормоза, ходы подвески.
## Формат данных
**Corner balance** — по каждому повороту:
- `gLat_max` — пиковая боковая перегрузка [G]
- `steer/g` — отношение угла руля к боковой G: высокое = рулишь много на данном G = understeer
- `front|slip|` / `rear|slip|` — средний модуль скольжения переднего / заднего осей в зоне поворота
- `balance` — итог: US (передняя ось проскальзывает сильнее), OS (задняя), — (нейтрально)
**Tyres** — по каждому колесу:
- `wearΔ%` — износ за круг
- `surf min/avg/max °C` — температура поверхности
- `carc min/avg/max °C` — температура каркаса
- `pressure` — горячее давление [PSI]
**Brakes** — температура мин/сред/макс по каждому колесу [°C]
**Suspension**:
- Rake — средняя высота носа vs кормы [мм]; положительный rake = нос выше кормы
- Roll по поворотам — крен кузова в каждом повороте [мм]; показывает загрузку внешних колёс
**Lap phases** — средняя температура поверхности шин по трём равным отрезкам круга
---
## Задача
Разбери каждый пункт. Каждый вывод подкрепляй числами из отчёта.
### 1. Механический баланс
- Corner balance: есть ли систематический US или OS по большинству поворотов
- Отдельно рассмотри быстрые (высокий gLat) vs медленные (низкий gLat) повороты — разные ли паттерны
- Высокий `steer/g` = машина требует больше руля на данной загрузке = недостаточная поворачиваемость; низкий `steer/g` при высоком скольжении задней оси = избыточная
- GT-машины чувствительны к балансу жёсткостей подвески и антикренов: аномальный Roll в поворотах может указывать на мягкую пружину или слабый стабилизатор на одной оси
### 2. Дифференциал и тяга
- По `spin%` из corner table: высокое скольжение задних на выходе указывает на агрессивный блок дифференциала на газу или слишком раннюю подачу газа
- Если `spin%` системно высок в медленных поворотах, но невелик в быстрых — причина, вероятно, в настройке OnThrottle дифференциала
- Оцени, насколько равномерно скольжение по левой и правой сторонам — асимметрия = проблема с геометрией или давлением шин
### 3. Тепловой режим шин
- Рабочий диапазон шин Pirelli (GT) ориентировочно ~70–90°C поверхности; выход за границы снижает сцепление
- Попадают ли шины в рабочий диапазон; кто перегревается, кто недогревается
- Разница поверхности и каркаса: большая разница = шина перегревается снаружи, но каркас холодный (риск расслоения), или наоборот
- Износ: равномерный ли по 4 колёсам — аномально высокий износ одного колеса указывает на дисбаланс нагрузки
- Lap phases: нормальный ли прогрев — должен расти в первой фазе и стабилизироваться
### 4. Давление шин
- Горячее давление (из Tyres): слишком высокое = перегрев и потеря контакта с дорогой; слишком низкое = недогрев, шина «плывёт»
- Баланс F/R давления: асимметрия влияет на механический баланс
- Разница L/R давления на одной оси — признак асимметричного нагрева или неправильного старта давления в холодном состоянии
### 5. Тормоза
- Диапазон температур: <200°C = холодные (плохое торможение), 350–800°C = рабочий диапазон GT, >900°C = опасный перегрев
- Асимметрия L/R: разные температуры на одной оси = неравномерный износ колодок или проблема с балансом тормозного усилия
- Если задние тормоза стабильно горячее передних — bias смещён назад; если передние — вперёд; оба отклонения ухудшают управляемость при торможении
### 6. Подвеска
- Rake: умеренный положительный rake стандартен для GT; очень большой rake увеличивает аэродинамическую прижимную силу кормы, но снижает нейтральность на входе
- Roll по поворотам: нормальный крен 3–8 мм; большой крен = мягкая подвеска или слабые антикрены; малый = жёсткая, чувствительная к буграм
- Асимметрия Roll L/R в одном повороте указывает на разные ходы подвески или неравномерную нагрузку
### 7. Рекомендации
По каждой выявленной проблеме — что конкретно менять (направление изменения настройки) и каков ожидаемый эффект. Приоритизируй по влиянию на время круга.
---
Ниже прикреплён отчёт. Начинай анализ.

View file

@ -0,0 +1,92 @@
Ты — опытный инженер-аналитик гоночной команды. Перед тобой телеметрия одного круга в Assetto Corsa Competizione (GT3/GT4). Проведи максимально детальный разбор стиля пилотирования.
## Формат данных
Отчёт содержит:
**Corners** — таблица поворотов. По каждому:
- `brake@m` — дистанция начала торможения от старта круга
- `Ventry / Vmin / Vexit` — скорость входа / апекса / выхода [км/ч]
- `apex@m` — дистанция точки минимальной скорости
- `gear` — передача в апексе
- `fullThr@m` — дистанция первого устойчивого полного газа после апекса
- `maxBrake%` — пиковое давление на тормоз
- `gLat` — пиковая боковая перегрузка [G]
- `time s` — время в зоне поворота
- `lock%` — пик относительного скольжения передних колёс при торможении (отрицательное; −10% и ниже = флаг LOCK)
- `spin%` — пик относительного скольжения задних колёс на газу (положительное; 8%+ = флаг SPIN)
- `coast m` — дистанция без тормоза и без полного газа между торможением и тягой; 0 = резкий переход
- `Tr peak °C` — пиковая температура задней шины в зоне
**Straights** — прямые: Vmax, время.
**Driving cost** — за весь круг: расход топлива [л], износ каждой шины, пиковые температуры тормозов.
**Trace** — построчная телеметрия по дистанции:
`dist` `time` `spd` `thr` `brk` `steer` `gear` `gLat` `gLon` `fuel` `Tf` `Tr` `rpm` `slipF` `slipR` `yaw`
Пояснения к ключевым колонкам:
- `slipF` — относительное скольжение передних колёс (% от скорости): уход в минус при торможении = блокировка
- `slipR` — задних колёс: положительный на газу = пробуксовка
- `yaw` — угловая скорость рыскания [рад/с]: большой `steer` + маленький `yaw` = машина не поворачивает (understeer)
- `Tf / Tr` — средняя температура передних / задних шин [°C]
---
## Задача
Разбери каждый из пунктов ниже. Каждый вывод подкрепляй конкретными числами из таблиц и трассы — не делай общих утверждений без ссылки на данные.
### 1. Торможения
По каждому повороту с ненулевым `brake@m`:
- Оцени точку торможения и полноту использования тормозов (`maxBrake%`)
- Проанализируй `lock%`: насколько сильная блокировка, стабильна ли она или разовая
- Оцени `coast m`: длинный накат — осторожный переход или trail braking; нулевой — резкий перенос ноги
- Сравни `Ventry` и `Vmin`: большая разница = активное торможение в повороте; малая = торможение завершено до поворота
### 2. Апексы
По каждому повороту:
- `Vmin` в контексте скоростного класса поворота — перетормозил или точно?
- `gear` в апексе — не слишком ли низкая передача (лишний момент, риск буксовки на выходе)
- `gLat` vs `Vmin`: высокий gLat при низком Vmin = хорошая загрузка в апексе; низкий gLat = промах мимо вершины
### 3. Выходы
По каждому повороту:
- `fullThr@m` относительно `apex@m`: чем меньше разрыв, тем агрессивнее выход
- `spin%`: высокое значение = буксовка на выходе — слишком рано или слишком резко газ
- `Vexit`: итоговый результат выхода — соответствует ли скоростному потенциалу поворота
- По Trace: как нарастает `thr` после апекса — ступенчато или плавно; есть ли просадки после полного газа
### 4. Работа руля и поворачиваемость
По Trace (фокус на зоны поворотов):
- `steer`: нет ли резких пилообразных движений или избыточных коррекций
- `steer` + `yaw`: в каких зонах yaw сильно отстаёт от steer — конкретные точки understeer
- В каких поворотах машина отказывается поворачивать (большой steer, слабый yaw)
### 5. Тяговый контроль
GT-машины не имеют гибридных систем — тяга управляется исключительно правой ногой и дифференциалом. По Trace:
- Как подаётся газ на выходе из медленных поворотов: резко или прогрессивно
- Где `slipR` выходит за пределы нормального скольжения — чрезмерная буксовка разрушает шины и замедляет выход
- Есть ли повороты, где пилот явно «переливает» газ раньше, чем машина готова к тяге
### 6. Тепловой режим
- `Tf / Tr` по Trace: как меняется температура по кругу, где пики, есть ли перегрев
- Рабочий диапазон шин Pirelli (GT) ориентировочно ~70–90°C; ниже = резина холодная, сцепление неполное; выше = риск деградации
- `Tr peak` по поворотам: какие повороты наиболее нагружают задние шины
- Brake temps из Driving cost: перегрев (>900°C — тревожный порог для GT), асимметрия L/R или F/R
### 7. Итог
- **Топ-3 зоны потери времени** с конкретными поворотами и причинами
- **Топ-3 сильные стороны** — что делается хорошо и почему
- **Рекомендации** по каждой проблемной зоне: что конкретно изменить в технике пилотирования
---
Ниже прикреплён отчёт. Начинай анализ.

View file

@ -4,12 +4,22 @@ import sys
from .parser import load_lap
from .report_common import report_path
from . import report_technique, report_setup, report_compare, report_race
from . import report_technique, report_setup, report_compare, report_race, report_profile
from .rename import rename_unprocessed
_RACES_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..', 'races'))
def is_fresh(report: str, sources: list[str]) -> bool:
if not os.path.isfile(report):
return False
try:
r_mtime = os.path.getmtime(report)
return all(os.path.getmtime(s) <= r_mtime for s in sources if os.path.isfile(s))
except OSError:
return False
def _print_result(abs_path: str, tokens: int) -> None:
print(abs_path)
print(f'~{tokens} tokens (budget 60k)')
@ -48,14 +58,23 @@ def main() -> None:
return
parser = argparse.ArgumentParser(prog='python -m telemetry')
parser.add_argument('mode', choices=['technique', 'setup', 'compare', 'race', 'rename', 'gui'])
parser.add_argument('mode', choices=['technique', 'setup', 'compare', 'race', 'profile', 'rename', 'gui'])
parser.add_argument('files', nargs='*')
parser.add_argument('--lang', choices=['ru', 'en'], default='ru')
parser.add_argument('--game', choices=['auto', 'f1', 'acc'], default='auto',
help='override game detection (default: auto, from the Game field)')
parser.add_argument('--no-prompt', action='store_true',
help='omit the embedded analysis prompt (for agent pipelines)')
parser.add_argument('--force', action='store_true',
help='regenerate even if a fresh report already exists')
args = parser.parse_args()
mode: str = args.mode
files: list[str] = args.files
lang: str = args.lang
game: str | None = None if args.game == 'auto' else args.game
include_prompt: bool = not args.no_prompt
force: bool = args.force
if mode == 'gui':
from .gui import main as gui_main
@ -75,38 +94,50 @@ def main() -> None:
if mode == 'compare' and len(files) < 2:
print('Error: compare requires 2+ files', file=sys.stderr)
sys.exit(2)
if mode == 'race' and len(files) != 1:
print('Error: race requires exactly 1 directory', file=sys.stderr)
if mode in ('race', 'profile') and len(files) != 1:
print(f'Error: {mode} requires exactly 1 directory', file=sys.stderr)
sys.exit(2)
def _emit(out: str, sources: list[str], gen) -> None:
if not force and is_fresh(out, sources):
print(os.path.abspath(out))
print('cached (up to date) — use --force to regenerate')
return
_print_result(*gen())
if mode == 'technique':
lap = load_lap(files[0])
out = report_path(files[0], 'technique')
_print_result(*report_technique.generate(lap, out, lang))
_emit(out, [files[0]],
lambda: report_technique.generate(load_lap(files[0]), out, lang, include_prompt, game))
elif mode == 'setup':
lap = load_lap(files[0])
out = report_path(files[0], 'setup')
_print_result(*report_setup.generate(lap, out, lang))
_emit(out, [files[0]],
lambda: report_setup.generate(load_lap(files[0]), out, lang, include_prompt, game))
elif mode == 'compare':
laps = [load_lap(f) for f in files]
stem_b = os.path.splitext(os.path.basename(files[1]))[0]
out = report_path(files[0], 'compare', stem_b)
_print_result(*report_compare.generate(laps, out, lang))
_emit(out, files,
lambda: report_compare.generate([load_lap(f) for f in files], out, lang, include_prompt, game))
elif mode == 'race':
elif mode in ('race', 'profile'):
race_dir = os.path.abspath(files[0])
csv_files = sorted(
f for f in os.listdir(race_dir) if f.lower().endswith('.csv')
csv_paths = sorted(
os.path.join(race_dir, f) for f in os.listdir(race_dir) if f.lower().endswith('.csv')
)
if not csv_files:
if not csv_paths:
print(f'Error: no CSV files found in {race_dir}', file=sys.stderr)
sys.exit(2)
laps = [load_lap(os.path.join(race_dir, f)) for f in csv_files]
dirname = os.path.basename(race_dir.rstrip('/\\'))
out = os.path.join(race_dir, 'reports', dirname + '_race.md')
_print_result(*report_race.generate(laps, out, lang))
suffix = '_race.md' if mode == 'race' else '_profile.md'
out = os.path.join(race_dir, 'reports', dirname + suffix)
if mode == 'race':
_emit(out, csv_paths,
lambda: report_race.generate([load_lap(p) for p in csv_paths], out, lang, include_prompt, game))
else:
_emit(out, csv_paths,
lambda: report_profile.generate([load_lap(p) for p in csv_paths], out, lang, include_prompt))
if __name__ == '__main__':

View file

@ -5,222 +5,301 @@ import tkinter as tk
from tkinter import filedialog, scrolledtext, ttk
from .parser import load_lap
from .report_common import report_path
from . import report_technique, report_setup, report_compare, report_race
from .report_common import game_of, report_path
from . import report_technique, report_setup, report_compare, report_race, report_profile
from .rename import rename_unprocessed
# project root = parent of the tool\ dir
_PROJECT_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..'))
_RACES_DIR = os.path.join(_PROJECT_ROOT, 'races')
# Maps tab id → display name used in mismatch warnings.
_TAB_GAME = {'f1': 'F1 22', 'acc': 'ACC'}
def _race_output_path(race_dir: str) -> str:
dirname = os.path.basename(race_dir.rstrip('/\\'))
return os.path.join(race_dir, 'reports', dirname + '_race.md')
class _TabState:
"""All mutable state that belongs to one tab."""
def __init__(self) -> None:
self.mode = tk.StringVar(value='technique')
self.lang = tk.StringVar(value='ru')
self.include_prompt = tk.BooleanVar(value=True)
self.path_var = tk.StringVar()
self.selection: list[str] = []
self.last_dir: dict[str, str] = {}
self.log_queue: queue.Queue[str | None] = queue.Queue()
self.busy = False
# widgets set by _build_panel
self.log_widget: scrolledtext.ScrolledText | None = None
self.btn_generate: ttk.Button | None = None
self.btn_rename: ttk.Button | None = None
class _App:
def __init__(self, root: tk.Tk) -> None:
self._root = root
root.title('F1 Telemetry Analyzer')
root.title('Telemetry Analyzer (F1 22 / ACC)')
root.resizable(True, True)
self._mode = tk.StringVar(value='technique')
self._lang = tk.StringVar(value='ru')
self._selection: list[str] = []
self._last_dir: dict[str, str] = {}
self._log_queue: queue.Queue[str | None] = queue.Queue()
self._busy = False
self._tabs: dict[str, _TabState] = {
'f1': _TabState(),
'acc': _TabState(),
}
self._build_ui()
self._poll_log()
self._poll_all_logs()
# ── UI construction ──────────────────────────────────────────────────────
def _build_ui(self) -> None:
nb = ttk.Notebook(self._root)
nb.pack(fill='both', expand=True, padx=6, pady=6)
for tab_id, label in [('f1', 'F1 22'), ('acc', 'ACC')]:
frame = ttk.Frame(nb)
nb.add(frame, text=label)
self._build_panel(frame, tab_id)
def _build_panel(self, parent: ttk.Frame, tab_id: str) -> None:
st = self._tabs[tab_id]
pad = {'padx': 8, 'pady': 4}
mode_frame = ttk.LabelFrame(self._root, text='Mode')
mode_frame = ttk.LabelFrame(parent, text='Mode')
mode_frame.pack(fill='x', **pad)
for label, value in [('Technique', 'technique'), ('Setup', 'setup'),
('Compare', 'compare'), ('Race', 'race')]:
ttk.Radiobutton(mode_frame, text=label, variable=self._mode,
('Compare', 'compare'), ('Race', 'race'),
('Profile', 'profile')]:
ttk.Radiobutton(mode_frame, text=label, variable=st.mode,
value=value).pack(side='left', padx=6, pady=4)
lang_frame = ttk.LabelFrame(self._root, text='Prompt language / Язык промпта')
lang_frame = ttk.LabelFrame(parent, text='Prompt language / Язык промпта')
lang_frame.pack(fill='x', **pad)
ttk.Radiobutton(lang_frame, text='RU', variable=self._lang,
ttk.Radiobutton(lang_frame, text='RU', variable=st.lang,
value='ru').pack(side='left', padx=6, pady=4)
ttk.Radiobutton(lang_frame, text='EN', variable=self._lang,
ttk.Radiobutton(lang_frame, text='EN', variable=st.lang,
value='en').pack(side='left', padx=6, pady=4)
ttk.Checkbutton(lang_frame, text='Включить промпт в отчёт / Include prompt',
variable=st.include_prompt).pack(side='left', padx=16, pady=4)
input_frame = ttk.LabelFrame(self._root, text='Input')
input_frame = ttk.LabelFrame(parent, text='Input')
input_frame.pack(fill='x', **pad)
self._path_var = tk.StringVar()
ttk.Entry(input_frame, textvariable=self._path_var,
ttk.Entry(input_frame, textvariable=st.path_var,
state='readonly', width=60).pack(side='left', fill='x',
expand=True, padx=(6, 2), pady=4)
ttk.Button(input_frame, text='Browse…',
command=self._browse).pack(side='left', padx=(2, 6), pady=4)
command=lambda: self._browse(tab_id)).pack(side='left', padx=(2, 6), pady=4)
btn_frame = ttk.Frame(self._root)
btn_frame = ttk.Frame(parent)
btn_frame.pack(fill='x', **pad)
self._btn_generate = ttk.Button(btn_frame, text='Generate report',
command=self._on_generate)
self._btn_generate.pack(side='left', padx=(0, 4))
self._btn_rename = ttk.Button(btn_frame, text='Rename unprocessed (add lap #)',
command=self._on_rename)
self._btn_rename.pack(side='left', padx=4)
st.btn_generate = ttk.Button(btn_frame, text='Generate report',
command=lambda: self._on_generate(tab_id))
st.btn_generate.pack(side='left', padx=(0, 4))
st.btn_rename = ttk.Button(btn_frame, text='Rename unprocessed (add lap #)',
command=lambda: self._on_rename(tab_id))
st.btn_rename.pack(side='left', padx=4)
ttk.Button(btn_frame, text='Open races folder',
command=self._open_reports).pack(side='left', padx=4)
log_frame = ttk.LabelFrame(self._root, text='Log')
log_frame = ttk.LabelFrame(parent, text='Log')
log_frame.pack(fill='both', expand=True, **pad)
self._log = scrolledtext.ScrolledText(log_frame, state='disabled',
st.log_widget = scrolledtext.ScrolledText(log_frame, state='disabled',
wrap='word', height=16)
self._log.pack(fill='both', expand=True, padx=4, pady=4)
st.log_widget.pack(fill='both', expand=True, padx=4, pady=4)
def _browse(self) -> None:
mode = self._mode.get()
init = self._last_dir.get(mode, _RACES_DIR)
if mode == 'race':
# ── Browse ───────────────────────────────────────────────────────────────
def _browse(self, tab_id: str) -> None:
st = self._tabs[tab_id]
mode = st.mode.get()
init = st.last_dir.get(mode, _RACES_DIR)
if mode in ('race', 'profile'):
d = filedialog.askdirectory(title='Select race folder', initialdir=init)
if d:
self._selection = [d]
self._path_var.set(d)
self._last_dir[mode] = d
st.selection = [d]
st.path_var.set(d)
st.last_dir[mode] = d
elif mode == 'compare':
files = filedialog.askopenfilenames(
title='Select 2+ CSV files', initialdir=init,
filetypes=[('CSV', '*.csv')])
if files:
self._selection = list(files)
self._path_var.set(' | '.join(os.path.basename(f) for f in files))
self._last_dir[mode] = os.path.dirname(files[0])
st.selection = list(files)
st.path_var.set(' | '.join(os.path.basename(f) for f in files))
st.last_dir[mode] = os.path.dirname(files[0])
else:
f = filedialog.askopenfilename(
title='Select CSV file', initialdir=init,
filetypes=[('CSV', '*.csv')])
if f:
self._selection = [f]
self._path_var.set(f)
self._last_dir[mode] = os.path.dirname(f)
st.selection = [f]
st.path_var.set(f)
st.last_dir[mode] = os.path.dirname(f)
def _validate_selection(self) -> str | None:
mode = self._mode.get()
sel = self._selection
# ── Validation / busy state / logging ────────────────────────────────────
def _validate_selection(self, tab_id: str) -> str | None:
st = self._tabs[tab_id]
mode = st.mode.get()
sel = st.selection
if not sel:
return 'No file selected'
if mode in ('technique', 'setup') and len(sel) != 1:
return f'{mode} requires exactly 1 file'
if mode == 'compare' and len(sel) < 2:
return 'compare requires 2+ files'
if mode == 'race' and (len(sel) != 1 or not os.path.isdir(sel[0])):
return 'race requires a directory'
if mode in ('race', 'profile') and (len(sel) != 1 or not os.path.isdir(sel[0])):
return f'{mode} requires a directory'
return None
def _set_busy(self, busy: bool) -> None:
self._busy = busy
def _set_busy(self, tab_id: str, busy: bool) -> None:
st = self._tabs[tab_id]
st.busy = busy
state = 'disabled' if busy else 'normal'
self._btn_generate.configure(state=state)
self._btn_rename.configure(state=state)
st.btn_generate.configure(state=state)
st.btn_rename.configure(state=state)
def _log_append(self, text: str) -> None:
self._log.configure(state='normal')
self._log.insert('end', text + '\n')
self._log.see('end')
self._log.configure(state='disabled')
def _log_append(self, tab_id: str, text: str) -> None:
w = self._tabs[tab_id].log_widget
w.configure(state='normal')
w.insert('end', text + '\n')
w.see('end')
w.configure(state='disabled')
def _poll_log(self) -> None:
def _push(self, tab_id: str, msg: str) -> None:
self._tabs[tab_id].log_queue.put(msg)
def _poll_all_logs(self) -> None:
for tab_id, st in self._tabs.items():
while True:
try:
msg = self._log_queue.get_nowait()
msg = st.log_queue.get_nowait()
except queue.Empty:
break
if msg is None:
self._set_busy(False)
self._set_busy(tab_id, False)
else:
self._log_append(msg)
self._root.after(100, self._poll_log)
self._log_append(tab_id, msg)
self._root.after(100, self._poll_all_logs)
def _push(self, msg: str) -> None:
self._log_queue.put(msg)
# ── Generate ─────────────────────────────────────────────────────────────
def _on_generate(self) -> None:
err = self._validate_selection()
def _on_generate(self, tab_id: str) -> None:
err = self._validate_selection(tab_id)
if err:
self._log_append(f'✗ {err}')
self._log_append(tab_id, f'✗ {err}')
return
mode = self._mode.get()
sel = list(self._selection)
lang = self._lang.get()
self._set_busy(True)
threading.Thread(target=self._worker_generate, args=(mode, sel, lang),
st = self._tabs[tab_id]
mode = st.mode.get()
sel = list(st.selection)
lang = st.lang.get()
inc = st.include_prompt.get()
self._set_busy(tab_id, True)
threading.Thread(target=self._worker_generate,
args=(tab_id, mode, sel, lang, inc),
daemon=True).start()
def _worker_generate(self, mode: str, sel: list[str], lang: str) -> None:
def _worker_generate(self, tab_id: str, mode: str, sel: list[str],
lang: str, inc: bool) -> None:
try:
self._check_game_mismatch(tab_id, mode, sel)
if mode == 'technique':
lap = load_lap(sel[0])
out = report_path(sel[0], 'technique')
abs_path, tokens = report_technique.generate(lap, out, lang)
self._push(f'✓ {abs_path} (~{tokens} tokens)')
abs_path, tokens = report_technique.generate(lap, out, lang, inc)
self._push(tab_id, f'✓ {abs_path} (~{tokens} tokens)')
elif mode == 'setup':
lap = load_lap(sel[0])
out = report_path(sel[0], 'setup')
abs_path, tokens = report_setup.generate(lap, out, lang)
self._push(f'✓ {abs_path} (~{tokens} tokens)')
abs_path, tokens = report_setup.generate(lap, out, lang, inc)
self._push(tab_id, f'✓ {abs_path} (~{tokens} tokens)')
elif mode == 'compare':
laps = [load_lap(f) for f in sel]
stem_b = os.path.splitext(os.path.basename(sel[1]))[0]
out = report_path(sel[0], 'compare', stem_b)
abs_path, tokens = report_compare.generate(laps, out, lang)
self._push(f'✓ {abs_path} (~{tokens} tokens)')
abs_path, tokens = report_compare.generate(laps, out, lang, inc)
self._push(tab_id, f'✓ {abs_path} (~{tokens} tokens)')
elif mode == 'race':
elif mode in ('race', 'profile'):
race_dir = os.path.abspath(sel[0])
csv_files = sorted(
f for f in os.listdir(race_dir) if f.lower().endswith('.csv')
)
if not csv_files:
self._push(f'✗ No CSV files found in {race_dir}')
self._push(tab_id, f'✗ No CSV files found in {race_dir}')
return
laps = [load_lap(os.path.join(race_dir, f)) for f in csv_files]
out = _race_output_path(race_dir)
abs_path, tokens = report_race.generate(laps, out, lang)
self._push(f'✓ {abs_path} (~{tokens} tokens)')
dirname = os.path.basename(race_dir.rstrip('/\\'))
suffix = '_race.md' if mode == 'race' else '_profile.md'
out = os.path.join(race_dir, 'reports', dirname + suffix)
gen_mod = report_race if mode == 'race' else report_profile
abs_path, tokens = gen_mod.generate(laps, out, lang, inc)
self._push(tab_id, f'✓ {abs_path} (~{tokens} tokens)')
except Exception as e:
self._push(f'✗ {e}')
self._push(tab_id, f'✗ {e}')
finally:
self._log_queue.put(None)
self._tabs[tab_id].log_queue.put(None)
def _on_rename(self) -> None:
init = self._last_dir.get('rename', os.path.expanduser('~'))
def _check_game_mismatch(self, tab_id: str, mode: str, sel: list[str]) -> None:
"""Warn (but don't block) when the file's game doesn't match the active tab."""
try:
if mode in ('race', 'profile'):
race_dir = os.path.abspath(sel[0])
csv_files = sorted(
f for f in os.listdir(race_dir) if f.lower().endswith('.csv')
)
if not csv_files:
return
probe = os.path.join(race_dir, csv_files[0])
else:
probe = sel[0]
detected = game_of(load_lap(probe))
except Exception:
return # if probe fails, let the main worker surface the real error
tab_game = tab_id # 'f1' or 'acc'
if detected != tab_game:
tab_label = _TAB_GAME[tab_id]
file_label = _TAB_GAME.get(detected, detected.upper())
self._push(tab_id,
f'⚠ Selected file is {file_label}, but you are on the '
f'{tab_label} tab — proceeding anyway.')
# ── Rename ───────────────────────────────────────────────────────────────
def _on_rename(self, tab_id: str) -> None:
st = self._tabs[tab_id]
init = st.last_dir.get('rename', os.path.expanduser('~'))
d = filedialog.askdirectory(title='Select folder with CSV files', initialdir=init)
if not d:
return
self._last_dir['rename'] = d
self._set_busy(True)
threading.Thread(target=self._worker_rename, args=(d,), daemon=True).start()
st.last_dir['rename'] = d
self._set_busy(tab_id, True)
threading.Thread(target=self._worker_rename, args=(tab_id, d),
daemon=True).start()
def _worker_rename(self, d: str) -> None:
def _worker_rename(self, tab_id: str, d: str) -> None:
try:
results = rename_unprocessed([d], races_dir=_RACES_DIR)
if not results:
self._push('No CSV files found')
self._push(tab_id, 'No CSV files found')
for r in results:
if r.status == 'renamed':
new_display = os.path.relpath(r.new, _RACES_DIR) if r.new and r.new.startswith(_RACES_DIR) else r.new
self._push(f'renamed: {r.old} -> {new_display}')
new_display = (os.path.relpath(r.new, _RACES_DIR)
if r.new and r.new.startswith(_RACES_DIR) else r.new)
self._push(tab_id, f'renamed: {r.old} -> {new_display}')
elif r.status == 'skipped':
self._push(f'skipped: {r.old} ({r.detail})')
self._push(tab_id, f'skipped: {r.old} ({r.detail})')
else:
self._push(f'error: {r.old} ({r.detail})')
self._push(tab_id, f'error: {r.old} ({r.detail})')
except Exception as e:
self._push(f'✗ {e}')
self._push(tab_id, f'✗ {e}')
finally:
self._log_queue.put(None)
self._tabs[tab_id].log_queue.put(None)
# ── Misc ─────────────────────────────────────────────────────────────────
def _open_reports(self) -> None:
target = _RACES_DIR if os.path.isdir(_RACES_DIR) else _PROJECT_ROOT

View file

@ -4,19 +4,61 @@ from dataclasses import dataclass
from .parser import _parse_header_row, _parse_value_row
_SESSION_PAT = re.compile(r'^(FP[1-3]|P[1-3]|Q[1-3]|R|Race|Sprint)$', re.IGNORECASE)
# Canonical token pattern — every token we ever insert must be listed here so
# _is_processed returns True on a second run (idempotency guarantee).
_SESSION_PAT = re.compile(
r'^(FP[1-3]|P[1-3]|P|Q[1-3]|SQ[1-3]|Q|R|Race|Sprint|HL|HS)$',
re.IGNORECASE,
)
# Maps ACC full-word event values to compact tokens used in filenames.
# Prefix-match on lowercased value so "Practice 1" etc. also normalise.
# F1 short codes pass through unchanged (they won't match any prefix here).
_ACC_NORM: list[tuple[str, str]] = [
('practice', 'P'),
('qualifying', 'Q'),
# Superpole is ACC's single-lap qualifying format → Q folder makes sense.
('superpole', 'Q'),
('hotlap', 'HL'),
('hotstint', 'HS'),
('race', 'R'),
('sprint', 'Sprint'),
]
def _normalise_event(raw: str) -> str:
"""Return the canonical session token for a raw event string.
F1 short codes (P1, Q2, R, Sprint, FP1 …) are returned as-is.
ACC full-word values are mapped to compact tokens via prefix match.
"""
lo = raw.strip().lower()
for prefix, token in _ACC_NORM:
if lo.startswith(prefix):
return token
# Unknown / F1 code — pass through unchanged so F1 files still work.
return raw.strip()
def _game_folder(game_raw: str) -> str:
return 'ACC' if game_raw.upper().startswith('ACC') else 'F1 22'
def _session_folder(event: str) -> str:
# event is already the canonical token at call sites inside this module.
e = event.upper()
if re.match(r'^(FP[1-3]|P[1-3])$', e):
if re.match(r'^(FP[1-3]|P[1-3]|P)$', e):
return 'Practice'
if re.match(r'^(SQ[1-3]|Q[1-3])$', e):
if re.match(r'^(SQ[1-3]|Q[1-3]|Q)$', e):
return 'Qualifying'
if re.match(r'^SPRINT$', e):
return 'Sprint'
if re.match(r'^(R|RACE)$', e):
return 'Race'
if re.match(r'^HL$', e):
return 'Hotlap'
if re.match(r'^HS$', e):
return 'Hotstint'
return 'Other'
@ -85,8 +127,8 @@ def read_session_type(path: str) -> str:
return session.get('event', '').strip()
def read_metadata(path: str) -> tuple[int, str, str]:
"""Returns (lap_number, event_type, track_name)."""
def read_metadata(path: str) -> tuple[int, str, str, str]:
"""Returns (lap_number, event_type, track_name, game_raw)."""
with open(path, encoding='utf-8') as f:
lines = [f.readline() for _ in range(5)]
sess_keys = _parse_header_row(lines[1])
@ -99,6 +141,7 @@ def read_metadata(path: str) -> tuple[int, str, str]:
int(float(track['Lap'])),
session.get('event', '').strip(),
session.get('track', '').strip(),
session.get('Game', '').strip(),
)
@ -143,26 +186,29 @@ def rename_unprocessed(targets: list[str], races_dir: str | None = None) -> list
results.append(RenameResult(name, None, 'skipped', 'already processed'))
continue
try:
lap, session_type, _ = read_metadata(p)
lap, session_type, _, _game = read_metadata(p)
except Exception as e:
results.append(RenameResult(name, None, 'error', str(e)))
continue
if not session_type:
results.append(RenameResult(name, None, 'error', 'session type not found in CSV'))
continue
session_type = _normalise_event(session_type)
new_name = insert_tokens(name, session_type, lap)
dest = os.path.join(os.path.dirname(p), new_name)
else:
try:
lap, session_type, track_name = read_metadata(p)
lap, session_type, track_name, game_raw = read_metadata(p)
except Exception as e:
results.append(RenameResult(name, None, 'error', str(e)))
continue
if not session_type:
results.append(RenameResult(name, None, 'error', 'session type not found in CSV'))
continue
session_type = _normalise_event(session_type)
new_name = name if _is_processed(stem) else insert_tokens(name, session_type, lap)
dest_dir = os.path.join(races_dir, _safe_dirname(track_name), _session_folder(session_type))
dest_dir = os.path.join(races_dir, _game_folder(game_raw),
_safe_dirname(track_name), _session_folder(session_type))
dest = os.path.join(dest_dir, new_name)
if os.path.abspath(p) == os.path.abspath(dest):
results.append(RenameResult(name, None, 'skipped', 'already in place'))

View file

@ -4,6 +4,10 @@ import tempfile
from .parser import Lap, laptime_s, sector_times
def game_of(lap: Lap) -> str:
return 'acc' if lap.session.get('Game', '').upper().startswith('ACC') else 'f1'
def header_block(lap: Lap) -> str:
s = lap.session
t = lap.track
@ -103,13 +107,20 @@ def write_report(path: str, text: str) -> tuple[str, int]:
return os.path.abspath(path), tokens
def load_prompt(mode: str, lang: str) -> str:
def load_prompt(mode: str, lang: str, game: str = 'f1') -> str:
here = os.path.dirname(__file__)
p = os.path.normpath(os.path.join(here, '..', 'prompts', lang, f'{mode}.md'))
base = os.path.join(here, '..', 'prompts', lang)
# ACC gets its own prompt set, falling back to the F1 prompt when absent.
candidates = []
if game == 'acc':
candidates.append(os.path.normpath(os.path.join(base, 'acc', f'{mode}.md')))
candidates.append(os.path.normpath(os.path.join(base, f'{mode}.md')))
for p in candidates:
try:
with open(p, encoding='utf-8') as f:
return f.read().strip()
except FileNotFoundError:
continue
return ''

View file

@ -4,10 +4,15 @@ from .parser import Lap, laptime_s, sector_times
from .segments import detect_corners, Corner
from .resample import adaptive_points, sample_at
from .report_common import (
legend, md_table, write_report, token_estimate, load_prompt,
legend, md_table, write_report, token_estimate, load_prompt, game_of,
LEGEND_DIST, LEGEND_SPD, LEGEND_DT_SEG, LEGEND_DT_CUM,
)
# seconds gained per kg of fuel burned — track-dependent in reality, so report a range
# (rule of thumb across F1 22 circuits) instead of one false-precise number.
FUEL_S_PER_KG_LOW = 0.03
FUEL_S_PER_KG_HIGH = 0.05
def _interp_laptime(lap: Lap, dist_query: float) -> float:
dist = lap.ch.get('LapDistance', [])
@ -118,6 +123,78 @@ def _corner_table(laps: list[Lap], corners: list[Corner]) -> str:
return md_table(headers, rows)
def _fuel_start(lap: Lap) -> float | None:
raw = lap.track.get('Fuel at Start', lap.track.get('FuelAtStart', ''))
raw = raw.strip().split()[0] if raw.strip() else ''
try:
return float(raw)
except ValueError:
return None
def _seg_time(lap: Lap, c: Corner) -> float | None:
if not (lap.ch.get('LapDistance') and lap.ch.get('LapTime')):
return None
return _interp_laptime(lap, c.end_m) - _interp_laptime(lap, c.start_m)
def _vmin(lap: Lap, c: Corner) -> float | None:
d = lap.ch.get('LapDistance', [])
spd = lap.ch.get('Speed', [])
idxs = [i for i, v in enumerate(d) if c.start_m <= v <= c.end_m and i < len(spd)]
return min(spd[i] for i in idxs) if idxs else None
def _verdict(laps: list[Lap], corners: list[Corner]) -> str:
# deterministic comparison of each non-reference lap vs lap A (lap 1)
lap_a = laps[0]
lines: list[str] = []
for k in range(1, len(laps)):
lap_k = laps[k]
dt_total = laptime_s(lap_k) - laptime_s(lap_a)
fa, fk = _fuel_start(lap_a), _fuel_start(lap_k)
lines.append(f'### L{k + 1} vs L1')
lines.append(f'- Итоговая Δ laptime: **{dt_total:+.3f} с** (L{k + 1} − L1)')
if fa is not None and fk is not None:
d_fuel = fa - fk
fuel_lo = d_fuel * FUEL_S_PER_KG_LOW
fuel_hi = d_fuel * FUEL_S_PER_KG_HIGH
# residual = laptime delta minus fuel benefit; wider fuel benefit → lower residual
res_lo = dt_total - max(fuel_lo, fuel_hi)
res_hi = dt_total - min(fuel_lo, fuel_hi)
lines.append(
f'- Топливо: L1 {fa:.1f} кг / L{k + 1} {fk:.1f} кг → '
f'поправка ~{min(fuel_lo, fuel_hi):+.3f}…{max(fuel_lo, fuel_hi):+.3f} с '
f'(0.03–0.05 с/кг). '
f'**Остаток (не топливо): {res_lo:+.3f}…{res_hi:+.3f} с**'
)
deltas: list[tuple[float, int, float | None, float | None]] = []
for c in corners:
ta, tk = _seg_time(lap_a, c), _seg_time(lap_k, c)
if ta is None or tk is None:
continue
deltas.append((tk - ta, c.number, _vmin(lap_a, c), _vmin(lap_k, c)))
losses = sorted([d for d in deltas if d[0] > 0], key=lambda x: -x[0])[:5]
gains = sorted([d for d in deltas if d[0] < 0], key=lambda x: x[0])[:3]
if losses:
lines.append(f'- **Где L{k + 1} теряет** (топ |Δt seg|):')
for dt, num, va, vk in losses:
vm = f' | Vmin {va:.0f}→{vk:.0f}' if (va is not None and vk is not None) else ''
lines.append(f' - T{num}: {dt:+.3f} с{vm}')
if gains:
lines.append(f'- **Где L{k + 1} быстрее**:')
for dt, num, va, vk in gains:
vm = f' | Vmin {va:.0f}→{vk:.0f}' if (va is not None and vk is not None) else ''
lines.append(f' - T{num}: {dt:+.3f} с{vm}')
lines.append('')
return '\n'.join(lines).rstrip()
def _delta_trace(laps: list[Lap], corners: list[Corner]) -> tuple[str, list[float]]:
lap_a = laps[0]
dists = adaptive_points(lap_a, corners)
@ -153,12 +230,15 @@ def _delta_trace(laps: list[Lap], corners: list[Corner]) -> tuple[str, list[floa
return block, final_deltas
def generate(laps: list[Lap], out_path: str, lang: str = 'ru') -> tuple[str, int]:
def generate(laps: list[Lap], out_path: str, lang: str = 'ru', include_prompt: bool = True,
game: str | None = None) -> tuple[str, int]:
lap_a = laps[0]
g = game or game_of(lap_a)
corners = detect_corners(lap_a)
head_table = _header_table(laps)
corner_table = _corner_table(laps, corners)
verdict = _verdict(laps, corners)
delta_block, final_deltas = _delta_trace(laps, corners)
for k in range(1, len(laps)):
@ -169,6 +249,8 @@ def generate(laps: list[Lap], out_path: str, lang: str = 'ru') -> tuple[str, int
parts = [
'# Lap Comparison\n',
head_table,
'\n## Verdict (вычислено детерминированно)',
verdict,
'\n## Corners',
corner_table,
'\n## Delta trace',
@ -176,7 +258,8 @@ def generate(laps: list[Lap], out_path: str, lang: str = 'ru') -> tuple[str, int
'\n' + legend([LEGEND_DIST, LEGEND_SPD, LEGEND_DT_SEG, LEGEND_DT_CUM]),
]
text = '\n'.join(parts)
prompt = load_prompt('compare', lang)
if include_prompt:
prompt = load_prompt('compare', lang, g)
if prompt:
text += '\n\n---\n\n' + prompt
return write_report(out_path, text)

205
telemetry/report_profile.py Normal file
View file

@ -0,0 +1,205 @@
# Cheap full-field aggregate: scans every lap (corner tables only, no trace) to surface
# chronic driver tendencies and auto-pick laps worth a deep dive. Track-agnostic:
# corners are clustered by apex distance (not index) and tendencies are relative to the
# session, so it works on any track / compound without per-track calibration.
from .parser import Lap, laptime_s
from .segments import detect_corners, Corner, merge_gap_for
from .report_common import md_table, write_report, load_prompt
from .report_race import _dedup_laps, _sort_laps
# Corners are clustered across laps by apex distance. The gap reuses the detector's own
# track-adaptive merge gap (merge_gap_for), so the cluster count stays aligned with per-lap
# corner numbers (profile Tn == technique Tn) on chicanes and long circuits alike.
MIN_LAPS_PER_COMPOUND = 3
# chronic = relative outlier vs the rest of the session's corners, with a physics floor
# so a benign track never raises false alarms.
COAST_REL, COAST_FLOOR = 1.5, 35.0
SPIN_REL, SPIN_FLOOR = 1.7, 4.0
LOCK_REL, LOCK_FLOOR = 1.7, 5.0
TR_MARGIN, TR_FLOOR = 8.0, 100.0
def _median(vals: list[float]) -> float:
if not vals:
return 0.0
s = sorted(vals)
n = len(s)
mid = n // 2
return s[mid] if n % 2 else (s[mid - 1] + s[mid]) / 2.0
def _is_valid(lap: Lap) -> bool:
return lap.track.get('Valid', 'true').lower() != 'false'
def _is_pit(lap: Lap) -> bool:
return lap.track.get('Pitlap', 'false').lower() == 'true'
def _lap_no(lap: Lap) -> str:
return lap.track.get('Lap', '?')
def _clean_laps(laps: list[Lap]) -> list[Lap]:
return [l for l in laps if _is_valid(l) and not _is_pit(l) and laptime_s(l) > 0]
def _by_compound(laps: list[Lap]) -> list[tuple[str, list[Lap]]]:
groups: dict[str, list[Lap]] = {}
for l in laps:
groups.setdefault(l.track.get('Tyre', '?'), []).append(l)
return sorted(groups.items(), key=lambda kv: -len(kv[1]))
def _cluster_corners(per: list[tuple[Lap, list[Corner]]], gap: float) -> list[list[Corner]]:
# group corners across laps by apex location, robust to a lap detecting one extra/fewer
flat = sorted(
((c.apex_m, c) for _, cs in per for c in cs),
key=lambda x: x[0],
)
if not flat:
return []
clusters: list[list[Corner]] = [[flat[0][1]]]
last_apex = flat[0][0]
for apex, c in flat[1:]:
if apex - last_apex > gap:
clusters.append([c])
else:
clusters[-1].append(c)
last_apex = apex
min_support = max(2, len(per) // 2)
return [cl for cl in clusters if len(cl) >= min_support]
def _aggregate_compound(laps: list[Lap]) -> tuple[str, list[str]]:
per = [(l, detect_corners(l)) for l in laps]
clusters = _cluster_corners(per, merge_gap_for(laps[0]))
if not clusters:
return '', []
agg = []
for num, cl in enumerate(clusters, 1):
agg.append({
'num': num,
'vmin': _median([c.v_min for c in cl]),
'coast': _median([c.coast_m for c in cl]),
'lock': _median([c.lock_pct for c in cl]),
'spin': _median([c.spin_pct for c in cl]),
'tr': _median([c.rear_tyre_temp_peak for c in cl]),
'lock_freq': sum(1 for c in cl if c.lockup) / len(cl) * 100,
'spin_freq': sum(1 for c in cl if c.wheelspin) / len(cl) * 100,
})
med_coast = _median([a['coast'] for a in agg])
med_spin = _median([a['spin'] for a in agg])
med_lockmag = _median([abs(a['lock']) for a in agg])
med_tr = _median([a['tr'] for a in agg])
rows, chronic = [], []
for a in agg:
rows.append([
f"T{a['num']}", f"{a['vmin']:.0f}", f"{a['coast']:.0f}",
f"{a['lock']:.1f}", f"{a['spin']:.1f}",
f"{a['lock_freq']:.0f}", f"{a['spin_freq']:.0f}", f"{a['tr']:.0f}",
])
if a['coast'] >= max(COAST_REL * med_coast, COAST_FLOOR):
chronic.append(f"T{a['num']}: длинный медианный coast {a['coast']:.0f} м — осторожный переход тормоз→газ")
if a['spin'] >= max(SPIN_REL * med_spin, SPIN_FLOOR) or a['spin_freq'] >= 25:
chronic.append(f"T{a['num']}: склонность к пробуксовке на выходе (медиана spin {a['spin']:.1f}%) — ранний/резкий газ")
if abs(a['lock']) >= max(LOCK_REL * med_lockmag, LOCK_FLOOR) or a['lock_freq'] >= 25:
chronic.append(f"T{a['num']}: склонность к блокировке на входе (медиана lock {a['lock']:.1f}%) — поздняя/жёсткая педаль")
if a['tr'] >= med_tr + TR_MARGIN and a['tr'] >= TR_FLOOR:
chronic.append(f"T{a['num']}: высокая нагрузка на задние шины (медиана Tr {a['tr']:.0f}°C) — тепловой риск в длинном стинте")
table = md_table(
['T', 'Vmin', 'coast m', 'lock%', 'spin%', 'LOCK %laps', 'SPIN %laps', 'Tr peak °C'],
rows,
)
return table, chronic
def _recommend_laps(all_clean: list[Lap]) -> str:
def rear_peak(l: Lap) -> float:
rl = l.ch.get('TyreTemperatureRearLeft', [])
rr = l.ch.get('TyreTemperatureRearRight', [])
return max((max(rl) if rl else 0), (max(rr) if rr else 0))
def worst_corner(l: Lap, attr: str, sign: float) -> float:
cs = detect_corners(l)
return max((sign * getattr(c, attr) for c in cs), default=0.0)
picks = [
('быстрейший валидный', min(all_clean, key=laptime_s)),
('худший валидный', max(all_clean, key=laptime_s)),
('тепловой пик (задние)', max(all_clean, key=rear_peak)),
('макс. пробуксовка', max(all_clean, key=lambda l: worst_corner(l, 'spin_pct', 1.0))),
('макс. блокировка', max(all_clean, key=lambda l: worst_corner(l, 'lock_pct', -1.0))),
]
seen: dict[str, str] = {}
tyre: dict[str, str] = {}
for reason, lap in picks:
key = _lap_no(lap)
seen[key] = (seen.get(key, '') + '; ' + reason).lstrip('; ')
tyre[key] = lap.track.get('Tyre', '?')
rows = [[f'L{k}', tyre[k], seen[k]] for k in seen]
return md_table(['Круг', 'Состав', 'Почему интересен'], rows)
def generate(laps: list[Lap], out_path: str, lang: str = 'ru', include_prompt: bool = True) -> tuple[str, int]:
laps = _sort_laps(_dedup_laps(laps))
clean = _clean_laps(laps)
if not clean:
return write_report(out_path, '_Нет валидных кругов для профилирования._')
first = laps[0]
breakdown = {}
for l in clean:
t = l.track.get('Tyre', '?')
breakdown[t] = breakdown.get(t, 0) + 1
breakdown_str = ', '.join(f'{k}: {v}' for k, v in breakdown.items())
parts = [
f'**Track:** {first.session.get("track", "?")} ',
f'**Car:** {first.session.get("car", "?")} ',
f'**Валидных кругов:** {len(clean)} из {len(laps)} ',
f'**Составы (валидные круги):** {breakdown_str} ',
]
profiled = False
for comp, claps in _by_compound(clean):
if len(claps) < MIN_LAPS_PER_COMPOUND:
continue
table, chronic = _aggregate_compound(claps)
if not table:
continue
profiled = True
parts.append(f'\n## Тенденции по поворотам — состав {comp} ({len(claps)} кругов)')
parts.append(table)
parts.append(f'\n### Хронические паттерны — {comp} (авто-детект, относительно сессии)')
parts.append('\n'.join(f'- {c}' for c in chronic) if chronic
else '- Явных хронических паттернов не выявлено.')
if not profiled:
parts.append(f'\n_Недостаточно кругов на один состав для агрегации (нужно ≥{MIN_LAPS_PER_COMPOUND}).')
parts += [
'\n## Рекомендованные круги для детального разбора',
_recommend_laps(clean),
'\n## Легенда',
'- `coast m` = медианный накат без тормоза и полного газа (большой = осторожный переход)',
'- `LOCK %laps` / `SPIN %laps` = доля кругов с флагом блокировки / пробуксовки в этом повороте',
'- Повороты кластеризуются по дистанции апекса (а не по индексу) — устойчиво к разному числу детектируемых поворотов',
'- Хроника считается ОТНОСИТЕЛЬНО медианы поворотов сессии (адаптивно к трассе) с физическим порогом-полом',
]
text = '\n'.join(parts)
if include_prompt:
prompt = load_prompt('profile', lang)
if prompt:
text += '\n\n---\n\n' + prompt
return write_report(out_path, text)

View file

@ -1,5 +1,5 @@
from .parser import Lap, laptime_s, sector_times
from .report_common import md_table, write_report, load_prompt
from .report_common import md_table, write_report, load_prompt, game_of
def _fmt_laptime(s: float) -> str:
@ -23,6 +23,37 @@ def _fuel_start(lap: Lap) -> float:
return v[0] if v else 0.0
def _dedup_laps(laps: list[Lap]) -> list[Lap]:
def lap_num(lap: Lap) -> int:
try:
return int(lap.track.get('Lap', '0'))
except ValueError:
return 0
# Only dedup laps that carry a real positive lap number. Laps without one (export
# missing the field → all parse to 0) are kept verbatim, so a missing 'Lap' column
# can never silently collapse the whole race to a single lap.
numbered = [l for l in laps if lap_num(l) > 0]
unnumbered = [l for l in laps if lap_num(l) <= 0]
best: dict[int, Lap] = {}
for lap in numbered:
n = lap_num(lap)
cur = best.get(n)
if cur is None:
best[n] = lap
continue
lap_valid = lap.track.get('Valid', 'true').lower() != 'false'
cur_valid = cur.track.get('Valid', 'true').lower() != 'false'
if lap_valid != cur_valid:
if lap_valid:
best[n] = lap
continue
if laptime_s(lap) < laptime_s(cur):
best[n] = lap
return list(best.values()) + unnumbered
def _sort_laps(laps: list[Lap]) -> list[Lap]:
def lap_num(lap: Lap) -> int:
try:
@ -262,9 +293,12 @@ def _ers_table(laps: list[Lap]) -> str:
return md_table(['Lap', 'ERS MJ', 'MGU-K MJ', 'MGU-H MJ'], rows)
def generate(laps: list[Lap], out_path: str, lang: str = 'ru') -> tuple[str, int]:
def generate(laps: list[Lap], out_path: str, lang: str = 'ru', include_prompt: bool = True,
game: str | None = None) -> tuple[str, int]:
laps = _dedup_laps(laps)
laps = _sort_laps(laps)
stints, ages = _assign_stints(laps)
g = game or (game_of(laps[0]) if laps else 'f1')
parts = [
_header(laps),
@ -276,11 +310,12 @@ def generate(laps: list[Lap], out_path: str, lang: str = 'ru') -> tuple[str, int
_tyre_wear_table(laps),
'\n## Thermal summary per lap',
_thermal_table(laps),
'\n## ERS per lap',
_ers_table(laps),
]
if g != 'acc':
parts += ['\n## ERS per lap', _ers_table(laps)]
text = '\n'.join(parts)
prompt = load_prompt('race', lang)
if include_prompt:
prompt = load_prompt('race', lang, g)
if prompt:
text += '\n\n---\n\n' + prompt
return write_report(out_path, text)

View file

@ -2,7 +2,7 @@ from .parser import Lap
from .segments import detect_corners, Corner
from .segments import _indices_in_range
from .report_common import (
header_block, legend, md_table, write_report, load_prompt,
header_block, legend, md_table, write_report, load_prompt, game_of,
LEGEND_DIST, LEGEND_TIME, LEGEND_SPD, LEGEND_THR_BRK, LEGEND_STEER,
LEGEND_GEAR, LEGEND_DRS, LEGEND_GLAT, LEGEND_GLON, LEGEND_FUEL,
LEGEND_LOCK, LEGEND_SPIN, LEGEND_STEER_G_CAVEAT, LEGEND_SUSP,
@ -220,14 +220,23 @@ def _suspension_section(lap: Lap, corners: list[Corner]) -> str | None:
return rake_line + '\n\n' + roll_table
def generate(lap: Lap, out_path: str, lang: str = 'ru') -> tuple[str, int]:
def generate(lap: Lap, out_path: str, lang: str = 'ru', include_prompt: bool = True,
game: str | None = None) -> tuple[str, int]:
g = game or game_of(lap)
corners = detect_corners(lap)
susp = _suspension_section(lap, corners)
if g == 'acc':
setup_section = ('_ACC does not export car setup — analysis derived from telemetry below._'
if lang == 'en' else
'_ACC не экспортирует сетап — разбор баланса из телеметрии ниже._')
else:
setup_section = _full_setup_table(lap)
parts = [
header_block(lap),
'\n## Setup',
_full_setup_table(lap),
setup_section,
'\n## Corner balance',
_corner_balance(lap, corners),
'\n## Tyres',
@ -249,7 +258,8 @@ def generate(lap: Lap, out_path: str, lang: str = 'ru') -> tuple[str, int]:
'\n' + legend(legend_entries),
]
text = '\n'.join(parts)
prompt = load_prompt('setup', lang)
if include_prompt:
prompt = load_prompt('setup', lang, g)
if prompt:
text += '\n\n---\n\n' + prompt
return write_report(out_path, text)

View file

@ -3,7 +3,7 @@ from .segments import detect_corners, straights, Corner, Straight
from .resample import adaptive_points, sample_at, format_trace_row
from .report_common import (
header_block, setup_summary, legend, md_table, write_report, report_path,
load_prompt,
load_prompt, game_of,
LEGEND_DIST, LEGEND_TIME, LEGEND_SPD, LEGEND_THR_BRK, LEGEND_STEER,
LEGEND_GEAR, LEGEND_DRS, LEGEND_GLAT, LEGEND_GLON, LEGEND_FUEL, LEGEND_ERS,
LEGEND_LOCK, LEGEND_SPIN, LEGEND_TF, LEGEND_RPM, LEGEND_SLIP, LEGEND_YAW,
@ -56,7 +56,7 @@ def _straight_rows(ss: list[Straight]) -> list[list[str]]:
return rows
def _driving_cost(lap: Lap, corners: list[Corner]) -> str:
def _driving_cost(lap: Lap, corners: list[Corner], game: str = 'f1') -> str:
ch = lap.ch
fuel = ch.get('FuelRemaining', [])
fuel_used = (fuel[0] - fuel[-1]) if len(fuel) >= 2 else 0.0
@ -94,7 +94,12 @@ def _driving_cost(lap: Lap, corners: list[Corner]) -> str:
f'- Fuel used: {fuel_used:.2f} kg',
'- Tyre wear Δ:',
*wear_lines,
f'- ERS spent: {ers_mj:.3f} MJ | MGU-K harvested: {mguk_mj:.3f} MJ | MGU-H harvested: {mguh_mj:.3f} MJ',
]
if game != 'acc':
lines.append(
f'- ERS spent: {ers_mj:.3f} MJ | MGU-K harvested: {mguk_mj:.3f} MJ | MGU-H harvested: {mguh_mj:.3f} MJ'
)
lines += [
f'- Corners flagged LOCK: {lock_count} | SPIN: {spin_count}',
f'- Worst lock%: {worst_lock_str} | Worst spin%: {worst_spin_str}',
'- Max brake temps:',
@ -150,7 +155,9 @@ def _trace_block(lap: Lap, corners: list[Corner]) -> str:
return '```\n' + header + '\n' + '\n'.join(rows) + '\n```'
def generate(lap: Lap, out_path: str, lang: str = 'ru') -> tuple[str, int]:
def generate(lap: Lap, out_path: str, lang: str = 'ru', include_prompt: bool = True,
game: str | None = None) -> tuple[str, int]:
g = game or game_of(lap)
corners = detect_corners(lap)
ss = straights(corners, lap)
@ -163,15 +170,16 @@ def generate(lap: Lap, out_path: str, lang: str = 'ru') -> tuple[str, int]:
_straight_rows(ss),
)
parts = [
header_block(lap),
f'\n**Setup:** {setup_summary(lap)}',
parts = [header_block(lap)]
if g != 'acc':
parts.append(f'\n**Setup:** {setup_summary(lap)}')
parts += [
'\n## Corners',
corner_table,
'\n## Straights',
straight_table,
'\n## Driving cost (per lap)',
_driving_cost(lap, corners),
_driving_cost(lap, corners, g),
'\n## Trace',
_trace_block(lap, corners),
'\n' + legend([
@ -181,7 +189,8 @@ def generate(lap: Lap, out_path: str, lang: str = 'ru') -> tuple[str, int]:
]),
]
text = '\n'.join(parts)
prompt = load_prompt('technique', lang)
if include_prompt:
prompt = load_prompt('technique', lang, g)
if prompt:
text += '\n\n---\n\n' + prompt
return write_report(out_path, text)

View file

@ -4,7 +4,10 @@ from .parser import Lap
STEER_ON = 6.0
BRAKE_ON = 5.0
MERGE_GAP_M = 40.0
MERGE_GAP_M = 40.0 # anchored at Zandvoort (~4257 m): tracks scale around this
MERGE_GAP_REF_LEN = 4257.0
MERGE_GAP_MIN = 28.0
MERGE_GAP_MAX = 60.0
MIN_LEN_M = 15.0
BRAKE_MARK = 20.0
FULL_THROTTLE = 98.0
@ -12,6 +15,22 @@ LOCKUP_REL_PCT = -10.0
SPIN_REL_PCT = 8.0
def merge_gap_for(lap: Lap) -> float:
"""Corner-merge / cluster gap scaled to track length so chicanes and long
circuits self-tune instead of relying on a fixed metre value. Equals
MERGE_GAP_M at the reference length; clamped at the extremes."""
raw = lap.track.get('Tracklen', '')
try:
tracklen = float(raw)
except (TypeError, ValueError):
dist = lap.ch.get('LapDistance', [])
tracklen = (dist[-1] - dist[0]) if len(dist) >= 2 else 0.0
if tracklen <= 0:
return MERGE_GAP_M
g = MERGE_GAP_M * tracklen / MERGE_GAP_REF_LEN
return min(max(g, MERGE_GAP_MIN), MERGE_GAP_MAX)
@dataclass(frozen=True)
class Corner:
number: int
@ -93,9 +112,10 @@ def detect_corners(lap: Lap) -> list[Corner]:
if in_zone:
zones.append((z_start, n - 1))
merge_gap = merge_gap_for(lap)
merged: list[tuple[int, int]] = []
for z in zones:
if merged and (dist[z[0]] - dist[merged[-1][1]]) < MERGE_GAP_M:
if merged and (dist[z[0]] - dist[merged[-1][1]]) < merge_gap:
merged[-1] = (merged[-1][0], z[1])
else:
merged.append(list(z))

View file

@ -18,3 +18,13 @@ def truncated_path():
@pytest.fixture(scope="session")
def sample_lap(sample_path):
return load_lap(sample_path)
@pytest.fixture(scope="session")
def acc_path():
return os.path.join(_FIXTURES, "acc_lap.csv")
@pytest.fixture(scope="session")
def acc_lap(acc_path):
return load_lap(acc_path)

40
tests/fixtures/acc_lap.csv vendored Normal file
View file

@ -0,0 +1,40 @@
player,v8,???,0,2026061914565357
Game,version,date,track,car,event,laptime [s],S1 [s],S2 [s],S3 [s],S4+ [s]
ACC,1.1,2026-06-19 12:01:23,Hungaroring,CaR,Practice,123.702,43.470,45.950,34.282
TrackID [int],Tracklen [m],EventID [int],EventID2 [int],Weather [txt],TeamID [int],Tyre [txt],Valid [b],Pitlap [b],Online [b],Lap [int],LapsInRace [int],Fuel at Start [kg],MaxGears [int],MaxRevs [rpm],IdleRevs [rpm},TrackTemp [C],AmbientTemp [C],Windspeed [m/s],WindDirection [ ]
7,4381.009765625,0,50,Clear,57,DHA,false,false,false,1,20,60.019676208496094,6,6500.0,1950.0001,27.946645736694336,27.15789031982422,2.18314528465271,75.69888305664062
FWing,RWing,OnThrottle,OffThrottle,FrontCamber,RearCamber,FrontToe,RearToe,FrontSusp,RearSusp,FrontAntiRoll,RearAntiRoll,FrontSuspH,RearSuspH,BrakePressure,BrakeBias,FLTyrePressure,FRTyrePressure,RLTyrePressure,RRTyrePressure,Ballast,FuelLoad
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
LapDistance [m],TotalDistance [m],LapTime [s],Sector1Time [s],Sector2Time [s],Speed [km/h],EngineRevs [rpm],ThrottlePercentage [%],BrakePercentage [%],Steer [%],Clutch [%],Gear [int],DRS [0/1],CanUseDRS [0/1],KERSLevel [J],EngineTemperature [C],RacePosition [int],Sector [int],InPits [int],FuelRemaining [l],CurrentFlag [int],CurrentLapInvalid [int],X [m],Y [m],Z [m],GForceLatitudinal [G],GForceLongitudinal [G],GForceVertical [G],WheelSpeedRearLeft [km/h],WheelSpeedRearRight [km/h],WheelSpeedFrontLeft [km/h],WheelSpeedFrontRight [km/h],BrakeTemperatureRearLeft [C],BrakeTemperatureRearRight [C],BrakeTemperatureFrontLeft [C],BrakeTemperatureFrontRight [C],TyreTemperatureRearLeft [C],TyreTemperatureRearRight [C],TyreTemperatureFrontLeft [C],TyreTemperatureFrontRight [C],TyreWearRearLeft [%],TyreWearRearRight [%],TyreWearFrontLeft [%],TyreWearFrontRight [%],SuspensionPositionRearLeft [m],SuspensionPositionRearRight [m],SuspensionPositionFrontLeft [m],SuspensionPositionFrontRight [m],SuspensionVelocityRearLeft [m/s],SuspensionVelocityRearRight [m/s],SuspensionVelocityFrontLeft [m/s],SuspensionVelocityFrontRight [m/s],SuspensionAccelerationRearLeft [m/s^2],SuspensionAccelerationRearRight [m/s^2],SuspensionAccelerationFrontLeft [m/s^2],SuspensionAccelerationFrontRight [m/s^2],MGUKHarvested [J],MGUHHarvested [J],ERSSpent [J],ERSMode [int],FuelMixMode [int],Yaw [rad],Roll [rad],Pitch [rad],TyreCarcassTemperatureRearLeft [C],TyreCarcassTemperatureRearRight [C],TyreCarcassTemperatureFrontLeft [C],TyreCarcassTemperatureFrontRight [C],TyrePressureRearLeft [PSI],TyrePressureRearRight [PSI],TyrePressureFrontLeft [PSI],TyrePressureFrontRight [PSI],WheelSlipRearLeft [%],WheelSlipRearRight [%],WheelSlipFrontLeft [%],WheelSlipFrontRight [%],Torque [Nm],Handbrake [%],RearLeftInside [C],RearLeftMiddle [C],RearLeftOutside [C],RearRightInside [C],RearRightMiddle [C],RearRightOutside [C],FrontLeftInside [C],FrontLeftMiddle [C],FrontLeftOutside [C],FrontRightInside [C],FrontRightMiddle [C],FrontRightOutside [C],WoldSpeedX [km/h],WoldSpeedY [km/h],WoldSpeedZ [km/h],LocalAngularVelocityX [rad/s],LocalAngularVelocityY [rad/s],LocalAngularVelocityZ [rad/s],IcePower [W],MgukPower [W],FrontRideHeight [m],RearRideHeight [m],LoadRearLeft [N],LoadRearRight [N],LoadFrontLeft [N],LoadFrontRight [N],Extra1 [m],Extra2 [],Extra3 [],Extra4 []
11.52857,4036.4016,0.282,0.0,0.0,169.64093,5723.0,100.0,0.0,-0.010964928,0.0,4.0,0.0,0.0,0.0,84.686035,1.0,0.0,0.0,60.019676,0.0,1.0,-109.23591,24.842262,544.1296,-0.036001448,0.2483705,0.025862258,173.77263,173.87315,169.28203,169.32233,305.99573,304.57455,401.18625,396.96573,73.76696,69.08077,70.795746,65.185524,0.0,0.0,0.0,0.0,0.034967814,0.034488626,0.013857091,0.013722197,2.075326E-5,1.89149E-5,-7.246143E-6,-2.5508643E-6,-1.3962284E-7,-1.2725481E-7,4.8750273E-8,1.716159E-8,0.0,0.0,0.0,-1.0,1.0,-2.1652644,-0.03211098,0.0017947316,73.76696,69.08077,70.795746,65.185524,25.10461,24.530798,25.619375,25.080778,0.024355568,0.024948094,-0.0021156704,-0.0018781277,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.042892,0.0030937572,26.396013,-0.0044563767,-5.508619E-4,0.013827926,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
12.014709,4036.7551,0.29,0.0,0.0,169.72768,5726.0,100.0,0.0,0.0018527993,0.0,4.0,0.0,0.0,0.0,84.688995,1.0,0.0,0.0,60.0194,0.0,1.0,-109.5287,24.84228,543.93164,-0.023693023,0.24786447,0.012986496,174.02707,173.90872,169.38522,169.4204,305.94714,304.52625,401.0366,396.81787,73.76947,69.08246,70.79732,65.18582,0.0,0.0,0.0,0.0,0.034994584,0.03462096,0.0141735785,0.013794035,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-1.0,1.0,-2.1652737,-0.03226933,0.0018673544,73.76947,69.08246,70.79732,65.18582,25.10485,24.53096,25.619526,25.080807,0.025331153,0.024633894,-0.0020176482,-0.0018104152,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.061405,0.0047848364,26.411741,-0.009338445,-8.7213836E-4,0.017504098,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
12.338801,4037.1086,0.3,0.0,0.0,169.79353,5729.0,100.0,0.0,-0.005238485,0.0,4.0,0.0,0.0,0.0,84.691216,1.0,0.0,0.0,60.019196,0.0,1.0,-109.82159,24.84231,543.73364,-0.019859513,0.24973202,0.0029058661,174.1567,173.91837,169.62521,169.51479,305.89853,304.47787,400.88684,396.66995,73.771355,69.083725,70.79849,65.18604,0.0,0.0,0.0,0.0,0.034860697,0.034790497,0.014345023,0.013859233,-0.0059509445,0.016770637,0.027107334,0.0076130964,-0.33060804,0.931702,1.505963,0.4229498,0.0,0.0,0.0,-1.0,1.0,-2.1652846,-0.03241021,0.0019555911,73.771355,69.083725,70.79849,65.18604,25.105034,24.531082,25.61964,25.08083,0.025696889,0.024293179,-9.913363E-4,-0.0016416886,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.07583,0.0050230986,26.423271,-0.012692683,-0.0012003235,0.017881883,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
12.82494,4037.5803,0.307,0.0,0.0,169.86078,5732.0,100.0,0.0,-0.0020330295,0.0,4.0,0.0,0.0,0.0,84.693436,1.0,0.0,0.0,60.01899,0.0,1.0,-110.21227,24.842361,543.4695,-0.015500802,0.25483412,-0.035387367,174.24815,174.01012,169.7397,169.63368,305.89853,304.47787,400.88684,396.66995,73.77323,69.08499,70.799644,65.18625,0.0,0.0,0.0,0.0,0.034660157,0.034980144,0.014283132,0.01383878,-0.019672163,0.021128532,0.006444314,0.002632082,-1.157186,1.2428548,0.3790773,0.15482835,0.0,0.0,0.0,-1.0,1.0,-2.1652973,-0.03250376,0.0020536045,73.77323,69.08499,70.799644,65.18625,25.105213,24.531204,25.619751,25.080849,0.025829243,0.024427839,-7.1281136E-4,-0.0013369576,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.090744,0.0024308157,26.434696,-0.012943774,-0.0012463271,0.009102043,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
13.149033,4037.9343,0.315,0.0,0.0,169.9762,5737.0,100.0,0.0,0.009160281,0.0,4.0,0.0,0.0,0.0,84.697136,1.0,0.0,0.0,60.018642,0.0,1.0,-110.50542,24.842388,543.27124,-0.032200195,0.26522526,-0.046108786,173.96013,174.05067,169.72847,169.74197,305.8012,304.38113,400.5873,396.37408,73.77638,69.087105,70.80156,65.1866,0.0,0.0,0.0,0.0,0.03464579,0.035214636,0.013901675,0.013589289,-0.014327218,0.028275946,-0.029556515,-0.017996257,-0.95514786,1.885063,-1.9704343,-1.1997504,0.0,0.0,0.0,-1.0,1.0,-2.1653147,-0.032495715,0.0021680715,73.77638,69.087105,70.80156,65.1866,25.105516,24.531408,25.619934,25.080883,0.02343825,0.023970794,-0.0014574057,-0.0013779213,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.115627,-0.0038357505,26.455435,-0.005286799,-0.0010240518,-0.0044617346,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
13.4731245,4038.2883,0.325,0.0,0.0,170.04463,5738.0,100.0,0.0,0.016547244,0.0,4.0,0.0,0.0,0.0,84.699356,1.0,0.0,0.0,60.018436,0.0,1.0,-110.798676,24.842382,543.0729,-0.026762703,0.25481597,-0.0077182837,174.0757,174.09074,169.71399,169.76973,305.8012,304.38113,400.5873,396.37408,73.778275,69.08838,70.80272,65.186806,0.0,0.0,0.0,0.0,0.03487389,0.03536873,0.013778092,0.013468641,0.011874156,0.021588057,-0.028057748,-0.020563291,0.65967536,1.1993365,-1.5587639,-1.142405,0.0,0.0,0.0,-1.0,1.0,-2.165323,-0.03248412,0.0021698782,73.778275,69.08838,70.80272,65.186806,25.105698,24.53153,25.620045,25.080902,0.023705985,0.023794403,-0.0019443971,-0.0016166419,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.130116,-0.0042215604,26.467607,0.0018667248,-0.001168449,9.254145E-4,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
13.959263,4038.7605,0.332,0.0,0.0,170.1122,5739.0,100.0,0.0,0.0083989715,0.0,4.0,0.0,0.0,0.0,84.70158,1.0,0.0,0.0,60.018227,0.0,1.0,-111.18986,24.842344,542.80835,-0.027512636,0.25494838,0.019121999,174.11198,174.16544,169.7345,169.77376,305.75256,304.33273,400.4375,396.2261,73.78017,69.089645,70.80388,65.18702,0.0,0.0,0.0,0.0,0.03509519,0.035522744,0.013817208,0.013471926,0.026435317,0.018123975,-0.004968661,-0.00690373,1.5550187,1.0661162,-0.29227418,-0.40610176,0.0,0.0,0.0,-1.0,1.0,-2.1653323,-0.032541778,0.0021323483,73.78017,69.089645,70.80388,65.18702,25.105879,24.531652,25.620157,25.080923,0.023512656,0.023826884,-0.0022203415,-0.001989509,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.144463,-0.0028066994,26.4798,0.006246484,-0.0014940045,0.0111888945,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
14.283356,4039.1147,0.342,0.0,0.0,170.17862,5739.0,100.0,0.0,-0.0034686916,0.0,4.0,0.0,0.0,0.0,84.7038,1.0,0.0,0.0,60.01802,0.0,1.0,-111.48337,24.842316,542.6098,-0.028546177,0.24940713,0.07171454,174.1544,174.22723,169.84029,169.7952,305.70386,304.28433,400.28775,396.07816,73.78207,69.09092,70.805046,65.18724,0.0,0.0,0.0,0.0,0.035337783,0.035671897,0.014006146,0.013649582,0.027287751,0.017833402,0.013414934,0.010643593,1.6051619,1.0490236,0.78911376,0.6260937,0.0,0.0,0.0,-1.0,1.0,-2.165343,-0.032671917,0.0020802144,73.78207,69.09092,70.805046,65.18724,25.106062,24.531775,25.62027,25.080944,0.023362413,0.02379034,-0.001988125,-0.0022530768,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.158398,0.002200487,26.491964,0.0066807917,-0.001680956,0.019356037,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
14.769494,4039.5874,0.35,0.0,0.0,170.26718,5740.0,100.0,0.0,2.7964596E-4,0.0,4.0,0.0,0.0,0.0,84.70676,1.0,0.0,0.0,60.017742,0.0,1.0,-111.87488,24.84231,542.3449,-0.024774725,0.25113165,0.080288865,174.34059,174.34473,170.01518,169.89398,305.65518,304.23593,400.13797,395.93024,73.784584,69.092606,70.80659,65.187546,0.0,0.0,0.0,0.0,0.03564696,0.03586361,0.014187679,0.013973102,0.030653965,0.0189371,0.020581711,0.02784313,1.702998,1.0520611,1.1434284,1.5468405,0.0,0.0,0.0,-1.0,1.0,-2.1653576,-0.03284161,0.0020180475,73.784584,69.092606,70.80659,65.187546,25.106304,24.531937,25.620417,25.080975,0.023923587,0.023947949,-0.0014799545,-0.002191845,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.17719,0.009847768,26.507994,0.005680933,-0.00166586,0.014170732,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
15.253259,4039.9421,0.357,0.0,0.0,170.3318,5741.0,100.0,0.0,-0.0037863501,0.0,4.0,0.0,0.0,0.0,84.70898,1.0,0.0,0.0,60.017536,0.0,1.0,-112.16864,24.842356,542.1462,-0.01671688,0.24182548,0.051545005,174.62819,174.5305,170.05142,169.97345,305.6065,304.1875,399.98825,395.7823,73.78646,69.09387,70.80774,65.187775,0.0,0.0,0.0,0.0,0.035722785,0.035942208,0.014252585,0.014245591,0.025666753,0.01802072,0.016429275,0.039733946,1.7111169,1.2013813,1.095285,2.6489298,0.0,0.0,0.0,-1.0,1.0,-2.16537,-0.032910272,0.001991446,73.78646,69.09387,70.80774,65.187775,25.106483,24.532059,25.62053,25.080997,0.025223643,0.024650058,-0.0016460561,-0.0021038703,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.191162,0.013625955,26.519114,0.002056227,-0.0017836776,0.0065005226,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
15.567857,4040.2969,0.367,0.0,0.0,170.39563,5744.0,100.0,0.0,2.2442082E-4,0.0,4.0,0.0,0.0,0.0,84.7112,1.0,0.0,0.0,60.01733,0.0,1.0,-112.4625,24.84244,541.9473,-0.01776832,0.24116075,0.024250237,174.77617,174.67091,170.07419,170.066,305.6065,304.1875,399.98825,395.7823,73.78833,69.09513,70.8089,65.18801,0.0,0.0,0.0,0.0,0.035606816,0.03589045,0.01432358,0.014522852,-0.0023613959,0.0015786466,0.007994199,0.032338258,-0.13890564,0.09286156,0.470247,1.9022504,0.0,0.0,0.0,-1.0,1.0,-2.1653836,-0.03292371,0.0020090973,73.78833,69.09513,70.8089,65.18801,25.106663,24.532179,25.62064,25.081018,0.025708094,0.025090385,-0.0018864868,-0.0019345023,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.20509,0.015505659,26.53018,-0.004986443,-0.0016786046,-3.4433513E-4,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
16.039753,4040.7703,0.375,0.0,0.0,170.45953,5748.0,100.0,0.0,0.0040749414,0.0,4.0,0.0,0.0,0.0,84.71342,1.0,0.0,0.0,60.01712,0.0,1.0,-112.854485,24.842587,541.68207,-0.018202942,0.2413529,0.017172432,174.78044,174.73691,170.10362,170.1539,305.5578,304.1391,399.8385,395.6344,73.79021,69.09639,70.81005,65.18824,0.0,0.0,0.0,0.0,0.03545997,0.035773963,0.0144413635,0.014785069,-0.014600862,-0.00934696,0.010487676,0.029970996,-0.811159,-0.51927555,0.58264863,1.6650554,0.0,0.0,0.0,-1.0,1.0,-2.1653988,-0.032894265,0.002088995,73.79021,69.09639,70.81005,65.18824,25.106844,24.532299,25.62075,25.081041,0.025348512,0.025093224,-0.002087924,-0.0017930274,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.21901,0.016820235,26.541273,-0.013632805,-0.0016907271,-0.0055084135,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
16.354351,4041.1255,0.382,0.0,0.0,170.54517,5755.0,100.0,0.0,0.0012987609,0.0,4.0,0.0,0.0,0.0,84.71641,1.0,0.0,0.0,60.016846,0.0,1.0,-113.14859,24.84271,541.48303,-0.022501389,0.2451741,0.0393255,174.67075,174.71301,170.24464,170.33131,305.5091,304.09067,399.68878,395.48648,73.79271,69.09807,70.81159,65.18854,0.0,0.0,0.0,0.0,0.035401274,0.035641193,0.0146766985,0.015073612,-0.013702859,-0.01661703,0.023541227,0.03671733,-0.913524,-1.107802,1.5694151,2.4478219,0.0,0.0,0.0,-1.0,1.0,-2.1654234,-0.032835644,0.002294316,73.79271,69.09807,70.81159,65.18854,25.107084,24.532461,25.6209,25.08107,0.024190536,0.024438366,-0.0017621501,-0.0012539756,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.237606,0.02003613,26.556547,-0.02424377,-0.0018405571,-0.00504553,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
16.66895,4041.4807,0.392,0.0,0.0,170.63432,5761.0,100.0,0.0,-0.012933018,0.0,4.0,0.0,0.0,0.0,84.7194,1.0,0.0,0.0,60.016567,0.0,1.0,-113.4428,24.842848,541.28394,-0.02279238,0.25629854,0.0285197,174.57756,174.62965,170.44597,170.5077,305.46036,304.0422,399.539,395.33853,73.79522,69.099754,70.81311,65.18882,0.0,0.0,0.0,0.0,0.035602823,0.035672452,0.014721351,0.0151250595,0.00840316,-0.005971202,0.016469836,0.01999944,0.49430352,-0.3512472,0.9688139,1.1764377,0.0,0.0,0.0,-1.0,1.0,-2.1654518,-0.032781146,0.0025523542,73.79522,69.099754,70.81311,65.18882,25.107327,24.532623,25.621044,25.081099,0.023109358,0.023414625,-0.0011038778,-7.420546E-4,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.257008,0.022770366,26.57248,-0.025350533,-0.0020437962,-0.00586406,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
17.140846,4041.9546,0.4,0.0,0.0,170.70256,5763.0,100.0,0.0,-0.021631196,0.0,4.0,0.0,0.0,0.0,84.72164,1.0,0.0,0.0,60.016357,0.0,1.0,-113.83525,24.843058,541.0183,-0.01922873,0.25786906,0.008813665,174.71027,174.66154,170.55545,170.61969,305.4116,303.9937,399.3892,395.19055,73.797104,69.10103,70.81424,65.18902,0.0,0.0,0.0,0.0,0.035794284,0.035845757,0.014541981,0.014907652,0.021833926,0.011364619,-0.007484315,-0.009220041,1.2129959,0.63136774,-0.41579527,-0.5122245,0.0,0.0,0.0,-1.0,1.0,-2.165474,-0.032745425,0.0027118518,73.797104,69.10103,70.81424,65.18902,25.107508,24.532745,25.621153,25.081116,0.023477638,0.02319233,-8.6181273E-4,-4.8549226E-4,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.271946,0.0233458,26.584316,-0.01879117,-0.0023562151,-0.003961567,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
17.612743,4042.3103,0.41,0.0,0.0,170.79535,5764.0,100.0,0.0,-0.05187564,0.0,4.0,0.0,0.0,0.0,84.72463,1.0,0.0,0.0,60.01608,0.0,1.0,-114.129715,24.843231,540.819,-0.016898781,0.26488048,-0.03438637,174.85413,174.76385,170.60626,170.6618,305.36285,303.94522,399.23935,395.0425,73.79961,69.10272,70.81574,65.18927,0.0,0.0,0.0,0.0,0.03597383,0.036112472,0.014156679,0.014442531,0.020611411,0.024445562,-0.03137067,-0.03791823,1.1450784,1.3580867,-1.742815,-2.1065683,0.0,0.0,0.0,-1.0,1.0,-2.1655045,-0.032718446,0.0028147027,73.79961,69.10272,70.81574,65.18927,25.107746,24.53291,25.621298,25.08114,0.023764038,0.023235457,-0.0011070737,-7.818763E-4,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.29251,0.020403259,26.600136,-0.005004784,-0.002925523,-0.0024073077,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
18.08464,4042.7847,0.417,0.0,0.0,170.84209,5764.0,100.0,0.0,-0.05187564,0.0,4.0,0.0,0.0,0.0,84.72613,1.0,0.0,0.0,60.01594,0.0,1.0,-114.522514,24.843454,540.5531,-0.017355524,0.2645813,-0.028130485,174.8914,174.82014,170.62549,170.66968,305.36285,303.94522,399.23935,395.0425,73.80086,69.10358,70.81649,65.1894,0.0,0.0,0.0,0.0,0.03605872,0.036253184,0.013992093,0.014216232,0.01555506,0.023966327,-0.032346368,-0.040671732,0.9150035,1.409784,-1.9027275,-2.3924549,0.0,0.0,0.0,-1.0,1.0,-2.1655192,-0.032705612,0.0028123893,73.80086,69.10358,70.81649,65.1894,25.107868,24.532991,25.621368,25.081152,0.023702193,0.023285033,-0.001267824,-0.0010091378,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.3028,0.019186966,26.608093,0.0021187798,-0.002998144,-0.0026582738,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
18.399239,4043.1406,0.427,0.0,0.0,170.9358,5763.0,100.0,0.0,-0.058447283,0.0,4.0,0.0,0.0,0.0,84.72912,1.0,0.0,0.0,60.015663,0.0,1.0,-114.817245,24.843603,540.3536,-0.017351182,0.26599306,-0.019857155,174.96233,174.9395,170.75694,170.68292,305.31406,303.8967,399.08948,394.89447,73.80337,69.10527,70.81798,65.18965,0.0,0.0,0.0,0.0,0.036252815,0.036529295,0.013673156,0.01382406,0.016410999,0.024518983,-0.028442537,-0.036380693,0.9653529,1.4422932,-1.6730905,-2.1400409,0.0,0.0,0.0,-1.0,1.0,-2.1655464,-0.032651495,0.00270092,73.80337,69.10527,70.81798,65.18965,25.108109,24.533154,25.621513,25.081179,0.023555756,0.023422193,-0.0010463779,-0.001479471,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.323437,0.017381918,26.624083,0.01657727,-0.0030503247,-0.008598872,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
18.871134,4043.6155,0.435,0.0,0.0,171.02904,5762.0,100.0,0.0,-0.05161566,0.0,4.0,0.0,0.0,0.0,84.73211,1.0,0.0,0.0,60.015385,0.0,1.0,-115.2104,24.843784,540.0874,-0.019829765,0.2624005,-0.0019270547,175.03053,175.09914,170.74245,170.69574,305.26523,303.84818,398.93958,394.7464,73.80588,69.106964,70.819466,65.18992,0.0,0.0,0.0,0.0,0.036502145,0.036762293,0.013357432,0.013613336,0.024634724,0.028283853,-0.03525894,-0.03349424,1.3685958,1.5713252,-1.9588301,-1.8607912,0.0,0.0,0.0,-1.0,1.0,-2.165567,-0.03249422,0.0024466952,73.80588,69.106964,70.819466,65.18992,25.10835,24.533318,25.621655,25.081203,0.023396553,0.02379777,-0.0016757208,-0.0019487994,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.34369,0.01690556,26.640028,0.030272061,-0.0027883833,-0.018927325,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
19.185734,4043.9717,0.442,0.0,0.0,171.09583,5763.0,100.0,0.0,-0.044326268,0.0,4.0,0.0,0.0,0.0,84.73435,1.0,0.0,0.0,60.01518,0.0,1.0,-115.5054,24.84391,539.88763,-0.01922547,0.24736242,0.008626948,175.27032,175.32149,170.73315,170.74261,305.21643,303.79965,398.7897,394.59833,73.807755,69.10822,70.820595,65.190125,0.0,0.0,0.0,0.0,0.036639597,0.036803745,0.013291526,0.013668455,0.025785467,0.018296639,-0.025441995,-0.01037363,1.7190311,1.2197759,-1.696133,-0.6915753,0.0,0.0,0.0,-1.0,1.0,-2.1655786,-0.03235155,0.002190889,73.807755,69.10822,70.820595,65.190125,25.10853,24.533438,25.621765,25.081223,0.024398552,0.024697684,-0.002119682,-0.0020644295,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.35799,0.017505331,26.651459,0.035164107,-0.0026199582,-0.018351326,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
19.657629,4044.3281,0.452,0.0,0.0,171.18163,5767.0,100.0,0.0,-0.042977434,0.0,4.0,0.0,0.0,0.0,84.73734,1.0,0.0,0.0,60.0149,0.0,1.0,-115.80052,24.844038,539.6878,-0.021774048,0.23947896,-0.0031411645,175.5444,175.60716,170.78072,170.87808,305.16763,303.7511,398.6399,394.45035,73.810234,69.10989,70.822105,65.1904,0.0,0.0,0.0,0.0,0.036588937,0.0365657,0.013468195,0.013896169,0.005105401,-0.011564396,0.006515478,0.016637256,0.3003177,-0.68025863,0.3832634,0.97866213,0.0,0.0,0.0,-1.0,1.0,-2.165594,-0.032213278,0.0018545655,73.810234,69.10989,70.822105,65.1904,25.108768,24.5336,25.62191,25.081251,0.02548629,0.02585287,-0.0023420742,-0.0017732137,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.376392,0.0173921,26.666403,0.031326007,-0.002644165,-0.008772555,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
20.129528,4044.8035,0.46,0.0,0.0,171.24388,5771.0,100.0,0.0,-0.047827054,0.0,4.0,0.0,0.0,0.0,84.739586,1.0,0.0,0.0,60.01469,0.0,1.0,-116.19417,24.844215,539.42126,-0.022482496,0.2338695,-0.03322093,175.69981,175.77182,170.86798,170.89658,305.1188,303.70258,398.4901,394.3024,73.812096,69.11114,70.82324,65.190605,0.0,0.0,0.0,0.0,0.036385402,0.03626933,0.013675446,0.014058191,-0.014121954,-0.029689737,0.02132889,0.021652007,-0.784553,-1.6494298,1.1849384,1.2028893,0.0,0.0,0.0,-1.0,1.0,-2.1656082,-0.032209214,0.0016591968,73.812096,69.11114,70.82324,65.190605,25.108948,24.533718,25.622019,25.08127,0.026021011,0.026441412,-0.002195113,-0.0020281922,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.38975,0.01505751,26.677374,0.022958325,-0.0027453755,0.0028090784,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
20.447374,4045.1602,0.47,0.0,0.0,171.3059,5778.0,100.0,0.0,-0.062244527,0.0,4.0,0.0,0.0,0.0,84.74183,1.0,0.0,0.0,60.01448,0.0,1.0,-116.489525,24.844341,539.2212,-0.02323555,0.23502758,-0.034066625,175.73271,175.83405,170.9717,170.9541,305.06998,303.65402,398.34036,394.15442,73.81395,69.11238,70.82438,65.19081,0.0,0.0,0.0,0.0,0.036146622,0.035968594,0.013882383,0.0142436335,-0.02457305,-0.03317247,0.023010446,0.019303575,-1.3651695,-1.8429149,1.2783581,1.0724208,0.0,0.0,0.0,-1.0,1.0,-2.1656253,-0.03225248,0.001537294,73.81395,69.11238,70.82438,65.19081,25.109125,24.533836,25.622128,25.08129,0.025841586,0.026433097,-0.0019509464,-0.0020535914,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.40316,0.0125868805,26.688421,0.012787492,-0.0027570797,0.0064510647,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
20.925768,4045.636,0.477,0.0,0.0,171.36821,5784.0,100.0,0.0,-0.06566952,0.0,4.0,0.0,0.0,0.0,84.744095,1.0,0.0,0.0,60.01427,0.0,1.0,-116.88349,24.844479,538.9544,-0.020693334,0.23424439,-0.032882582,175.83826,175.87889,171.07758,171.0785,305.06998,303.65402,398.34036,394.15442,73.8158,69.113625,70.8255,65.191025,0.0,0.0,0.0,0.0,0.03591611,0.03568515,0.014066943,0.014416676,-0.027605496,-0.034363393,0.023029251,0.021087334,-1.6238527,-2.0213761,1.3546618,1.2404314,0.0,0.0,0.0,-1.0,1.0,-2.1656444,-0.032297246,0.0014890591,73.8158,69.113625,70.8255,65.191025,25.109304,24.533957,25.622236,25.081312,0.026084458,0.026321536,-0.001695974,-0.001690497,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.416653,0.009974838,26.699299,0.0031771893,-0.0027551234,0.0058646416,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
21.244698,4045.993,0.485,0.0,0.0,171.4524,5792.0,100.0,0.0,-0.073485024,0.0,4.0,0.0,0.0,0.0,84.747116,1.0,0.0,0.0,60.013992,0.0,1.0,-117.17908,24.84456,538.75415,-0.014153696,0.24148393,-0.06599551,175.97643,175.8869,171.22888,171.17216,305.02115,303.60547,398.19058,394.00647,73.81827,69.11529,70.826996,65.19129,0.0,0.0,0.0,0.0,0.035547975,0.03535814,0.014167956,0.014536584,-0.03990978,-0.040697057,0.01903822,0.01953002,-2.660652,-2.7131371,1.2692147,1.3020014,0.0,0.0,0.0,-1.0,1.0,-2.1656735,-0.032350145,0.0015241038,73.81827,69.11529,70.826996,65.19129,25.10954,24.534117,25.62238,25.081337,0.026386462,0.025864357,-0.0013036191,-0.0016344176,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.43546,0.003714017,26.713604,-0.0072334763,-0.0028177002,0.0038037421,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
21.723093,4046.35,0.495,0.0,0.0,171.518,5796.0,100.0,0.0,-0.07679934,0.0,4.0,0.0,0.0,0.0,84.74938,1.0,0.0,0.0,60.013783,0.0,1.0,-117.47478,24.844614,538.5539,-0.013632373,0.25245753,-0.06559541,175.91951,175.82158,171.30203,171.30792,304.9723,303.5569,398.04077,393.8585,73.82012,69.11654,70.8281,65.19148,0.0,0.0,0.0,0.0,0.03531124,0.03522051,0.0140859345,0.01452019,-0.033603773,-0.025813363,0.001055085,0.005750762,-1.8668762,-1.4340757,0.058615834,0.31948677,0.0,0.0,0.0,-1.0,1.0,-2.165697,-0.032351553,0.001600171,73.82012,69.11654,70.8281,65.19148,25.109718,24.534237,25.622486,25.081356,0.025662057,0.02509104,-0.0012592374,-0.0012248752,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.45033,-0.0011681489,26.724682,-0.011095868,-0.0028733583,-0.0014085505,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
22.201488,4046.8264,0.502,0.0,0.0,171.6084,5798.0,100.0,0.0,-0.083627366,0.0,4.0,0.0,0.0,0.0,84.7524,1.0,0.0,0.0,60.013504,0.0,1.0,-117.8692,24.844625,538.2867,-0.013671633,0.25631142,-0.057238538,175.86954,175.81245,171.36868,171.37608,304.92343,303.5083,397.8909,393.71042,73.8226,69.11822,70.829575,65.191734,0.0,0.0,0.0,0.0,0.035153784,0.035233572,0.013865768,0.014293048,-0.023187742,-0.0073274267,-0.01777577,-0.014325659,-1.3639848,-0.4310251,-1.0456336,-0.8426858,0.0,0.0,0.0,-1.0,1.0,-2.16573,-0.032319028,0.0017003558,73.8226,69.11822,70.829575,65.191734,25.109957,24.534399,25.622627,25.081379,0.024830593,0.024497962,-0.0013969055,-0.0013537349,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.47048,-0.00694992,26.739777,-0.008831456,-0.003015354,-0.004377713,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
22.520418,4047.1838,0.51,0.0,0.0,171.67668,5797.0,100.0,0.0,-0.08616711,0.0,4.0,0.0,0.0,0.0,84.75467,1.0,0.0,0.0,60.013294,0.0,1.0,-118.16515,24.844593,538.0862,-0.019227957,0.2582171,-0.044518888,175.88156,175.88544,171.37125,171.43143,304.8745,303.45966,397.741,393.56235,73.82446,69.11948,70.83068,65.191925,0.0,0.0,0.0,0.0,0.035166744,0.035313994,0.013727826,0.014096176,-0.009633104,0.0062321625,-0.023873895,-0.028267564,-0.64220697,0.41547748,-1.591593,-1.8845043,0.0,0.0,0.0,-1.0,1.0,-2.1657543,-0.032279488,0.0017462259,73.82446,69.11948,70.83068,65.191925,25.110136,24.53452,25.622734,25.081398,0.024492983,0.024515623,-0.0017791511,-0.0014285297,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.48551,-0.010167306,26.75147,-0.004878632,-0.0030249038,-0.005207439,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
22.839348,4047.5415,0.517,0.0,0.0,171.74548,5795.0,100.0,0.0,-0.08860586,0.0,4.0,0.0,0.0,0.0,84.756935,1.0,0.0,0.0,60.013084,0.0,1.0,-118.46122,24.844532,537.8856,-0.016487962,0.26103613,-0.022542765,175.92683,175.91576,171.46367,171.48314,304.82562,303.41104,397.5911,393.41428,73.826324,69.120735,70.83179,65.19211,0.0,0.0,0.0,0.0,0.03527517,0.03543609,0.013676049,0.01391935,0.008092324,0.013501197,-0.012647919,-0.02491319,0.53948826,0.9000798,-0.8431946,-1.6608794,0.0,0.0,0.0,-1.0,1.0,-2.165778,-0.032253243,0.0017618624,73.826324,69.120735,70.83179,65.19211,25.110315,24.534641,25.62284,25.081415,0.024346262,0.024281703,-0.0016409023,-0.0015274936,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.500732,-0.011831859,26.763191,-6.815667E-4,-0.0031339019,-0.00269921,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
23.317741,4047.8992,0.527,0.0,0.0,171.83765,5792.0,100.0,0.0,-0.09496216,0.0,4.0,0.0,0.0,0.0,84.75996,1.0,0.0,0.0,60.012806,0.0,1.0,-118.7574,24.844452,537.68494,-0.016842874,0.26147953,-0.011131329,176.03027,176.0295,171.57098,171.57745,304.77667,303.36237,397.44116,393.26617,73.828804,69.12241,70.83325,65.19235,0.0,0.0,0.0,0.0,0.03545368,0.03564789,0.013607756,0.013733252,0.016878633,0.019641045,-0.007062931,-0.021348488,0.99286073,1.1553556,-0.41546655,-1.2557933,0.0,0.0,0.0,-1.0,1.0,-2.1658087,-0.032234173,0.0017262091,73.828804,69.12241,70.83325,65.19235,25.110554,24.534801,25.622982,25.08144,0.024398785,0.0243943,-0.0015518327,-0.001514221,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.520996,-0.013003436,26.778904,0.005979049,-0.0031720053,-0.0017585277,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
23.796137,4048.3765,0.535,0.0,0.0,171.88348,5792.0,100.0,0.0,-0.09539544,0.0,4.0,0.0,0.0,0.0,84.76147,1.0,0.0,0.0,60.012665,0.0,1.0,-119.15249,24.844328,537.41724,-0.018743541,0.25865552,-0.018018097,176.11974,176.11406,171.6248,171.62872,304.77667,303.36237,397.44116,393.26617,73.83004,69.12325,70.833984,65.192474,0.0,0.0,0.0,0.0,0.035528727,0.035723917,0.013566235,0.013642805,0.014086564,0.015990395,-0.0061007836,-0.015363615,0.7825869,0.88835526,-0.33893242,-0.85353416,0.0,0.0,0.0,-1.0,1.0,-2.165823,-0.032226935,0.0016839136,73.83004,69.12325,70.833984,65.192474,25.110672,24.534883,25.62305,25.081451,0.024646044,0.024612974,-0.0015050384,-0.0014821178,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.530987,-0.014075285,26.786732,0.009123739,-0.0031545938,-0.001318757,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
24.115067,4048.7346,0.542,0.0,0.0,171.97446,5793.0,100.0,0.0,-0.101628795,0.0,4.0,0.0,0.0,0.0,84.76449,1.0,0.0,0.0,60.012386,0.0,1.0,-119.44893,24.844227,537.2164,-0.016709078,0.25675243,-0.04038965,176.28296,176.25171,171.69395,171.72241,304.72772,303.3137,397.29123,393.11807,73.83251,69.12493,70.83545,65.19272,0.0,0.0,0.0,0.0,0.03555339,0.035814002,0.01346909,0.013442952,0.00664716,0.011074047,-0.009244432,-0.019353317,0.443144,0.73826975,-0.61629546,-1.2902211,0.0,0.0,0.0,-1.0,1.0,-2.1658506,-0.032227688,0.0015559354,73.83251,69.12493,70.83545,65.19272,25.11091,24.535046,25.623194,25.081476,0.025053147,0.0248715,-0.001631055,-0.0014655663,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.550945,-0.017810632,26.80214,0.014740507,-0.0031819611,0.0013267533,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
1 player,v8,???,0,2026061914565357
2 Game,version,date,track,car,event,laptime [s],S1 [s],S2 [s],S3 [s],S4+ [s]
3 ACC,1.1,2026-06-19 12:01:23,Hungaroring,CaR,Practice,123.702,43.470,45.950,34.282
4 TrackID [int],Tracklen [m],EventID [int],EventID2 [int],Weather [txt],TeamID [int],Tyre [txt],Valid [b],Pitlap [b],Online [b],Lap [int],LapsInRace [int],Fuel at Start [kg],MaxGears [int],MaxRevs [rpm],IdleRevs [rpm},TrackTemp [C],AmbientTemp [C],Windspeed [m/s],WindDirection [ ]
5 7,4381.009765625,0,50,Clear,57,DHA,false,false,false,1,20,60.019676208496094,6,6500.0,1950.0001,27.946645736694336,27.15789031982422,2.18314528465271,75.69888305664062
6 FWing,RWing,OnThrottle,OffThrottle,FrontCamber,RearCamber,FrontToe,RearToe,FrontSusp,RearSusp,FrontAntiRoll,RearAntiRoll,FrontSuspH,RearSuspH,BrakePressure,BrakeBias,FLTyrePressure,FRTyrePressure,RLTyrePressure,RRTyrePressure,Ballast,FuelLoad
7 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
8 LapDistance [m],TotalDistance [m],LapTime [s],Sector1Time [s],Sector2Time [s],Speed [km/h],EngineRevs [rpm],ThrottlePercentage [%],BrakePercentage [%],Steer [%],Clutch [%],Gear [int],DRS [0/1],CanUseDRS [0/1],KERSLevel [J],EngineTemperature [C],RacePosition [int],Sector [int],InPits [int],FuelRemaining [l],CurrentFlag [int],CurrentLapInvalid [int],X [m],Y [m],Z [m],GForceLatitudinal [G],GForceLongitudinal [G],GForceVertical [G],WheelSpeedRearLeft [km/h],WheelSpeedRearRight [km/h],WheelSpeedFrontLeft [km/h],WheelSpeedFrontRight [km/h],BrakeTemperatureRearLeft [C],BrakeTemperatureRearRight [C],BrakeTemperatureFrontLeft [C],BrakeTemperatureFrontRight [C],TyreTemperatureRearLeft [C],TyreTemperatureRearRight [C],TyreTemperatureFrontLeft [C],TyreTemperatureFrontRight [C],TyreWearRearLeft [%],TyreWearRearRight [%],TyreWearFrontLeft [%],TyreWearFrontRight [%],SuspensionPositionRearLeft [m],SuspensionPositionRearRight [m],SuspensionPositionFrontLeft [m],SuspensionPositionFrontRight [m],SuspensionVelocityRearLeft [m/s],SuspensionVelocityRearRight [m/s],SuspensionVelocityFrontLeft [m/s],SuspensionVelocityFrontRight [m/s],SuspensionAccelerationRearLeft [m/s^2],SuspensionAccelerationRearRight [m/s^2],SuspensionAccelerationFrontLeft [m/s^2],SuspensionAccelerationFrontRight [m/s^2],MGUKHarvested [J],MGUHHarvested [J],ERSSpent [J],ERSMode [int],FuelMixMode [int],Yaw [rad],Roll [rad],Pitch [rad],TyreCarcassTemperatureRearLeft [C],TyreCarcassTemperatureRearRight [C],TyreCarcassTemperatureFrontLeft [C],TyreCarcassTemperatureFrontRight [C],TyrePressureRearLeft [PSI],TyrePressureRearRight [PSI],TyrePressureFrontLeft [PSI],TyrePressureFrontRight [PSI],WheelSlipRearLeft [%],WheelSlipRearRight [%],WheelSlipFrontLeft [%],WheelSlipFrontRight [%],Torque [Nm],Handbrake [%],RearLeftInside [C],RearLeftMiddle [C],RearLeftOutside [C],RearRightInside [C],RearRightMiddle [C],RearRightOutside [C],FrontLeftInside [C],FrontLeftMiddle [C],FrontLeftOutside [C],FrontRightInside [C],FrontRightMiddle [C],FrontRightOutside [C],WoldSpeedX [km/h],WoldSpeedY [km/h],WoldSpeedZ [km/h],LocalAngularVelocityX [rad/s],LocalAngularVelocityY [rad/s],LocalAngularVelocityZ [rad/s],IcePower [W],MgukPower [W],FrontRideHeight [m],RearRideHeight [m],LoadRearLeft [N],LoadRearRight [N],LoadFrontLeft [N],LoadFrontRight [N],Extra1 [m],Extra2 [],Extra3 [],Extra4 []
9 11.52857,4036.4016,0.282,0.0,0.0,169.64093,5723.0,100.0,0.0,-0.010964928,0.0,4.0,0.0,0.0,0.0,84.686035,1.0,0.0,0.0,60.019676,0.0,1.0,-109.23591,24.842262,544.1296,-0.036001448,0.2483705,0.025862258,173.77263,173.87315,169.28203,169.32233,305.99573,304.57455,401.18625,396.96573,73.76696,69.08077,70.795746,65.185524,0.0,0.0,0.0,0.0,0.034967814,0.034488626,0.013857091,0.013722197,2.075326E-5,1.89149E-5,-7.246143E-6,-2.5508643E-6,-1.3962284E-7,-1.2725481E-7,4.8750273E-8,1.716159E-8,0.0,0.0,0.0,-1.0,1.0,-2.1652644,-0.03211098,0.0017947316,73.76696,69.08077,70.795746,65.185524,25.10461,24.530798,25.619375,25.080778,0.024355568,0.024948094,-0.0021156704,-0.0018781277,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.042892,0.0030937572,26.396013,-0.0044563767,-5.508619E-4,0.013827926,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
10 12.014709,4036.7551,0.29,0.0,0.0,169.72768,5726.0,100.0,0.0,0.0018527993,0.0,4.0,0.0,0.0,0.0,84.688995,1.0,0.0,0.0,60.0194,0.0,1.0,-109.5287,24.84228,543.93164,-0.023693023,0.24786447,0.012986496,174.02707,173.90872,169.38522,169.4204,305.94714,304.52625,401.0366,396.81787,73.76947,69.08246,70.79732,65.18582,0.0,0.0,0.0,0.0,0.034994584,0.03462096,0.0141735785,0.013794035,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-1.0,1.0,-2.1652737,-0.03226933,0.0018673544,73.76947,69.08246,70.79732,65.18582,25.10485,24.53096,25.619526,25.080807,0.025331153,0.024633894,-0.0020176482,-0.0018104152,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.061405,0.0047848364,26.411741,-0.009338445,-8.7213836E-4,0.017504098,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
11 12.338801,4037.1086,0.3,0.0,0.0,169.79353,5729.0,100.0,0.0,-0.005238485,0.0,4.0,0.0,0.0,0.0,84.691216,1.0,0.0,0.0,60.019196,0.0,1.0,-109.82159,24.84231,543.73364,-0.019859513,0.24973202,0.0029058661,174.1567,173.91837,169.62521,169.51479,305.89853,304.47787,400.88684,396.66995,73.771355,69.083725,70.79849,65.18604,0.0,0.0,0.0,0.0,0.034860697,0.034790497,0.014345023,0.013859233,-0.0059509445,0.016770637,0.027107334,0.0076130964,-0.33060804,0.931702,1.505963,0.4229498,0.0,0.0,0.0,-1.0,1.0,-2.1652846,-0.03241021,0.0019555911,73.771355,69.083725,70.79849,65.18604,25.105034,24.531082,25.61964,25.08083,0.025696889,0.024293179,-9.913363E-4,-0.0016416886,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.07583,0.0050230986,26.423271,-0.012692683,-0.0012003235,0.017881883,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
12 12.82494,4037.5803,0.307,0.0,0.0,169.86078,5732.0,100.0,0.0,-0.0020330295,0.0,4.0,0.0,0.0,0.0,84.693436,1.0,0.0,0.0,60.01899,0.0,1.0,-110.21227,24.842361,543.4695,-0.015500802,0.25483412,-0.035387367,174.24815,174.01012,169.7397,169.63368,305.89853,304.47787,400.88684,396.66995,73.77323,69.08499,70.799644,65.18625,0.0,0.0,0.0,0.0,0.034660157,0.034980144,0.014283132,0.01383878,-0.019672163,0.021128532,0.006444314,0.002632082,-1.157186,1.2428548,0.3790773,0.15482835,0.0,0.0,0.0,-1.0,1.0,-2.1652973,-0.03250376,0.0020536045,73.77323,69.08499,70.799644,65.18625,25.105213,24.531204,25.619751,25.080849,0.025829243,0.024427839,-7.1281136E-4,-0.0013369576,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.090744,0.0024308157,26.434696,-0.012943774,-0.0012463271,0.009102043,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
13 13.149033,4037.9343,0.315,0.0,0.0,169.9762,5737.0,100.0,0.0,0.009160281,0.0,4.0,0.0,0.0,0.0,84.697136,1.0,0.0,0.0,60.018642,0.0,1.0,-110.50542,24.842388,543.27124,-0.032200195,0.26522526,-0.046108786,173.96013,174.05067,169.72847,169.74197,305.8012,304.38113,400.5873,396.37408,73.77638,69.087105,70.80156,65.1866,0.0,0.0,0.0,0.0,0.03464579,0.035214636,0.013901675,0.013589289,-0.014327218,0.028275946,-0.029556515,-0.017996257,-0.95514786,1.885063,-1.9704343,-1.1997504,0.0,0.0,0.0,-1.0,1.0,-2.1653147,-0.032495715,0.0021680715,73.77638,69.087105,70.80156,65.1866,25.105516,24.531408,25.619934,25.080883,0.02343825,0.023970794,-0.0014574057,-0.0013779213,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.115627,-0.0038357505,26.455435,-0.005286799,-0.0010240518,-0.0044617346,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
14 13.4731245,4038.2883,0.325,0.0,0.0,170.04463,5738.0,100.0,0.0,0.016547244,0.0,4.0,0.0,0.0,0.0,84.699356,1.0,0.0,0.0,60.018436,0.0,1.0,-110.798676,24.842382,543.0729,-0.026762703,0.25481597,-0.0077182837,174.0757,174.09074,169.71399,169.76973,305.8012,304.38113,400.5873,396.37408,73.778275,69.08838,70.80272,65.186806,0.0,0.0,0.0,0.0,0.03487389,0.03536873,0.013778092,0.013468641,0.011874156,0.021588057,-0.028057748,-0.020563291,0.65967536,1.1993365,-1.5587639,-1.142405,0.0,0.0,0.0,-1.0,1.0,-2.165323,-0.03248412,0.0021698782,73.778275,69.08838,70.80272,65.186806,25.105698,24.53153,25.620045,25.080902,0.023705985,0.023794403,-0.0019443971,-0.0016166419,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.130116,-0.0042215604,26.467607,0.0018667248,-0.001168449,9.254145E-4,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
15 13.959263,4038.7605,0.332,0.0,0.0,170.1122,5739.0,100.0,0.0,0.0083989715,0.0,4.0,0.0,0.0,0.0,84.70158,1.0,0.0,0.0,60.018227,0.0,1.0,-111.18986,24.842344,542.80835,-0.027512636,0.25494838,0.019121999,174.11198,174.16544,169.7345,169.77376,305.75256,304.33273,400.4375,396.2261,73.78017,69.089645,70.80388,65.18702,0.0,0.0,0.0,0.0,0.03509519,0.035522744,0.013817208,0.013471926,0.026435317,0.018123975,-0.004968661,-0.00690373,1.5550187,1.0661162,-0.29227418,-0.40610176,0.0,0.0,0.0,-1.0,1.0,-2.1653323,-0.032541778,0.0021323483,73.78017,69.089645,70.80388,65.18702,25.105879,24.531652,25.620157,25.080923,0.023512656,0.023826884,-0.0022203415,-0.001989509,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.144463,-0.0028066994,26.4798,0.006246484,-0.0014940045,0.0111888945,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
16 14.283356,4039.1147,0.342,0.0,0.0,170.17862,5739.0,100.0,0.0,-0.0034686916,0.0,4.0,0.0,0.0,0.0,84.7038,1.0,0.0,0.0,60.01802,0.0,1.0,-111.48337,24.842316,542.6098,-0.028546177,0.24940713,0.07171454,174.1544,174.22723,169.84029,169.7952,305.70386,304.28433,400.28775,396.07816,73.78207,69.09092,70.805046,65.18724,0.0,0.0,0.0,0.0,0.035337783,0.035671897,0.014006146,0.013649582,0.027287751,0.017833402,0.013414934,0.010643593,1.6051619,1.0490236,0.78911376,0.6260937,0.0,0.0,0.0,-1.0,1.0,-2.165343,-0.032671917,0.0020802144,73.78207,69.09092,70.805046,65.18724,25.106062,24.531775,25.62027,25.080944,0.023362413,0.02379034,-0.001988125,-0.0022530768,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.158398,0.002200487,26.491964,0.0066807917,-0.001680956,0.019356037,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
17 14.769494,4039.5874,0.35,0.0,0.0,170.26718,5740.0,100.0,0.0,2.7964596E-4,0.0,4.0,0.0,0.0,0.0,84.70676,1.0,0.0,0.0,60.017742,0.0,1.0,-111.87488,24.84231,542.3449,-0.024774725,0.25113165,0.080288865,174.34059,174.34473,170.01518,169.89398,305.65518,304.23593,400.13797,395.93024,73.784584,69.092606,70.80659,65.187546,0.0,0.0,0.0,0.0,0.03564696,0.03586361,0.014187679,0.013973102,0.030653965,0.0189371,0.020581711,0.02784313,1.702998,1.0520611,1.1434284,1.5468405,0.0,0.0,0.0,-1.0,1.0,-2.1653576,-0.03284161,0.0020180475,73.784584,69.092606,70.80659,65.187546,25.106304,24.531937,25.620417,25.080975,0.023923587,0.023947949,-0.0014799545,-0.002191845,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.17719,0.009847768,26.507994,0.005680933,-0.00166586,0.014170732,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
18 15.253259,4039.9421,0.357,0.0,0.0,170.3318,5741.0,100.0,0.0,-0.0037863501,0.0,4.0,0.0,0.0,0.0,84.70898,1.0,0.0,0.0,60.017536,0.0,1.0,-112.16864,24.842356,542.1462,-0.01671688,0.24182548,0.051545005,174.62819,174.5305,170.05142,169.97345,305.6065,304.1875,399.98825,395.7823,73.78646,69.09387,70.80774,65.187775,0.0,0.0,0.0,0.0,0.035722785,0.035942208,0.014252585,0.014245591,0.025666753,0.01802072,0.016429275,0.039733946,1.7111169,1.2013813,1.095285,2.6489298,0.0,0.0,0.0,-1.0,1.0,-2.16537,-0.032910272,0.001991446,73.78646,69.09387,70.80774,65.187775,25.106483,24.532059,25.62053,25.080997,0.025223643,0.024650058,-0.0016460561,-0.0021038703,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.191162,0.013625955,26.519114,0.002056227,-0.0017836776,0.0065005226,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
19 15.567857,4040.2969,0.367,0.0,0.0,170.39563,5744.0,100.0,0.0,2.2442082E-4,0.0,4.0,0.0,0.0,0.0,84.7112,1.0,0.0,0.0,60.01733,0.0,1.0,-112.4625,24.84244,541.9473,-0.01776832,0.24116075,0.024250237,174.77617,174.67091,170.07419,170.066,305.6065,304.1875,399.98825,395.7823,73.78833,69.09513,70.8089,65.18801,0.0,0.0,0.0,0.0,0.035606816,0.03589045,0.01432358,0.014522852,-0.0023613959,0.0015786466,0.007994199,0.032338258,-0.13890564,0.09286156,0.470247,1.9022504,0.0,0.0,0.0,-1.0,1.0,-2.1653836,-0.03292371,0.0020090973,73.78833,69.09513,70.8089,65.18801,25.106663,24.532179,25.62064,25.081018,0.025708094,0.025090385,-0.0018864868,-0.0019345023,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.20509,0.015505659,26.53018,-0.004986443,-0.0016786046,-3.4433513E-4,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
20 16.039753,4040.7703,0.375,0.0,0.0,170.45953,5748.0,100.0,0.0,0.0040749414,0.0,4.0,0.0,0.0,0.0,84.71342,1.0,0.0,0.0,60.01712,0.0,1.0,-112.854485,24.842587,541.68207,-0.018202942,0.2413529,0.017172432,174.78044,174.73691,170.10362,170.1539,305.5578,304.1391,399.8385,395.6344,73.79021,69.09639,70.81005,65.18824,0.0,0.0,0.0,0.0,0.03545997,0.035773963,0.0144413635,0.014785069,-0.014600862,-0.00934696,0.010487676,0.029970996,-0.811159,-0.51927555,0.58264863,1.6650554,0.0,0.0,0.0,-1.0,1.0,-2.1653988,-0.032894265,0.002088995,73.79021,69.09639,70.81005,65.18824,25.106844,24.532299,25.62075,25.081041,0.025348512,0.025093224,-0.002087924,-0.0017930274,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.21901,0.016820235,26.541273,-0.013632805,-0.0016907271,-0.0055084135,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
21 16.354351,4041.1255,0.382,0.0,0.0,170.54517,5755.0,100.0,0.0,0.0012987609,0.0,4.0,0.0,0.0,0.0,84.71641,1.0,0.0,0.0,60.016846,0.0,1.0,-113.14859,24.84271,541.48303,-0.022501389,0.2451741,0.0393255,174.67075,174.71301,170.24464,170.33131,305.5091,304.09067,399.68878,395.48648,73.79271,69.09807,70.81159,65.18854,0.0,0.0,0.0,0.0,0.035401274,0.035641193,0.0146766985,0.015073612,-0.013702859,-0.01661703,0.023541227,0.03671733,-0.913524,-1.107802,1.5694151,2.4478219,0.0,0.0,0.0,-1.0,1.0,-2.1654234,-0.032835644,0.002294316,73.79271,69.09807,70.81159,65.18854,25.107084,24.532461,25.6209,25.08107,0.024190536,0.024438366,-0.0017621501,-0.0012539756,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.237606,0.02003613,26.556547,-0.02424377,-0.0018405571,-0.00504553,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
22 16.66895,4041.4807,0.392,0.0,0.0,170.63432,5761.0,100.0,0.0,-0.012933018,0.0,4.0,0.0,0.0,0.0,84.7194,1.0,0.0,0.0,60.016567,0.0,1.0,-113.4428,24.842848,541.28394,-0.02279238,0.25629854,0.0285197,174.57756,174.62965,170.44597,170.5077,305.46036,304.0422,399.539,395.33853,73.79522,69.099754,70.81311,65.18882,0.0,0.0,0.0,0.0,0.035602823,0.035672452,0.014721351,0.0151250595,0.00840316,-0.005971202,0.016469836,0.01999944,0.49430352,-0.3512472,0.9688139,1.1764377,0.0,0.0,0.0,-1.0,1.0,-2.1654518,-0.032781146,0.0025523542,73.79522,69.099754,70.81311,65.18882,25.107327,24.532623,25.621044,25.081099,0.023109358,0.023414625,-0.0011038778,-7.420546E-4,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.257008,0.022770366,26.57248,-0.025350533,-0.0020437962,-0.00586406,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
23 17.140846,4041.9546,0.4,0.0,0.0,170.70256,5763.0,100.0,0.0,-0.021631196,0.0,4.0,0.0,0.0,0.0,84.72164,1.0,0.0,0.0,60.016357,0.0,1.0,-113.83525,24.843058,541.0183,-0.01922873,0.25786906,0.008813665,174.71027,174.66154,170.55545,170.61969,305.4116,303.9937,399.3892,395.19055,73.797104,69.10103,70.81424,65.18902,0.0,0.0,0.0,0.0,0.035794284,0.035845757,0.014541981,0.014907652,0.021833926,0.011364619,-0.007484315,-0.009220041,1.2129959,0.63136774,-0.41579527,-0.5122245,0.0,0.0,0.0,-1.0,1.0,-2.165474,-0.032745425,0.0027118518,73.797104,69.10103,70.81424,65.18902,25.107508,24.532745,25.621153,25.081116,0.023477638,0.02319233,-8.6181273E-4,-4.8549226E-4,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.271946,0.0233458,26.584316,-0.01879117,-0.0023562151,-0.003961567,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
24 17.612743,4042.3103,0.41,0.0,0.0,170.79535,5764.0,100.0,0.0,-0.05187564,0.0,4.0,0.0,0.0,0.0,84.72463,1.0,0.0,0.0,60.01608,0.0,1.0,-114.129715,24.843231,540.819,-0.016898781,0.26488048,-0.03438637,174.85413,174.76385,170.60626,170.6618,305.36285,303.94522,399.23935,395.0425,73.79961,69.10272,70.81574,65.18927,0.0,0.0,0.0,0.0,0.03597383,0.036112472,0.014156679,0.014442531,0.020611411,0.024445562,-0.03137067,-0.03791823,1.1450784,1.3580867,-1.742815,-2.1065683,0.0,0.0,0.0,-1.0,1.0,-2.1655045,-0.032718446,0.0028147027,73.79961,69.10272,70.81574,65.18927,25.107746,24.53291,25.621298,25.08114,0.023764038,0.023235457,-0.0011070737,-7.818763E-4,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.29251,0.020403259,26.600136,-0.005004784,-0.002925523,-0.0024073077,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
25 18.08464,4042.7847,0.417,0.0,0.0,170.84209,5764.0,100.0,0.0,-0.05187564,0.0,4.0,0.0,0.0,0.0,84.72613,1.0,0.0,0.0,60.01594,0.0,1.0,-114.522514,24.843454,540.5531,-0.017355524,0.2645813,-0.028130485,174.8914,174.82014,170.62549,170.66968,305.36285,303.94522,399.23935,395.0425,73.80086,69.10358,70.81649,65.1894,0.0,0.0,0.0,0.0,0.03605872,0.036253184,0.013992093,0.014216232,0.01555506,0.023966327,-0.032346368,-0.040671732,0.9150035,1.409784,-1.9027275,-2.3924549,0.0,0.0,0.0,-1.0,1.0,-2.1655192,-0.032705612,0.0028123893,73.80086,69.10358,70.81649,65.1894,25.107868,24.532991,25.621368,25.081152,0.023702193,0.023285033,-0.001267824,-0.0010091378,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.3028,0.019186966,26.608093,0.0021187798,-0.002998144,-0.0026582738,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
26 18.399239,4043.1406,0.427,0.0,0.0,170.9358,5763.0,100.0,0.0,-0.058447283,0.0,4.0,0.0,0.0,0.0,84.72912,1.0,0.0,0.0,60.015663,0.0,1.0,-114.817245,24.843603,540.3536,-0.017351182,0.26599306,-0.019857155,174.96233,174.9395,170.75694,170.68292,305.31406,303.8967,399.08948,394.89447,73.80337,69.10527,70.81798,65.18965,0.0,0.0,0.0,0.0,0.036252815,0.036529295,0.013673156,0.01382406,0.016410999,0.024518983,-0.028442537,-0.036380693,0.9653529,1.4422932,-1.6730905,-2.1400409,0.0,0.0,0.0,-1.0,1.0,-2.1655464,-0.032651495,0.00270092,73.80337,69.10527,70.81798,65.18965,25.108109,24.533154,25.621513,25.081179,0.023555756,0.023422193,-0.0010463779,-0.001479471,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.323437,0.017381918,26.624083,0.01657727,-0.0030503247,-0.008598872,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
27 18.871134,4043.6155,0.435,0.0,0.0,171.02904,5762.0,100.0,0.0,-0.05161566,0.0,4.0,0.0,0.0,0.0,84.73211,1.0,0.0,0.0,60.015385,0.0,1.0,-115.2104,24.843784,540.0874,-0.019829765,0.2624005,-0.0019270547,175.03053,175.09914,170.74245,170.69574,305.26523,303.84818,398.93958,394.7464,73.80588,69.106964,70.819466,65.18992,0.0,0.0,0.0,0.0,0.036502145,0.036762293,0.013357432,0.013613336,0.024634724,0.028283853,-0.03525894,-0.03349424,1.3685958,1.5713252,-1.9588301,-1.8607912,0.0,0.0,0.0,-1.0,1.0,-2.165567,-0.03249422,0.0024466952,73.80588,69.106964,70.819466,65.18992,25.10835,24.533318,25.621655,25.081203,0.023396553,0.02379777,-0.0016757208,-0.0019487994,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.34369,0.01690556,26.640028,0.030272061,-0.0027883833,-0.018927325,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
28 19.185734,4043.9717,0.442,0.0,0.0,171.09583,5763.0,100.0,0.0,-0.044326268,0.0,4.0,0.0,0.0,0.0,84.73435,1.0,0.0,0.0,60.01518,0.0,1.0,-115.5054,24.84391,539.88763,-0.01922547,0.24736242,0.008626948,175.27032,175.32149,170.73315,170.74261,305.21643,303.79965,398.7897,394.59833,73.807755,69.10822,70.820595,65.190125,0.0,0.0,0.0,0.0,0.036639597,0.036803745,0.013291526,0.013668455,0.025785467,0.018296639,-0.025441995,-0.01037363,1.7190311,1.2197759,-1.696133,-0.6915753,0.0,0.0,0.0,-1.0,1.0,-2.1655786,-0.03235155,0.002190889,73.807755,69.10822,70.820595,65.190125,25.10853,24.533438,25.621765,25.081223,0.024398552,0.024697684,-0.002119682,-0.0020644295,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.35799,0.017505331,26.651459,0.035164107,-0.0026199582,-0.018351326,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
29 19.657629,4044.3281,0.452,0.0,0.0,171.18163,5767.0,100.0,0.0,-0.042977434,0.0,4.0,0.0,0.0,0.0,84.73734,1.0,0.0,0.0,60.0149,0.0,1.0,-115.80052,24.844038,539.6878,-0.021774048,0.23947896,-0.0031411645,175.5444,175.60716,170.78072,170.87808,305.16763,303.7511,398.6399,394.45035,73.810234,69.10989,70.822105,65.1904,0.0,0.0,0.0,0.0,0.036588937,0.0365657,0.013468195,0.013896169,0.005105401,-0.011564396,0.006515478,0.016637256,0.3003177,-0.68025863,0.3832634,0.97866213,0.0,0.0,0.0,-1.0,1.0,-2.165594,-0.032213278,0.0018545655,73.810234,69.10989,70.822105,65.1904,25.108768,24.5336,25.62191,25.081251,0.02548629,0.02585287,-0.0023420742,-0.0017732137,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.376392,0.0173921,26.666403,0.031326007,-0.002644165,-0.008772555,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
30 20.129528,4044.8035,0.46,0.0,0.0,171.24388,5771.0,100.0,0.0,-0.047827054,0.0,4.0,0.0,0.0,0.0,84.739586,1.0,0.0,0.0,60.01469,0.0,1.0,-116.19417,24.844215,539.42126,-0.022482496,0.2338695,-0.03322093,175.69981,175.77182,170.86798,170.89658,305.1188,303.70258,398.4901,394.3024,73.812096,69.11114,70.82324,65.190605,0.0,0.0,0.0,0.0,0.036385402,0.03626933,0.013675446,0.014058191,-0.014121954,-0.029689737,0.02132889,0.021652007,-0.784553,-1.6494298,1.1849384,1.2028893,0.0,0.0,0.0,-1.0,1.0,-2.1656082,-0.032209214,0.0016591968,73.812096,69.11114,70.82324,65.190605,25.108948,24.533718,25.622019,25.08127,0.026021011,0.026441412,-0.002195113,-0.0020281922,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.38975,0.01505751,26.677374,0.022958325,-0.0027453755,0.0028090784,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
31 20.447374,4045.1602,0.47,0.0,0.0,171.3059,5778.0,100.0,0.0,-0.062244527,0.0,4.0,0.0,0.0,0.0,84.74183,1.0,0.0,0.0,60.01448,0.0,1.0,-116.489525,24.844341,539.2212,-0.02323555,0.23502758,-0.034066625,175.73271,175.83405,170.9717,170.9541,305.06998,303.65402,398.34036,394.15442,73.81395,69.11238,70.82438,65.19081,0.0,0.0,0.0,0.0,0.036146622,0.035968594,0.013882383,0.0142436335,-0.02457305,-0.03317247,0.023010446,0.019303575,-1.3651695,-1.8429149,1.2783581,1.0724208,0.0,0.0,0.0,-1.0,1.0,-2.1656253,-0.03225248,0.001537294,73.81395,69.11238,70.82438,65.19081,25.109125,24.533836,25.622128,25.08129,0.025841586,0.026433097,-0.0019509464,-0.0020535914,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.40316,0.0125868805,26.688421,0.012787492,-0.0027570797,0.0064510647,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
32 20.925768,4045.636,0.477,0.0,0.0,171.36821,5784.0,100.0,0.0,-0.06566952,0.0,4.0,0.0,0.0,0.0,84.744095,1.0,0.0,0.0,60.01427,0.0,1.0,-116.88349,24.844479,538.9544,-0.020693334,0.23424439,-0.032882582,175.83826,175.87889,171.07758,171.0785,305.06998,303.65402,398.34036,394.15442,73.8158,69.113625,70.8255,65.191025,0.0,0.0,0.0,0.0,0.03591611,0.03568515,0.014066943,0.014416676,-0.027605496,-0.034363393,0.023029251,0.021087334,-1.6238527,-2.0213761,1.3546618,1.2404314,0.0,0.0,0.0,-1.0,1.0,-2.1656444,-0.032297246,0.0014890591,73.8158,69.113625,70.8255,65.191025,25.109304,24.533957,25.622236,25.081312,0.026084458,0.026321536,-0.001695974,-0.001690497,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.416653,0.009974838,26.699299,0.0031771893,-0.0027551234,0.0058646416,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
33 21.244698,4045.993,0.485,0.0,0.0,171.4524,5792.0,100.0,0.0,-0.073485024,0.0,4.0,0.0,0.0,0.0,84.747116,1.0,0.0,0.0,60.013992,0.0,1.0,-117.17908,24.84456,538.75415,-0.014153696,0.24148393,-0.06599551,175.97643,175.8869,171.22888,171.17216,305.02115,303.60547,398.19058,394.00647,73.81827,69.11529,70.826996,65.19129,0.0,0.0,0.0,0.0,0.035547975,0.03535814,0.014167956,0.014536584,-0.03990978,-0.040697057,0.01903822,0.01953002,-2.660652,-2.7131371,1.2692147,1.3020014,0.0,0.0,0.0,-1.0,1.0,-2.1656735,-0.032350145,0.0015241038,73.81827,69.11529,70.826996,65.19129,25.10954,24.534117,25.62238,25.081337,0.026386462,0.025864357,-0.0013036191,-0.0016344176,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.43546,0.003714017,26.713604,-0.0072334763,-0.0028177002,0.0038037421,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
34 21.723093,4046.35,0.495,0.0,0.0,171.518,5796.0,100.0,0.0,-0.07679934,0.0,4.0,0.0,0.0,0.0,84.74938,1.0,0.0,0.0,60.013783,0.0,1.0,-117.47478,24.844614,538.5539,-0.013632373,0.25245753,-0.06559541,175.91951,175.82158,171.30203,171.30792,304.9723,303.5569,398.04077,393.8585,73.82012,69.11654,70.8281,65.19148,0.0,0.0,0.0,0.0,0.03531124,0.03522051,0.0140859345,0.01452019,-0.033603773,-0.025813363,0.001055085,0.005750762,-1.8668762,-1.4340757,0.058615834,0.31948677,0.0,0.0,0.0,-1.0,1.0,-2.165697,-0.032351553,0.001600171,73.82012,69.11654,70.8281,65.19148,25.109718,24.534237,25.622486,25.081356,0.025662057,0.02509104,-0.0012592374,-0.0012248752,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.45033,-0.0011681489,26.724682,-0.011095868,-0.0028733583,-0.0014085505,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
35 22.201488,4046.8264,0.502,0.0,0.0,171.6084,5798.0,100.0,0.0,-0.083627366,0.0,4.0,0.0,0.0,0.0,84.7524,1.0,0.0,0.0,60.013504,0.0,1.0,-117.8692,24.844625,538.2867,-0.013671633,0.25631142,-0.057238538,175.86954,175.81245,171.36868,171.37608,304.92343,303.5083,397.8909,393.71042,73.8226,69.11822,70.829575,65.191734,0.0,0.0,0.0,0.0,0.035153784,0.035233572,0.013865768,0.014293048,-0.023187742,-0.0073274267,-0.01777577,-0.014325659,-1.3639848,-0.4310251,-1.0456336,-0.8426858,0.0,0.0,0.0,-1.0,1.0,-2.16573,-0.032319028,0.0017003558,73.8226,69.11822,70.829575,65.191734,25.109957,24.534399,25.622627,25.081379,0.024830593,0.024497962,-0.0013969055,-0.0013537349,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.47048,-0.00694992,26.739777,-0.008831456,-0.003015354,-0.004377713,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
36 22.520418,4047.1838,0.51,0.0,0.0,171.67668,5797.0,100.0,0.0,-0.08616711,0.0,4.0,0.0,0.0,0.0,84.75467,1.0,0.0,0.0,60.013294,0.0,1.0,-118.16515,24.844593,538.0862,-0.019227957,0.2582171,-0.044518888,175.88156,175.88544,171.37125,171.43143,304.8745,303.45966,397.741,393.56235,73.82446,69.11948,70.83068,65.191925,0.0,0.0,0.0,0.0,0.035166744,0.035313994,0.013727826,0.014096176,-0.009633104,0.0062321625,-0.023873895,-0.028267564,-0.64220697,0.41547748,-1.591593,-1.8845043,0.0,0.0,0.0,-1.0,1.0,-2.1657543,-0.032279488,0.0017462259,73.82446,69.11948,70.83068,65.191925,25.110136,24.53452,25.622734,25.081398,0.024492983,0.024515623,-0.0017791511,-0.0014285297,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.48551,-0.010167306,26.75147,-0.004878632,-0.0030249038,-0.005207439,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
37 22.839348,4047.5415,0.517,0.0,0.0,171.74548,5795.0,100.0,0.0,-0.08860586,0.0,4.0,0.0,0.0,0.0,84.756935,1.0,0.0,0.0,60.013084,0.0,1.0,-118.46122,24.844532,537.8856,-0.016487962,0.26103613,-0.022542765,175.92683,175.91576,171.46367,171.48314,304.82562,303.41104,397.5911,393.41428,73.826324,69.120735,70.83179,65.19211,0.0,0.0,0.0,0.0,0.03527517,0.03543609,0.013676049,0.01391935,0.008092324,0.013501197,-0.012647919,-0.02491319,0.53948826,0.9000798,-0.8431946,-1.6608794,0.0,0.0,0.0,-1.0,1.0,-2.165778,-0.032253243,0.0017618624,73.826324,69.120735,70.83179,65.19211,25.110315,24.534641,25.62284,25.081415,0.024346262,0.024281703,-0.0016409023,-0.0015274936,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.500732,-0.011831859,26.763191,-6.815667E-4,-0.0031339019,-0.00269921,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
38 23.317741,4047.8992,0.527,0.0,0.0,171.83765,5792.0,100.0,0.0,-0.09496216,0.0,4.0,0.0,0.0,0.0,84.75996,1.0,0.0,0.0,60.012806,0.0,1.0,-118.7574,24.844452,537.68494,-0.016842874,0.26147953,-0.011131329,176.03027,176.0295,171.57098,171.57745,304.77667,303.36237,397.44116,393.26617,73.828804,69.12241,70.83325,65.19235,0.0,0.0,0.0,0.0,0.03545368,0.03564789,0.013607756,0.013733252,0.016878633,0.019641045,-0.007062931,-0.021348488,0.99286073,1.1553556,-0.41546655,-1.2557933,0.0,0.0,0.0,-1.0,1.0,-2.1658087,-0.032234173,0.0017262091,73.828804,69.12241,70.83325,65.19235,25.110554,24.534801,25.622982,25.08144,0.024398785,0.0243943,-0.0015518327,-0.001514221,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.520996,-0.013003436,26.778904,0.005979049,-0.0031720053,-0.0017585277,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
39 23.796137,4048.3765,0.535,0.0,0.0,171.88348,5792.0,100.0,0.0,-0.09539544,0.0,4.0,0.0,0.0,0.0,84.76147,1.0,0.0,0.0,60.012665,0.0,1.0,-119.15249,24.844328,537.41724,-0.018743541,0.25865552,-0.018018097,176.11974,176.11406,171.6248,171.62872,304.77667,303.36237,397.44116,393.26617,73.83004,69.12325,70.833984,65.192474,0.0,0.0,0.0,0.0,0.035528727,0.035723917,0.013566235,0.013642805,0.014086564,0.015990395,-0.0061007836,-0.015363615,0.7825869,0.88835526,-0.33893242,-0.85353416,0.0,0.0,0.0,-1.0,1.0,-2.165823,-0.032226935,0.0016839136,73.83004,69.12325,70.833984,65.192474,25.110672,24.534883,25.62305,25.081451,0.024646044,0.024612974,-0.0015050384,-0.0014821178,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.530987,-0.014075285,26.786732,0.009123739,-0.0031545938,-0.001318757,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0
40 24.115067,4048.7346,0.542,0.0,0.0,171.97446,5793.0,100.0,0.0,-0.101628795,0.0,4.0,0.0,0.0,0.0,84.76449,1.0,0.0,0.0,60.012386,0.0,1.0,-119.44893,24.844227,537.2164,-0.016709078,0.25675243,-0.04038965,176.28296,176.25171,171.69395,171.72241,304.72772,303.3137,397.29123,393.11807,73.83251,69.12493,70.83545,65.19272,0.0,0.0,0.0,0.0,0.03555339,0.035814002,0.01346909,0.013442952,0.00664716,0.011074047,-0.009244432,-0.019353317,0.443144,0.73826975,-0.61629546,-1.2902211,0.0,0.0,0.0,-1.0,1.0,-2.1658506,-0.032227688,0.0015559354,73.83251,69.12493,70.83545,65.19272,25.11091,24.535046,25.623194,25.081476,0.025053147,0.0248715,-0.001631055,-0.0014655663,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,39.550945,-0.017810632,26.80214,0.014740507,-0.0031819611,0.0013267533,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,58.0

View file

@ -42,7 +42,11 @@ def test_lap_token_present(stem, expected):
("zandvoort_Q2_L3_ferrari", True),
("zandvoort_R_L1_ferrari", True),
("zandvoort_FP1_L5_ferrari", True),
("zandvoort_P_74.074_ferrari", False), # bare P without digit not matched
# ACC compact tokens
("hungaroring_P_L1_123.702_ktm_gt4", True), # bare P (ACC Practice)
("hungaroring_Q_L1_123.702_ktm_gt4", True), # bare Q (ACC Qualifying)
("hungaroring_HL_L1_123.702_ktm_gt4", True), # Hotlap
("hungaroring_HS_L1_123.702_ktm_gt4", True), # Hotstint
("zandvoort_L7_74.074_ferrari", False),
("notoken", False),
])
@ -81,10 +85,11 @@ def test_read_session_type_sample(sample_path):
# --- read_metadata ---
def test_read_metadata_sample(sample_path):
lap, event, track = read_metadata(sample_path)
lap, event, track, game = read_metadata(sample_path)
assert lap == 7
assert event == 'P1'
assert track != ''
assert 'F1' in game or game != ''
# --- rename_unprocessed ---
@ -137,6 +142,7 @@ def test_rename_unprocessed_moves_to_races_dir(tmp_path, sample_path):
assert r.new is not None and 'P1' in r.new and 'L7' in r.new
assert os.path.exists(r.new)
assert r.new.startswith(races_dir)
assert 'F1 22' in r.new
assert 'Practice' in r.new
assert not os.path.exists(src)
@ -150,6 +156,7 @@ def test_rename_unprocessed_moves_already_renamed(tmp_path, sample_path):
assert len(results) == 1
r = results[0]
assert r.status == 'renamed'
assert 'F1 22' in r.new
assert 'Practice' in r.new
assert os.path.exists(r.new)
assert not os.path.exists(src)
@ -176,3 +183,45 @@ def test_rename_unprocessed_walks_subdirs(tmp_path, sample_path):
assert len(results) == 1
assert results[0].status == 'renamed'
assert 'Practice' in results[0].new
# --- ACC support ---
def test_acc_rename_inserts_compact_token(tmp_path, acc_path):
"""ACC 'Practice' event normalises to 'P' and lap reads as L1."""
src = str(tmp_path / "hungaroring_123.702_ktm_gt4.csv")
shutil.copy(acc_path, src)
results = rename_unprocessed([src])
assert len(results) == 1
r = results[0]
assert r.status == 'renamed'
assert r.new is not None
new_stem = os.path.splitext(os.path.basename(r.new))[0]
tokens = new_stem.split('_')
assert 'P' in tokens
assert 'L1' in tokens
def test_acc_rename_lands_in_practice_folder(tmp_path, acc_path):
"""ACC 'Practice' event files into races/ACC/<Track>/Practice/, not Other."""
src = str(tmp_path / "hungaroring_123.702_ktm_gt4.csv")
shutil.copy(acc_path, src)
races_dir = str(tmp_path / 'races')
results = rename_unprocessed([src], races_dir=races_dir)
assert len(results) == 1
r = results[0]
assert r.status == 'renamed'
assert r.new is not None
assert 'ACC' in r.new
assert 'Practice' in r.new
assert 'Other' not in r.new
assert os.path.exists(r.new)
def test_acc_rename_idempotent(tmp_path, acc_path):
"""Second rename_unprocessed run on an already-renamed ACC file returns skipped."""
src = str(tmp_path / "hungaroring_123.702_ktm_gt4.csv")
shutil.copy(acc_path, src)
rename_unprocessed([src])
results = rename_unprocessed([str(tmp_path)])
assert all(r.status == 'skipped' for r in results)

View file

@ -1,9 +1,15 @@
import os
import dataclasses
import pytest
import telemetry.report_technique as rt
import telemetry.report_setup as rs
import telemetry.report_compare as rc
import telemetry.report_race as rr
import telemetry.report_profile as rp
from telemetry.__main__ import is_fresh
from telemetry.parser import Lap
from telemetry import report_common
from telemetry.report_common import game_of, load_prompt
def _check(result, path):
@ -57,6 +63,177 @@ def test_setup_output_contains_setup(sample_lap, tmp_path):
assert "## Tyres" in text
def test_dedup_laps_prefers_valid():
invalid_fast = Lap(
session={'laptime': '113.5'},
track={'Lap': '8', 'Valid': 'false'},
setup={},
ch={},
n=0,
)
valid_slow = Lap(
session={'laptime': '115.2'},
track={'Lap': '8', 'Valid': 'true'},
setup={},
ch={},
n=0,
)
result = rr._dedup_laps([invalid_fast, valid_slow])
assert len(result) == 1
assert result[0].track['Valid'] == 'true'
def _variant(lap, *, laptime=None, lap_no=None, fuel=None, tyre=None):
session = dict(lap.session)
track = dict(lap.track)
if laptime is not None:
session['laptime'] = str(laptime)
if lap_no is not None:
track['Lap'] = str(lap_no)
if fuel is not None:
track['Fuel at Start'] = str(fuel)
if tyre is not None:
track['Tyre'] = tyre
return dataclasses.replace(lap, session=session, track=track)
def test_include_prompt_toggle(sample_lap, tmp_path):
with_p = str(tmp_path / "with.md")
no_p = str(tmp_path / "no.md")
rt.generate(sample_lap, with_p, 'ru', include_prompt=True)
rt.generate(sample_lap, no_p, 'ru', include_prompt=False)
a = open(with_p, encoding="utf-8").read()
b = open(no_p, encoding="utf-8").read()
assert len(b) < len(a)
assert "## Trace" in b # data kept
assert "Начинай анализ" not in b # prompt dropped
assert "Начинай анализ" in a
def test_compare_verdict_fuel_correction_range(sample_lap, tmp_path):
lap_a = _variant(sample_lap, laptime=100.0, fuel=60.0)
lap_b = _variant(sample_lap, laptime=99.0, fuel=40.0)
out = str(tmp_path / "cmp.md")
rc.generate([lap_a, lap_b], out, 'ru', include_prompt=False)
text = open(out, encoding="utf-8").read()
assert "## Verdict" in text
# fuel benefit (60-40)*[0.03..0.05] = 0.6..1.0 ; residual = -1.0 - [1.0..0.6] = -2.0..-1.6
assert "-2.000" in text and "-1.600" in text
def test_profile_sections_and_pick(sample_lap, tmp_path):
laps = [
_variant(sample_lap, lap_no=1, laptime=80.0, tyre='C3'),
_variant(sample_lap, lap_no=2, laptime=75.0, tyre='C3'),
_variant(sample_lap, lap_no=3, laptime=78.0, tyre='C3'),
]
out = str(tmp_path / "prof.md")
rp.generate(laps, out, 'ru', include_prompt=False)
text = open(out, encoding="utf-8").read()
assert "Тенденции по поворотам" in text
assert "Рекомендованные круги" in text
assert "L2" in text # fastest valid lap picked
def test_profile_covers_multiple_compounds(sample_lap, tmp_path):
laps = (
[_variant(sample_lap, lap_no=i, laptime=80.0, tyre='C3') for i in range(1, 4)]
+ [_variant(sample_lap, lap_no=i, laptime=82.0, tyre='C2') for i in range(4, 7)]
)
out = str(tmp_path / "prof_multi.md")
rp.generate(laps, out, 'ru', include_prompt=False)
text = open(out, encoding="utf-8").read()
assert "состав C3" in text
assert "состав C2" in text # secondary compound not ignored
def test_merge_gap_scales_with_track_length(sample_lap):
from telemetry.segments import merge_gap_for, MERGE_GAP_M, MERGE_GAP_MIN, MERGE_GAP_MAX
# sample lap is Zandvoort (~4257 m) → anchor, unchanged
assert abs(merge_gap_for(sample_lap) - MERGE_GAP_M) < 1.0
# tight circuit clamps low, long circuit clamps high
short = _variant(sample_lap)
short.track['Tracklen'] = '1500'
assert merge_gap_for(short) == MERGE_GAP_MIN
long = _variant(sample_lap)
long.track['Tracklen'] = '9000'
assert merge_gap_for(long) == MERGE_GAP_MAX
def test_dedup_keeps_laps_without_number():
# export missing the 'Lap' field must NOT collapse the field to one lap
laps = [
Lap(session={'laptime': str(80 + i)}, track={'Tyre': 'C3'}, setup={}, ch={}, n=0)
for i in range(5)
]
result = rr._dedup_laps(laps)
assert len(result) == 5
def test_is_fresh_cache(tmp_path):
src = tmp_path / "src.csv"
rep = tmp_path / "rep.md"
src.write_text("x")
rep.write_text("y")
os.utime(rep, (src.stat().st_atime + 10, src.stat().st_mtime + 10))
assert is_fresh(str(rep), [str(src)]) is True
os.utime(src, (rep.stat().st_atime + 20, rep.stat().st_mtime + 20))
assert is_fresh(str(rep), [str(src)]) is False
class TestACC:
"""ACC support: game detection, prompt fallback, game-aware report sections."""
def test_game_of_acc_vs_f1(self, acc_lap, sample_lap):
assert game_of(acc_lap) == 'acc'
assert game_of(sample_lap) == 'f1'
def test_load_prompt_acc_falls_back_to_f1(self):
# ACC requests resolve to the ACC prompt if present, else the F1 prompt —
# never empty while an F1 prompt exists.
base = os.path.normpath(os.path.join(
os.path.dirname(report_common.__file__), '..', 'prompts', 'ru'))
for mode in ('technique', 'setup', 'compare', 'race'):
acc_file = os.path.join(base, 'acc', f'{mode}.md')
f1_file = os.path.join(base, f'{mode}.md')
expected = open(acc_file if os.path.exists(acc_file) else f1_file,
encoding='utf-8').read().strip()
assert load_prompt(mode, 'ru', 'acc') == expected
assert load_prompt(mode, 'ru', 'f1') == open(f1_file, encoding='utf-8').read().strip()
def test_acc_setup_no_param_table_keeps_balance(self, acc_lap, tmp_path):
out = str(tmp_path / "acc_setup.md")
rs.generate(acc_lap, out, 'ru', include_prompt=False)
text = open(out, encoding="utf-8").read()
assert "| Parameter | Value |" not in text # zero-filled setup table dropped
assert "не экспортирует сетап" in text # explanatory note instead
assert "## Corner balance" in text # telemetry-derived sections kept
assert "## Tyres" in text
def test_acc_technique_no_setup_line_no_ers(self, acc_lap, tmp_path):
out = str(tmp_path / "acc_tech.md")
rt.generate(acc_lap, out, 'ru', include_prompt=False)
text = open(out, encoding="utf-8").read()
assert "## Corners" in text
assert "**Setup:**" not in text # zero-filled setup summary skipped
assert "ERS spent:" not in text # hybrid block dropped
def test_f1_technique_keeps_setup_and_ers(self, sample_lap, tmp_path):
# regression: F1 path unchanged
out = str(tmp_path / "f1_tech.md")
rt.generate(sample_lap, out, 'ru', include_prompt=False)
text = open(out, encoding="utf-8").read()
assert "**Setup:**" in text
assert "ERS spent:" in text
def test_acc_race_skips_ers_table(self, acc_lap, tmp_path):
out = str(tmp_path / "acc_race.md")
rr.generate([acc_lap], out, 'ru', include_prompt=False)
text = open(out, encoding="utf-8").read()
assert "## ERS per lap" not in text
assert "## Lap times" in text
class TestRegressionReports:
"""Regression: report generators must not crash on valid single-lap input."""