initial release

This commit is contained in:
Andrew Pimenov 2026-06-13 18:53:52 +03:00
commit 5b6b68748f
43 changed files with 5523 additions and 0 deletions

30
.gitignore vendored Normal file
View file

@ -0,0 +1,30 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
.pytest_cache/
.mypy_cache/
.venv/
venv/
# Telemetry data and generated output (live outside this repo, ignored if nested)
*.csv
reports/
races/
# ...but test fixtures must be tracked
!tests/fixtures/
!tests/fixtures/*.csv
# OS
.DS_Store
Thumbs.db
# Editor / tooling
.idea/
.vscode/
.claude/
.aider*
.cursorrules
*_ai_notes*
llm_context*

58
ARCHITECTURE.md Normal file
View file

@ -0,0 +1,58 @@
[Русский](ARCHITECTURE.ru.md)
# 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.
## Files
```
telemetry/
__main__.py CLI entry: arg parsing, mode dispatch, stdout. No args -> GUI.
parser.py CSV -> Lap (frozen dataclass: session, track, setup, ch, n).
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, 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, stints, wear, thermals, ERS.
rename.py Insert lap number into filenames; skip already-processed.
gui.py tkinter window; runs generation/rename on a worker thread.
prompts/ Russian LLM prompts, one per report mode.
tests/ pytest suite + fixture laps.
```
## How a CSV becomes a report
1. **`__main__.main`** parses `<mode> <files…>`, validates arity (technique/setup = 1 file, compare = 2+, race = 1 dir), and dispatches.
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`** function assembles Markdown from `report_common` helpers (`header_block`, `md_table`, `legend`) and the data above, then calls **`report_common.write_report`**, which writes atomically (temp file + `os.replace`) and returns `(abs_path, tokens)`.
6. **`__main__`** prints that path and `~N tokens (budget 60k)`. The **GUI** shows the same tuple in its log instead of printing.
## Adding things
**A new report mode.** Create `telemetry/report_x.py` with `generate(lap, out_path) -> tuple[str, int]` (or `generate(laps, out_path)` for multi-lap). Build the text from `report_common` helpers and end with `return write_report(out_path, text)`. In `__main__.py` add `'x'` to the mode choices and a dispatch branch that computes the output path (`report_path(input, 'x')`) and calls your `generate`. Add the mode to the GUI's radio list and `_worker_generate` if you want it in the UI.
**A new trace channel.** Sample it in the report's trace builder via `sample_at(lap, 'ChannelName', dists)`, add it to the trace header and `resample.format_trace_row`, and add a `LEGEND_*` string in `report_common.py` so the column is documented.
**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.
## 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.
**Many channels are all-zero in this game version** — `Torque`, `IcePower`, ride heights, suspension loads, the per-section tyre temps, `Clutch`, `Handbrake`, and more. The parser drops any column that is entirely zero, so downstream code must treat a missing channel as "not recorded", not "error".
**`WheelSlip*` is a km/h speed difference, not a ratio.** A front value of −4.5 under braking is normal. Lock/spin are therefore computed as *relative* slip (`slip / max(speed, 30) * 100`) with thresholds at −10 % / +8 %; raw slip values are not comparable across speeds, and front-vs-rear baselines differ.
**`Steer` is a percent (−100..100), not a fraction.** Small values on a straight are still percent.
**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 the `L<digits>` token.** `lap_token_present` is the single source of truth for "already processed". Changing the token format means changing both `insert_lap_token` and `lap_token_present` together, or rename stops being safe to re-run.
**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 computes it from the race folder's parent. Moving the tool does not move where reports land.

58
ARCHITECTURE.ru.md Normal file
View file

@ -0,0 +1,58 @@
[English](ARCHITECTURE.md)
# Архитектура
CSV-телеметрия превращается в Markdown-отчёт через короткий линейный конвейер. Всё на стандартной библиотеке Python; ни фреймворка, ни глобального состояния.
## Файлы
```
telemetry/
__main__.py Точка входа CLI: разбор аргументов, диспатч режима, stdout. Без аргументов -> GUI.
parser.py CSV -> Lap (frozen dataclass: session, track, setup, ch, n).
segments.py Lap -> повороты и прямые. Детекция зон + нумерация.
corner_metrics.py Подсчёт по зоне: точка торможения, lock/spin, накат, температуры.
resample.py Адаптивная сетка по дистанции + интерполяция каналов для трассы.
report_common.py Шапка, строки легенды, md_table, token_estimate, атомарная запись.
report_technique.py Отчёт о пилотировании (повороты, прямые, расход, трасса).
report_setup.py Отчёт о машине (сетап, баланс, шины, тормоза, подвеска, фазы).
report_compare.py Два+ круга рядом, дельта по поворотам и накопленная.
report_race.py Папка кругов: лаптаймы, стинты, износ, тепло, ERS.
rename.py Вставка номера круга в имена файлов; пропуск обработанных.
gui.py Окно tkinter; генерация/переименование в рабочем потоке.
prompts/ Русские промпты для LLM, по одному на режим.
tests/ Набор pytest + фикстуры-круги.
```
## Как CSV становится отчётом
1. **`__main__.main`** разбирает `<режим> <файлы…>`, проверяет количество (technique/setup = 1 файл, compare = 2+, race = 1 папка) и диспатчит.
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`** собирает Markdown из помощников `report_common` (`header_block`, `md_table`, `legend`) и данных выше, затем вызывает **`report_common.write_report`**, которая пишет атомарно (временный файл + `os.replace`) и возвращает `(abs_path, tokens)`.
6. **`__main__`** печатает путь и `~N tokens (budget 60k)`. **GUI** показывает тот же кортеж в логе вместо печати.
## Как добавлять
**Новый режим отчёта.** Создай `telemetry/report_x.py` с `generate(lap, out_path) -> tuple[str, int]` (или `generate(laps, out_path)` для multi-lap). Собери текст из помощников `report_common` и заверши `return write_report(out_path, text)`. В `__main__.py` добавь `'x'` в список режимов и ветку диспатча, которая считает путь вывода (`report_path(input, 'x')`) и вызывает твой `generate`. Добавь режим в радиокнопки GUI и `_worker_generate`, если нужен в интерфейсе.
**Новый канал трассы.** Сэмплируй его в построителе трассы через `sample_at(lap, 'ChannelName', dists)`, добавь в заголовок трассы и `resample.format_trace_row`, и заведи строку `LEGEND_*` в `report_common.py`, чтобы колонка была задокументирована.
**Новая метрика поворота.** Добавь поле в dataclass `Corner` в `segments.py` и посчитай его в `corner_metrics.corner_from_zone`. Поскольку `Corner` — frozen, задавай через конструктор.
## Подводные камни
**Файлы в UTF-8, а кодировка ОС здесь — cp1251.** `parser.load_lap` всегда передаёт `encoding='utf-8'`; открытие файла телеметрии дефолтным кодеком падает с `UnicodeDecodeError` на первом не-ASCII байте. `rename.read_lap_number` читает только первые пять строк, но обязан использовать ту же кодировку.
**Многие каналы в этой версии игры целиком нулевые** — `Torque`, `IcePower`, высоты подвески, нагрузки, посекционные температуры шин, `Clutch`, `Handbrake` и другие. Парсер отбрасывает любую полностью нулевую колонку, поэтому код ниже должен трактовать отсутствие канала как «не записан», а не «ошибка».
**`WheelSlip*` — это разница скоростей в км/ч, не отношение.** Переднее значение −4.5 при торможении — норма. Поэтому lock/spin считаются как *относительное* скольжение (`slip / max(speed, 30) * 100`) с порогами −10 % / +8 %; сырые значения слипа несравнимы между скоростями, и базовые уровни передней/задней оси различаются.
**`Steer` — это процент (−100..100), не доля.** Малые значения на прямой — всё равно проценты.
**Строка сетапа заканчивается лишней запятой** (пустой хвост `FuelLoad` в части экспортов). Парсер отбрасывает пустые хвостовые поля, чтобы в словаре сетапа не было ключа с пустой строкой.
**Идемпотентность rename держится на токене `L<цифры>`.** `lap_token_present` — единственный источник истины для «уже обработан». Смена формата токена требует менять `insert_lap_token` и `lap_token_present` вместе, иначе rename перестаёт быть безопасным для повторного запуска.
**Отчёты выходят рядом с входом, а не рядом с программой.** `report_path` выводит папку `reports/` из папки *входного CSV*; режим `race` считает её от родителя папки гонки. Перемещение инструмента не меняет, куда попадают отчёты.

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Andrew Pimenov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

97
README.md Normal file
View file

@ -0,0 +1,97 @@
[Русский](README.ru.md)
# F1 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.
```bash
# from the tool/ folder
python -m telemetry technique ../zandvoort_P_L7_74.074_ferrari.csv
# -> ../reports/zandvoort_P_L7_74.074_ferrari_technique.md (~34k tokens)
```
Or just double-click `run-gui.bat` and click **Generate report**.
## Install
- Python 3.10+ (uses only the standard library — no `pip install`).
- Tk is required for the GUI; it ships with the standard Python installer on Windows/macOS. The CLI works without it.
Download or clone the repository. Keep your telemetry CSVs in the parent folder of `tool/` (see [folder layout](#folder-layout)).
## What it does
Five modes. Four produce reports, one renames files.
| 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 |
| `rename` | files/folder | insert the lap number into each filename (`..._74.074_...` → `..._L7_74.074_...`) |
Reports include a legend explaining every column, so the LLM understands the data without extra prompting. Ready-made analysis prompts live in [`prompts/`](prompts/) — one per mode.
### Folder layout
The program lives in `tool/`; your telemetry sits one level up, output lands next to it:
```
Telemetry/
zandvoort_P_L7_74.074_ferrari.csv <- raw laps here
races/
Zandvoort_Race_2026/ <- one folder per race (for `race` mode)
reports/ <- generated reports land here
tool/ <- this repository
telemetry/
prompts/
run-gui.bat
```
## 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, browse to the file/folder, click Generate. The **Rename unprocessed** button adds lap numbers to files that don't have them yet and leaves the rest alone.
**CLI** — from the `tool/` folder:
```bash
python -m telemetry technique ../lap.csv
python -m telemetry setup ../lap.csv
python -m telemetry compare ../lap_a.csv ../lap_b.csv
python -m telemetry race ../races/Zandvoort_Race_2026
python -m telemetry rename ../races/Zandvoort_Race_2026 # or individual files
```
Every report run prints the output path and a token estimate:
```
C:\...\Telemetry\reports\lap_technique.md
~33834 tokens (budget 60k)
```
**Rename only touches unprocessed files.** A file is "processed" once its name contains an `L<number>` token, so re-running rename is safe and idempotent.
## 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.
## For developers
Pure-stdlib Python. Pipeline: `parser` → `segments` (+ `corner_metrics`) → `resample` → `report_*`. See [ARCHITECTURE.md](ARCHITECTURE.md).
```bash
cd tool
python -m pytest -q # run the test suite
python -m telemetry gui # launch the GUI
```
Adding a mode: drop a `telemetry/report_<mode>.py` exposing `generate(...) -> (abs_path, tokens)` and wire one line into `telemetry/__main__.py`. The parser, corner detection, adaptive trace, and shared report blocks are reused.
## Credits
Built for analysing personal F1 22 hotlaps. Telemetry export format from [F1Laps](https://www.f1laps.com).
## License
MIT — see [LICENSE](LICENSE).

97
README.ru.md Normal file
View file

@ -0,0 +1,97 @@
[English](README.md)
# F1 Telemetry Analyzer
Превращает сырую телеметрию круга F1 22 в компактные Markdown-отчёты для анализа через LLM. Сырой круг — около 1М токенов; отчёты — 1–35k.
```bash
# из папки tool/
python -m telemetry technique ../zandvoort_P_L7_74.074_ferrari.csv
# -> ../reports/zandvoort_P_L7_74.074_ferrari_technique.md (~34k токенов)
```
Или просто двойной клик по `run-gui.bat` и кнопка **Generate report**.
## Установка
- Python 3.10+ (только стандартная библиотека — без `pip install`).
- Для GUI нужен Tk; он идёт в стандартном установщике Python на Windows/macOS. CLI работает и без него.
Скачай или склонируй репозиторий. Держи CSV-файлы телеметрии в папке на уровень выше `tool/` (см. [структуру папок](#структура-папок)).
## Что умеет
Пять режимов. Четыре строят отчёты, один переименовывает файлы.
| Режим | Вход | Для анализа |
|-------|------|-------------|
| `technique` | один круг | **как** едешь — точки торможения, скорости в апексах, работа газом, блокировки, расход ресурсов |
| `setup` | один круг | **что** настроено — баланс (US/OS), температуры шин, тормоза, rake и крен подвески |
| `compare` | два+ круга | **где** один круг быстрее — дельта по поворотам и накопленная |
| `race` | папка кругов | вся гонка — темп, деградация в стинтах, износ резины, ERS по кругам |
| `rename` | файлы/папка | вставить номер круга в имя файла (`..._74.074_...` → `..._L7_74.074_...`) |
Отчёты содержат легенду с пояснением каждой колонки — LLM понимает данные без дополнительных подсказок. Готовые промпты для анализа лежат в [`prompts/`](prompts/) — по одному на режим.
### Структура папок
Программа живёт в `tool/`; телеметрия — на уровень выше, отчёты выходят рядом с ней:
```
Telemetry/
zandvoort_P_L7_74.074_ferrari.csv <- сырые круги здесь
races/
Zandvoort_Race_2026/ <- по папке на гонку (для режима `race`)
reports/ <- сюда выходят отчёты
tool/ <- этот репозиторий
telemetry/
prompts/
run-gui.bat
```
## Полезное
**GUI** — двойной клик по `run-gui.bat` (или `run-gui.pyw` без окна консоли, либо `python -m telemetry gui`). Выбери режим, укажи файл/папку, нажми Generate. Кнопка **Rename unprocessed** добавляет номера круга к файлам, у которых их ещё нет, и не трогает остальные.
**CLI** — из папки `tool/`:
```bash
python -m telemetry technique ../lap.csv
python -m telemetry setup ../lap.csv
python -m telemetry compare ../lap_a.csv ../lap_b.csv
python -m telemetry race ../races/Zandvoort_Race_2026
python -m telemetry rename ../races/Zandvoort_Race_2026 # или отдельные файлы
```
Каждый прогон печатает путь к отчёту и оценку токенов:
```
C:\...\Telemetry\reports\lap_technique.md
~33834 tokens (budget 60k)
```
**Rename трогает только необработанные файлы.** Файл считается обработанным, как только в имени появляется токен `L<номер>` — поэтому повторный запуск безопасен и идемпотентен.
## Входной формат
Телеметрия F1 22 (UDP), экспортированная в CSV (тестировалось на [F1Laps](https://www.f1laps.com)). UTF-8, ~1800 строк на круг (≈29 Гц), 108 каналов. Один файл = один круг. Для режима `race` сложи все круги гонки в одну папку.
## Для разработчиков
Чистый Python на стандартной библиотеке. Конвейер: `parser` → `segments` (+ `corner_metrics`) → `resample` → `report_*`. См. [ARCHITECTURE.ru.md](ARCHITECTURE.ru.md).
```bash
cd tool
python -m pytest -q # прогнать тесты
python -m telemetry gui # запустить GUI
```
Новый режим: добавь `telemetry/report_<mode>.py` с функцией `generate(...) -> (abs_path, tokens)` и одну строку в `telemetry/__main__.py`. Парсер, детекция поворотов, адаптивная трасса и общие блоки отчётов переиспользуются.
## Благодарности
Сделано для разбора личных хотлапов в F1 22. Формат экспорта телеметрии — [F1Laps](https://www.f1laps.com).
## Лицензия
MIT — см. [LICENSE](LICENSE).

64
prompts/compare.md Normal file
View file

@ -0,0 +1,64 @@
Ты — гоночный инженер, сравнивающий два круга одного пилота. Первый круг (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 лучше тянет (раньше газ или меньше буксовки)
- **Прямые**: разница скоростей на прямой = результат предыдущего выхода из поворота, а не самой прямой
### 4. Паттерны
- Один круг систематически лучше в медленных поворотах, другой — в быстрых?
- Один пилот (или стиль) рискует на входах, другой — на выходах?
- Есть ли повороты, где «медленный» круг на самом деле быстрее в этом конкретном месте — и почему?
### 5. Итог и приоритеты
- **Что взять из быстрого круга**: конкретные повороты и техника
- **Приоритетные зоны для работы**: где наибольший потенциал (самые большие `|Δt seg|`)
- **Что не копировать**: если в «быстром» круге есть места, где он всё же медленнее — зафиксируй
---
Ниже прикреплён отчёт сравнения. Начинай анализ.

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

@ -0,0 +1,62 @@
You are a race engineer comparing two laps from the same driver in F1 22. 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, 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)
- **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.

96
prompts/en/race.md Normal file
View file

@ -0,0 +1,96 @@
You are a race engineer analysing full-race telemetry in F1 22. The report covers all laps: lap times, stints, tyre wear, thermals, and ERS. 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 (C1–C5, I, W)
- `Age` — laps on this set (1 = first lap on fresh set)
- `Fuel kg` — fuel at lap start [kg]
- `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
**ERS per lap**:
- `ERS MJ` — total energy from ERS over the lap (cumulative deployed at lap end)
- `MGU-K MJ` — MGU-K recovery
- `MGU-H MJ` — MGU-H recovery
---
## 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, 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
- For the first (or long) stint: correlate `Fuel kg` and `Time` per lap
- How much does fuel effect explain early-stint pace improvement? (typical ≈ 0.03–0.05 s/kg)
- If early-stint pace is poor — tyre warm-up or fuel load?
- Were early-stint laps too slow relative to the fuel correction
### 5. Thermal management
From `Thermal summary`:
- How do tyre temps evolve through each stint: warm-up, stable, overheating?
- Signs of overheating (sharp `Tf` or `Tr` rise with simultaneous pace drop)?
- 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 load?
- Why did the best lap happen when it did — track rubber, late braking, cool air?
- 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.

91
prompts/en/setup.md Normal file
View file

@ -0,0 +1,91 @@
You are an experienced F1 22 race engineer. The following is a car setup report for one lap. Perform a detailed setup analysis: what is configured, how it affects car behaviour, what should be changed.
## Data format
**Setup** — full parameter table:
- `FWing / RWing` — front/rear wing angle [0–50]
- `OnThrottle / OffThrottle` — differential lock on/off throttle [%]
- `FrontCamber / RearCamber` — front/rear wheel camber [°], negative = tilted inward
- `FrontToe / RearToe` — front/rear toe [°]
- `FrontSusp / RearSusp` — spring stiffness [1–11]
- `FrontAntiRoll / RearAntiRoll` — anti-roll bar stiffness [1–11]
- `FrontSuspH / RearSuspH` — ride height [mm]
- `BrakePressure` — maximum brake system pressure [%]
- `BrakeBias` — front/rear brake balance [%], >50 = forward bias
- `FLTyrePressure / FRTyrePressure / RLTyrePressure / RRTyrePressure` — cold tyre pressure [PSI]
- `Ballast` — ballast
- `FuelLoad` — fuel at start
**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. Aerodynamic balance
- FWing/RWing ratio: front/rear downforce balance
- How high is the downforce level for this track type (many slow corners = more needed, high-speed = can reduce drag)
- Any potential to cut drag without hurting balance
### 2. Mechanical balance
- Corner balance: systematic US or OS across most corners?
- Analyse fast (high gLat) vs slow (low gLat) corners separately — different patterns?
- Link imbalance to specific settings: F/R spring stiffness, anti-roll bars, camber, toe — most likely cause
- Differential (OnThrottle/OffThrottle): effect on exits — high OnThrottle = more traction but harder to rotate on exit
### 3. Tyre thermal window
- Optimal operating temperature for dry tyres ≈ 80–110°C surface
- 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) vs cold (from Setup): delta shows how much the tyre heated
- Too-high hot pressure = overheating; too-low = under-temperature
- F/R pressure balance: asymmetry affects aero and mechanical balance
### 5. Brakes
- BrakeBias: evaluate against temperatures — if rears are hotter, bias is rearward; if fronts, forward
- Temperature range: <200°C = cold (poor braking), 400–900°C = normal, >1000°C = overheating
- L/R asymmetry: different temperatures on same axle = uneven pad wear or balance issue
### 6. Suspension
- Rake: positive = nose higher than tail — standard for downforce; very large = risk of nose "sticking" at speed
- F/R stiffness: stiffer front = OS tendency; stiffer rear = US
- Anti-roll bars: high values = less body roll but more sensitive to bumps; link to Roll per corner
- Roll per corner: typical roll 3–6 mm; large roll = soft suspension
### 7. Recommendations
Per identified issue — what specifically to change (parameter, direction) and expected effect. Prioritise by lap time impact.
---
Below is the attached report. Begin analysis.

91
prompts/en/technique.md Normal file
View file

@ -0,0 +1,91 @@
You are an experienced racing engineer. The following is single-lap telemetry from F1 22. 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, DRS, time.
**Driving cost** — whole lap: fuel used, per-tyre wear, ERS (spent/harvested), peak brake temperatures.
**Trace** — per-sample telemetry by distance:
`dist` `time` `spd` `thr` `brk` `steer` `gear` `drs` `gLat` `gLon` `fuel` `ers` `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. ERS management
From Trace (`ers` — cumulative MJ deployed):
- Where ERS is used most heavily — straights or corner exits
- How much of the lap budget is consumed (compare with Driving cost)
- Recovery balance (MGU-K vs MGU-H)
### 6. Thermal management
- `Tf / Tr` from Trace: how temperatures evolve through the lap, where peaks occur, any overheating signs
- `Tr peak` per corner: which corners load the rear tyres hardest
- Brake temps from Driving cost: overheating (>1000°C is critical), 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.

99
prompts/race.md Normal file
View file

@ -0,0 +1,99 @@
Ты — гоночный инженер, анализирующий телеметрию целой гонки. Перед тобой отчёт по всем кругам: времена, стинты, износ шин, тепловой режим и ERS. Проведи детальный разбор гонки.
## Формат данных
Отчёт содержит:
**Lap times** — таблица кругов:
- `Lap` — номер круга
- `Time` — время круга в формате `M:SS.mmm`
- `S1 / S2 / S3` — времена секторов [с]
- `Tyre` — состав шины (C1–C5, I, W)
- `Age` — количество кругов на данном комплекте (тирейдж, 1 = первый круг на новом комплекте)
- `Fuel kg` — топливо на старте круга [кг]
- `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` — заднего правого тормоза
**ERS per lap** — энергетика:
- `ERS MJ` — суммарная энергия из ERS за круг (накопленный расход в конце круга)
- `MGU-K MJ` — рекуперация MGU-K
- `MGU-H MJ` — рекуперация MGU-H
---
## Задача
Разбери каждый из пунктов. Каждый вывод подкрепляй конкретными числами из таблиц — не делай общих утверждений без ссылки на данные.
### 1. Общая картина гонки
- Как менялся темп по ходу гонки: прогрессия или деградация?
- Были ли выбросы (аномально медленные или быстрые круги) — назови номера кругов и причину (PIT, INV, safety car?)
- Сравни лучший и средний темп: насколько велик разрыв?
- Итоговая оценка гонки в одном предложении
### 2. Анализ стинтов
По каждому стинту из `Stint summary`:
- Средний темп и его относительное качество (лучший / худший стинт?)
- Разброс `Best–Worst`: большой разброс = непостоянный темп, малый = стабильный
- Сравни стинты между собой: где пилот ехал быстрее и почему (шина, топливо, трек?)
- Был ли питстоп своевременным: не слишком ли рано/поздно по тренду деградации?
### 3. Деградация резины
По `Deg s/lap` и таблице `Tyre wear`:
- В каком стинте деградация наибольшая и наименьшая?
- Сопоставь `Deg s/lap` с фактическим приростом износа по кругам — соответствуют ли друг другу?
- Когда резина «умерла»: определи круг, после которого темп резко упал
- Равномерность износа: есть ли асимметрия FL/FR или RL/RR?
### 4. Топливный эффект
- По первому стинту (или длинному стинту): сопоставь `Fuel kg` и `Time` по кругам
- Насколько топливный эффект объясняет улучшение темпа в начале стинта? (типичная оценка: ~0.03–0.05 с/кг)
- Если темп в начале стинта плохой — не шина, а топливо или прогрев?
- Оцени, были ли круги в начале стинта слишком медленными относительно топливной поправки
### 5. Тепловой менеджмент
По `Thermal summary`:
- Как меняются температуры шин по кругам в каждом стинте: прогрев, стабилизация, перегрев?
- Есть ли признаки перегрева (резкий рост `Tf` или `Tr` при одновременном ухудшении темпа)?
- Температуры тормозов: рост по дистанции или стабильные? Асимметрия FL vs RR?
- В каких кругах тепловой режим был критическим?
### 6. Лучшие и худшие круги
- `FAST` круг: в каком стинте, на каком тирейдже, при каком расходе топлива?
- Почему лучший круг получился лучшим — трек rubber? позднее торможение? холодный воздух?
- Худший действительный круг (не INV, не PIT): причина — деградация, трафик, ошибка?
- Есть ли интересные паттерны: «out lap» всегда медленный, «in lap» провальный по темпу?
### 7. Итог и рекомендации
- **Что сработало:** что в стратегии и пилотировании было сделано хорошо
- **Главные потери:** конкретные круги и зоны, где потеряно больше всего
- **Стратегические рекомендации:** момент питстопа, выбор состава, длина стинтов
- **Техника пилотирования:** если деградация аномальна — что нужно изменить в стиле езды
---
Ниже прикреплён отчёт гонки. Начинай анализ.

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

@ -0,0 +1,64 @@
Ты — гоночный инженер, сравнивающий два круга одного пилота в F1 22. Первый круг (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 лучше тянет (раньше газ или меньше буксовки)
- **Прямые**: разница скоростей на прямой = результат предыдущего выхода из поворота, а не самой прямой
### 4. Паттерны
- Один круг систематически лучше в медленных поворотах, другой — в быстрых?
- Один пилот (или стиль) рискует на входах, другой — на выходах?
- Есть ли повороты, где «медленный» круг на самом деле быстрее в этом конкретном месте — и почему?
### 5. Итог и приоритеты
- **Что взять из быстрого круга**: конкретные повороты и техника
- **Приоритетные зоны для работы**: где наибольший потенциал (самые большие `|Δt seg|`)
- **Что не копировать**: если в «быстром» круге есть места, где он всё же медленнее — зафиксируй
---
Ниже прикреплён отчёт сравнения. Начинай анализ.

99
prompts/ru/race.md Normal file
View file

@ -0,0 +1,99 @@
Ты — гоночный инженер, анализирующий телеметрию целой гонки в F1 22. Перед тобой отчёт по всем кругам: времена, стинты, износ шин, тепловой режим и ERS. Проведи детальный разбор гонки.
## Формат данных
Отчёт содержит:
**Lap times** — таблица кругов:
- `Lap` — номер круга
- `Time` — время круга в формате `M:SS.mmm`
- `S1 / S2 / S3` — времена секторов [с]
- `Tyre` — состав шины (C1–C5, I, W)
- `Age` — количество кругов на данном комплекте (тирейдж, 1 = первый круг на новом комплекте)
- `Fuel kg` — топливо на старте круга [кг]
- `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` — заднего правого тормоза
**ERS per lap** — энергетика:
- `ERS MJ` — суммарная энергия из ERS за круг (накопленный расход в конце круга)
- `MGU-K MJ` — рекуперация MGU-K
- `MGU-H MJ` — рекуперация MGU-H
---
## Задача
Разбери каждый из пунктов. Каждый вывод подкрепляй конкретными числами из таблиц — не делай общих утверждений без ссылки на данные.
### 1. Общая картина гонки
- Как менялся темп по ходу гонки: прогрессия или деградация?
- Были ли выбросы (аномально медленные или быстрые круги) — назови номера кругов и причину (PIT, INV, safety car?)
- Сравни лучший и средний темп: насколько велик разрыв?
- Итоговая оценка гонки в одном предложении
### 2. Анализ стинтов
По каждому стинту из `Stint summary`:
- Средний темп и его относительное качество (лучший / худший стинт?)
- Разброс `Best–Worst`: большой разброс = непостоянный темп, малый = стабильный
- Сравни стинты между собой: где пилот ехал быстрее и почему (шина, топливо, трек?)
- Был ли питстоп своевременным: не слишком ли рано/поздно по тренду деградации?
### 3. Деградация резины
По `Deg s/lap` и таблице `Tyre wear`:
- В каком стинте деградация наибольшая и наименьшая?
- Сопоставь `Deg s/lap` с фактическим приростом износа по кругам — соответствуют ли друг другу?
- Когда резина «умерла»: определи круг, после которого темп резко упал
- Равномерность износа: есть ли асимметрия FL/FR или RL/RR?
### 4. Топливный эффект
- По первому стинту (или длинному стинту): сопоставь `Fuel kg` и `Time` по кругам
- Насколько топливный эффект объясняет улучшение темпа в начале стинта? (типичная оценка: ~0.03–0.05 с/кг)
- Если темп в начале стинта плохой — не шина, а топливо или прогрев?
- Оцени, были ли круги в начале стинта слишком медленными относительно топливной поправки
### 5. Тепловой менеджмент
По `Thermal summary`:
- Как меняются температуры шин по кругам в каждом стинте: прогрев, стабилизация, перегрев?
- Есть ли признаки перегрева (резкий рост `Tf` или `Tr` при одновременном ухудшении темпа)?
- Температуры тормозов: рост по дистанции или стабильные? Асимметрия FL vs RR?
- В каких кругах тепловой режим был критическим?
### 6. Лучшие и худшие круги
- `FAST` круг: в каком стинте, на каком тирейдже, при каком расходе топлива?
- Почему лучший круг получился лучшим — трек rubber? позднее торможение? холодный воздух?
- Худший действительный круг (не INV, не PIT): причина — деградация, трафик, ошибка?
- Есть ли интересные паттерны: «out lap» всегда медленный, «in lap» провальный по темпу?
### 7. Итог и рекомендации
- **Что сработало:** что в стратегии и пилотировании было сделано хорошо
- **Главные потери:** конкретные круги и зоны, где потеряно больше всего
- **Стратегические рекомендации:** момент питстопа, выбор состава, длина стинтов
- **Техника пилотирования:** если деградация аномальна — что нужно изменить в стиле езды
---
Ниже прикреплён отчёт гонки. Начинай анализ.

91
prompts/ru/setup.md Normal file
View file

@ -0,0 +1,91 @@
Ты — опытный гоночный инженер. Перед тобой отчёт по настройкам машины за один круг в F1 22. Проведи детальный разбор сетапа: что настроено, как это влияет на поведение машины, что стоит изменить.
## Формат данных
**Setup** — полная таблица параметров:
- `FWing / RWing` — угол переднего / заднего антикрыла [0–50]
- `OnThrottle / OffThrottle` — блокировка дифференциала на газу / без газа [%]
- `FrontCamber / RearCamber` — развал передних / задних колёс [°], отрицательный = наклон внутрь
- `FrontToe / RearToe` — схождение передних / задних колёс [°]
- `FrontSusp / RearSusp` — жёсткость пружин подвески [1–11]
- `FrontAntiRoll / RearAntiRoll` — жёсткость стабилизатора поперечной устойчивости [1–11]
- `FrontSuspH / RearSuspH` — высота подвески (ride height) [мм]
- `BrakePressure` — максимальное давление в тормозной системе [%]
- `BrakeBias` — баланс тормозного усилия перед/зад [%], >50 = смещение вперёд
- `FLTyrePressure / FRTyrePressure / RLTyrePressure / RRTyrePressure` — давление шин холодное [PSI]
- `Ballast` — балласт
- `FuelLoad` — топливо на старт
**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. Аэродинамический баланс
- Соотношение FWing/RWing: баланс прижимной силы F/R
- Насколько высокая прижимная сила относительно типа трассы (много медленных поворотов = нужно больше, высокоскоростные = можно снизить сопротивление)
- Есть ли потенциал для снижения лобового сопротивления без ущерба балансу
### 2. Механический баланс
- Corner balance: есть ли систематический US или OS по большинству поворотов
- Отдельно рассмотри быстрые (высокий gLat) vs медленные (низкий gLat) повороты — разные ли паттерны
- Вяжи дисбаланс с конкретными настройками: жёсткость F/R подвески, антикрены, развал, схождение — что наиболее вероятная причина
- Дифференциал (OnThrottle/OffThrottle): как влияет на выходы — высокий OnThrottle = больше тяги, но труднее управлять при развороте
### 3. Тепловой режим шин
- Оптимальное окно рабочей температуры для сухих шин — примерно 80–110°C поверхности
- Попадают ли шины в рабочий диапазон; кто перегревается, кто недогревается
- Разница поверхности и каркаса: большая разница = шина перегревается снаружи, но каркас холодный (риск расслоения), или наоборот
- Износ: равномерный ли по 4 колёсам — аномально высокий износ одного колеса указывает на дисбаланс нагрузки
- Lap phases: нормальный ли прогрев — должен расти в первой фазе и стабилизироваться
### 4. Давление шин
- Горячее давление (из Tyres) vs холодное (из Setup): дельта показывает, насколько шина нагрелась
- Слишком высокое горячее давление = шина перегревается; слишком низкое = недогрев
- Баланс F/R давления: асимметрия влияет на аэро- и механический баланс
### 5. Тормоза
- BrakeBias: оцени в контексте температур — если задние тормоза горячее, bias смещён назад; если передние — вперёд
- Диапазон температур: <200°C = холодные (плохое торможение), 400–900°C = норма, >1000°C = перегрев
- Асимметрия L/R: разные температуры на одной оси = неравномерный износ колодок или проблема с балансом
### 6. Подвеска
- Rake: положительный = нос выше кормы — стандарт для прижимной силы; если очень большой — риск "прилипания" носа на скорости
- Жёсткость F/R: более жёсткая передняя = склонность к OS; более жёсткая задняя = US
- Антикрены: высокие значения = машина меньше кренится, но чувствительнее к буграм; связывай с Roll в поворотах
- Roll по поворотам: нормальный крен 3–6 мм; большой крен = мягкая подвеска
### 7. Рекомендации
По каждой выявленной проблеме — что конкретно менять (параметр, направление изменения) и каков ожидаемый эффект. Приоритизируй по влиянию на время круга.
---
Ниже прикреплён отчёт. Начинай анализ.

91
prompts/ru/technique.md Normal file
View file

@ -0,0 +1,91 @@
Ты — опытный инженер-аналитик гоночной команды. Перед тобой телеметрия одного круга в F1 22. Проведи максимально детальный разбор стиля пилотирования.
## Формат данных
Отчёт содержит:
**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, DRS, время.
**Driving cost** — за весь круг: расход топлива, износ каждой шины, ERS (потрачено / рекуперировано), пиковые температуры тормозов.
**Trace** — построчная телеметрия по дистанции:
`dist` `time` `spd` `thr` `brk` `steer` `gear` `drs` `gLat` `gLon` `fuel` `ers` `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. ERS-менеджмент
По Trace (`ers` — накопленный расход в МДж):
- Где ERS расходуется активнее — прямые или выходы из поворотов
- Полнота использования запаса за круг (сравни с Driving cost)
- Темп рекуперации (MGU-K vs MGU-H): баланс
### 6. Тепловой режим
- `Tf / Tr` по Trace: как меняется температура по кругу, где пики, есть ли перегрев
- `Tr peak` по поворотам: какие повороты наиболее нагружают задние шины
- Brake temps из Driving cost: перегрев (>1000°C критично), асимметрия L/R или F/R
### 7. Итог
- **Топ-3 зоны потери времени** с конкретными поворотами и причинами
- **Топ-3 сильные стороны** — что делается хорошо и почему
- **Рекомендации** по каждой проблемной зоне: что конкретно изменить в технике пилотирования
---
Ниже прикреплён отчёт. Начинай анализ.

91
prompts/setup.md Normal file
View file

@ -0,0 +1,91 @@
Ты — опытный гоночный инженер F1. Перед тобой отчёт по настройкам машины за один круг. Проведи детальный разбор сетапа: что настроено, как это влияет на поведение машины, что стоит изменить.
## Формат данных
**Setup** — полная таблица параметров:
- `FWing / RWing` — угол переднего / заднего антикрыла [0–50]
- `OnThrottle / OffThrottle` — блокировка дифференциала на газу / без газа [%]
- `FrontCamber / RearCamber` — развал передних / задних колёс [°], отрицательный = наклон внутрь
- `FrontToe / RearToe` — схождение передних / задних колёс [°]
- `FrontSusp / RearSusp` — жёсткость пружин подвески [1–11]
- `FrontAntiRoll / RearAntiRoll` — жёсткость стабилизатора поперечной устойчивости [1–11]
- `FrontSuspH / RearSuspH` — высота подвески (ride height) [мм]
- `BrakePressure` — максимальное давление в тормозной системе [%]
- `BrakeBias` — баланс тормозного усилия перед/зад [%], >50 = смещение вперёд
- `FLTyrePressure / FRTyrePressure / RLTyrePressure / RRTyrePressure` — давление шин холодное [PSI]
- `Ballast` — балласт
- `FuelLoad` — топливо на старт
**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. Аэродинамический баланс
- Соотношение FWing/RWing: баланс прижимной силы F/R
- Насколько высокая прижимная сила относительно типа трассы (много медленных поворотов = нужно больше, высокоскоростные = можно снизить сопротивление)
- Есть ли потенциал для снижения лобового сопротивления без ущерба балансу
### 2. Механический баланс
- Corner balance: есть ли систематический US или OS по большинству поворотов
- Отдельно рассмотри быстрые (высокий gLat) vs медленные (низкий gLat) повороты — разные ли паттерны
- Вяжи дисбаланс с конкретными настройками: жёсткость F/R подвески, антикрены, развал, схождение — что наиболее вероятная причина
- Дифференциал (OnThrottle/OffThrottle): как влияет на выходы — высокий OnThrottle = больше тяги, но труднее управлять при развороте
### 3. Тепловой режим шин
- Оптимальное окно рабочей температуры для сухих шин — примерно 80–110°C поверхности
- Попадают ли шины в рабочий диапазон; кто перегревается, кто недогревается
- Разница поверхности и каркаса: большая разница = шина перегревается снаружи, но каркас холодный (риск расслоения), или наоборот
- Износ: равномерный ли по 4 колёсам — аномально высокий износ одного колеса указывает на дисбаланс нагрузки
- Lap phases: нормальный ли прогрев — должен расти в первой фазе и стабилизироваться
### 4. Давление шин
- Горячее давление (из Tyres) vs холодное (из Setup): дельта показывает, насколько шина нагрелась
- Слишком высокое горячее давление = шина перегревается; слишком низкое = недогрев
- Баланс F/R давления: асимметрия влияет на аэро- и механический баланс
### 5. Тормоза
- BrakeBias: оцени в контексте температур — если задние тормоза горячее, bias смещён назад; если передние — вперёд
- Диапазон температур: <200°C = холодные (плохое торможение), 400–900°C = норма, >1000°C = перегрев
- Асимметрия L/R: разные температуры на одной оси = неравномерный износ колодок или проблема с балансом
### 6. Подвеска
- Rake: положительный = нос выше кормы — стандарт для прижимной силы; если очень большой — риск "прилипания" носа на скорости
- Жёсткость F/R: более жёсткая передняя = склонность к OS; более жёсткая задняя = US
- Антикрены: высокие значения = машина меньше кренится, но чувствительнее к буграм; связывай с Roll в поворотах
- Roll по поворотам: нормальный крен 3–6 мм; большой крен = мягкая подвеска
### 7. Рекомендации
По каждой выявленной проблеме — что конкретно менять (параметр, направление изменения) и каков ожидаемый эффект. Приоритизируй по влиянию на время круга.
---
Ниже прикреплён отчёт. Начинай анализ.

91
prompts/technique.md Normal file
View file

@ -0,0 +1,91 @@
Ты — опытный инженер-аналитик гоночной команды. Перед тобой телеметрия одного круга в F1 22. Проведи максимально детальный разбор стиля пилотирования.
## Формат данных
Отчёт содержит:
**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, DRS, время.
**Driving cost** — за весь круг: расход топлива, износ каждой шины, ERS (потрачено / рекуперировано), пиковые температуры тормозов.
**Trace** — построчная телеметрия по дистанции:
`dist` `time` `spd` `thr` `brk` `steer` `gear` `drs` `gLat` `gLon` `fuel` `ers` `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. ERS-менеджмент
По Trace (`ers` — накопленный расход в МДж):
- Где ERS расходуется активнее — прямые или выходы из поворотов
- Полнота использования запаса за круг (сравни с Driving cost)
- Темп рекуперации (MGU-K vs MGU-H): баланс
### 6. Тепловой режим
- `Tf / Tr` по Trace: как меняется температура по кругу, где пики, есть ли перегрев
- `Tr peak` по поворотам: какие повороты наиболее нагружают задние шины
- Brake temps из Driving cost: перегрев (>1000°C критично), асимметрия L/R или F/R
### 7. Итог
- **Топ-3 зоны потери времени** с конкретными поворотами и причинами
- **Топ-3 сильные стороны** — что делается хорошо и почему
- **Рекомендации** по каждой проблемной зоне: что конкретно изменить в технике пилотирования
---
Ниже прикреплён отчёт. Начинай анализ.

2
pytest.ini Normal file
View file

@ -0,0 +1,2 @@
[pytest]
testpaths = tests

3
run-gui.bat Normal file
View file

@ -0,0 +1,3 @@
@echo off
cd /d "%~dp0"
python -m telemetry gui

7
run-gui.pyw Normal file
View file

@ -0,0 +1,7 @@
import sys
import os
sys.path.insert(0, os.path.dirname(__file__))
from telemetry.gui import main
main()

0
telemetry/__init__.py Normal file
View file

113
telemetry/__main__.py Normal file
View file

@ -0,0 +1,113 @@
import argparse
import os
import sys
from .parser import load_lap
from .report_common import report_path
from . import report_technique, report_setup, report_compare, report_race
from .rename import rename_unprocessed
def _print_result(abs_path: str, tokens: int) -> None:
print(abs_path)
print(f'~{tokens} tokens (budget 60k)')
def _run_rename(targets: list[str]) -> None:
expanded: list[str] = []
for t in targets:
p = os.path.abspath(t)
if os.path.isdir(p):
for name in os.listdir(p):
if name.lower().endswith('.csv'):
expanded.append(os.path.join(p, name))
elif p.lower().endswith('.csv'):
expanded.append(p)
if not expanded:
print('Error: no CSV targets resolved', file=sys.stderr)
sys.exit(2)
results = rename_unprocessed(targets)
for r in results:
if r.status == 'renamed':
print(f'renamed: {r.old} -> {r.new}')
elif r.status == 'skipped':
print(f'skipped: {r.old} (already has lap number)')
else:
print(f'error: {r.old} ({r.detail})')
def main() -> None:
# no args → GUI
if len(sys.argv) == 1:
from .gui import main as gui_main
gui_main()
return
parser = argparse.ArgumentParser(prog='python -m telemetry')
parser.add_argument('mode', choices=['technique', 'setup', 'compare', 'race', 'rename', 'gui'])
parser.add_argument('files', nargs='*')
parser.add_argument('--lang', choices=['ru', 'en'], default='ru')
args = parser.parse_args()
mode: str = args.mode
files: list[str] = args.files
lang: str = args.lang
if mode == 'gui':
from .gui import main as gui_main
gui_main()
return
if mode == 'rename':
if not files:
print('Error: rename requires at least one file or directory', file=sys.stderr)
sys.exit(2)
_run_rename(files)
return
if mode in ('technique', 'setup') and len(files) != 1:
print(f'Error: {mode} requires exactly 1 file', file=sys.stderr)
sys.exit(2)
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)
sys.exit(2)
if mode == 'technique':
lap = load_lap(files[0])
out = report_path(files[0], 'technique')
_print_result(*report_technique.generate(lap, out, lang))
elif mode == 'setup':
lap = load_lap(files[0])
out = report_path(files[0], 'setup')
_print_result(*report_setup.generate(lap, out, lang))
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))
elif mode == 'race':
race_dir = os.path.abspath(files[0])
csv_files = sorted(
f for f in os.listdir(race_dir) if f.lower().endswith('.csv')
)
if not csv_files:
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(
os.path.dirname(os.path.dirname(race_dir)), 'reports', dirname + '_race.md'
)
_print_result(*report_race.generate(laps, out, lang))
if __name__ == '__main__':
main()

142
telemetry/corner_metrics.py Normal file
View file

@ -0,0 +1,142 @@
from .parser import Lap
from .segments import (
Corner,
BRAKE_MARK, FULL_THROTTLE, LOCKUP_REL_PCT, SPIN_REL_PCT,
)
_SAFE_SPEED_FLOOR = 30.0
def _safe(lst: list[float], i: int, default: float = 0.0) -> float:
return lst[i] if i < len(lst) else default
def corner_from_zone(
lap: Lap,
ia: int,
ib: int,
number: int,
dist: list[float],
speed: list[float],
brake: list[float],
throttle: list[float],
gear: list[float],
glat: list[float],
laptime: list[float],
slip_fl: list[float],
slip_fr: list[float],
slip_rl: list[float],
slip_rr: list[float],
tyre_temp_rl: list[float],
tyre_temp_rr: list[float],
) -> Corner:
idxs = list(range(ia, ib + 1))
spds = [_safe(speed, i) for i in idxs]
v_entry = spds[0] if spds else 0.0
v_exit = spds[-1] if spds else 0.0
v_min = min(spds) if spds else 0.0
apex_i = idxs[spds.index(v_min)]
apex_m = dist[apex_i]
gear_apex = int(_safe(gear, apex_i))
brake_start_m: float | None = None
for i in idxs:
if _safe(brake, i) > BRAKE_MARK:
brake_start_m = dist[i]
break
brake_vals = [_safe(brake, i) for i in idxs]
brake_max = max(brake_vals) if brake_vals else 0.0
glat_vals = [abs(_safe(glat, i)) for i in idxs]
g_lat_max = max(glat_vals) if glat_vals else 0.0
full_throttle_m: float | None = None
apex_idx_in_idxs = spds.index(v_min)
post_apex = idxs[apex_idx_in_idxs:]
if laptime:
i = 0
while i < len(post_apex):
pi = post_apex[i]
if _safe(throttle, pi) >= FULL_THROTTLE:
t_start = _safe(laptime, pi)
j = i
while j < len(post_apex) and _safe(throttle, post_apex[j]) >= FULL_THROTTLE:
j += 1
t_end = _safe(laptime, post_apex[j - 1])
if (t_end - t_start) >= 0.3:
full_throttle_m = dist[pi]
break
i = j
else:
i += 1
else:
for pi in post_apex:
if _safe(throttle, pi) >= FULL_THROTTLE:
full_throttle_m = dist[pi]
break
time_zone = 0.0
if laptime and idxs:
time_zone = _safe(laptime, ib) - _safe(laptime, ia)
# lock_pct: min relative front slip under braking (negative = locking tendency)
brake_slip_vals = []
for i in idxs:
if _safe(brake, i) > 20:
front = min(_safe(slip_fl, i), _safe(slip_fr, i))
spd = max(_safe(speed, i), _SAFE_SPEED_FLOOR)
brake_slip_vals.append(front / spd * 100.0)
lock_pct = min(brake_slip_vals) if brake_slip_vals else 0.0
# spin_pct: max relative rear slip on throttle (positive = spinning tendency)
throttle_slip_vals = []
for i in idxs:
if _safe(throttle, i) > 80:
rear = max(_safe(slip_rl, i), _safe(slip_rr, i))
spd = max(_safe(speed, i), _SAFE_SPEED_FLOOR)
throttle_slip_vals.append(rear / spd * 100.0)
spin_pct = max(throttle_slip_vals) if throttle_slip_vals else 0.0
lockup = lock_pct < LOCKUP_REL_PCT
wheelspin = spin_pct > SPIN_REL_PCT
brake_off_m: float | None = None
if brake_start_m is not None:
brake_start_idx = next((i for i in idxs if dist[i] >= brake_start_m), None)
if brake_start_idx is not None:
for i in idxs[idxs.index(brake_start_idx):idxs.index(apex_i) + 1]:
if _safe(brake, i) > BRAKE_MARK:
brake_off_m = dist[i]
coast_m = 0.0
if brake_off_m is not None and full_throttle_m is not None and full_throttle_m > brake_off_m:
coast_m = full_throttle_m - brake_off_m
rear_tyre_temp_peak = 0.0
if tyre_temp_rl and tyre_temp_rr:
peaks = [max(_safe(tyre_temp_rl, i), _safe(tyre_temp_rr, i)) for i in idxs]
rear_tyre_temp_peak = max(peaks) if peaks else 0.0
return Corner(
number=number,
start_m=dist[ia],
end_m=dist[ib],
brake_start_m=brake_start_m,
v_entry=v_entry,
v_min=v_min,
apex_m=apex_m,
gear_apex=gear_apex,
full_throttle_m=full_throttle_m,
v_exit=v_exit,
brake_max=brake_max,
g_lat_max=g_lat_max,
time_s=time_zone,
lock_pct=lock_pct,
spin_pct=spin_pct,
coast_m=coast_m,
rear_tyre_temp_peak=rear_tyre_temp_peak,
lockup=lockup,
wheelspin=wheelspin,
)

228
telemetry/gui.py Normal file
View file

@ -0,0 +1,228 @@
import os
import queue
import threading
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 .rename import rename_unprocessed
# project root = parent of the tool\ dir
_PROJECT_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..'))
_REPORTS_DIR = os.path.join(_PROJECT_ROOT, 'reports')
def _race_output_path(race_dir: str) -> str:
dirname = os.path.basename(race_dir.rstrip('/\\'))
return os.path.join(_PROJECT_ROOT, 'reports', dirname + '_race.md')
class _App:
def __init__(self, root: tk.Tk) -> None:
self._root = root
root.title('F1 Telemetry Analyzer')
root.resizable(True, True)
self._mode = tk.StringVar(value='technique')
self._lang = tk.StringVar(value='ru')
self._selection: list[str] = []
self._log_queue: queue.Queue[str | None] = queue.Queue()
self._busy = False
self._build_ui()
self._poll_log()
def _build_ui(self) -> None:
pad = {'padx': 8, 'pady': 4}
mode_frame = ttk.LabelFrame(self._root, 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,
value=value).pack(side='left', padx=6, pady=4)
lang_frame = ttk.LabelFrame(self._root, text='Prompt language / Язык промпта')
lang_frame.pack(fill='x', **pad)
ttk.Radiobutton(lang_frame, text='RU', variable=self._lang,
value='ru').pack(side='left', padx=6, pady=4)
ttk.Radiobutton(lang_frame, text='EN', variable=self._lang,
value='en').pack(side='left', padx=6, pady=4)
input_frame = ttk.LabelFrame(self._root, text='Input')
input_frame.pack(fill='x', **pad)
self._path_var = tk.StringVar()
ttk.Entry(input_frame, textvariable=self._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)
btn_frame = ttk.Frame(self._root)
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)
ttk.Button(btn_frame, text='Open reports folder',
command=self._open_reports).pack(side='left', padx=4)
log_frame = ttk.LabelFrame(self._root, text='Log')
log_frame.pack(fill='both', expand=True, **pad)
self._log = scrolledtext.ScrolledText(log_frame, state='disabled',
wrap='word', height=16)
self._log.pack(fill='both', expand=True, padx=4, pady=4)
def _browse(self) -> None:
mode = self._mode.get()
init = _PROJECT_ROOT
if mode == 'race':
d = filedialog.askdirectory(title='Select race folder', initialdir=init)
if d:
self._selection = [d]
self._path_var.set(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))
else:
f = filedialog.askopenfilename(
title='Select CSV file', initialdir=init,
filetypes=[('CSV', '*.csv')])
if f:
self._selection = [f]
self._path_var.set(f)
def _validate_selection(self) -> str | None:
mode = self._mode.get()
sel = self._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'
return None
def _set_busy(self, busy: bool) -> None:
self._busy = busy
state = 'disabled' if busy else 'normal'
self._btn_generate.configure(state=state)
self._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 _poll_log(self) -> None:
while True:
try:
msg = self._log_queue.get_nowait()
except queue.Empty:
break
if msg is None:
self._set_busy(False)
else:
self._log_append(msg)
self._root.after(100, self._poll_log)
def _push(self, msg: str) -> None:
self._log_queue.put(msg)
def _on_generate(self) -> None:
err = self._validate_selection()
if err:
self._log_append(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),
daemon=True).start()
def _worker_generate(self, mode: str, sel: list[str], lang: str) -> None:
try:
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)')
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)')
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)')
elif mode == 'race':
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}')
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)')
except Exception as e:
self._push(f'✗ {e}')
finally:
self._log_queue.put(None)
def _on_rename(self) -> None:
d = filedialog.askdirectory(title='Select folder to rename', initialdir=_PROJECT_ROOT)
if not d:
return
self._set_busy(True)
threading.Thread(target=self._worker_rename, args=(d,), daemon=True).start()
def _worker_rename(self, d: str) -> None:
try:
results = rename_unprocessed([d])
if not results:
self._push('No CSV files found')
for r in results:
if r.status == 'renamed':
self._push(f'renamed: {r.old} -> {r.new}')
elif r.status == 'skipped':
self._push(f'skipped: {r.old} ({r.detail})')
else:
self._push(f'error: {r.old} ({r.detail})')
except Exception as e:
self._push(f'✗ {e}')
finally:
self._log_queue.put(None)
def _open_reports(self) -> None:
if not os.path.isdir(_REPORTS_DIR):
self._log_append(f'Reports folder does not exist yet: {_REPORTS_DIR}')
return
os.startfile(_REPORTS_DIR)
def main() -> None:
root = tk.Tk()
_App(root)
root.mainloop()

76
telemetry/parser.py Normal file
View file

@ -0,0 +1,76 @@
import re
from dataclasses import dataclass
def _strip_unit(name: str) -> str:
return re.sub(r'\s*[\[({][^\]})]*[\]})]\s*$', '', name).strip()
def _parse_header_row(line: str) -> list[str]:
return [_strip_unit(h) for h in line.rstrip('\n').split(',')]
def _parse_value_row(line: str) -> list[str]:
return line.rstrip('\n').split(',')
@dataclass(frozen=True)
class Lap:
session: dict[str, str]
track: dict[str, str]
setup: dict[str, str]
ch: dict[str, list[float]]
n: int
def load_lap(path: str) -> Lap:
with open(path, encoding='utf-8') as f:
raw = f.readlines()
session_keys = _parse_header_row(raw[1])
session_vals = _parse_value_row(raw[2])
session = dict(zip(session_keys, session_vals))
track_keys = _parse_header_row(raw[3])
track_vals = _parse_value_row(raw[4])
track = dict(zip(track_keys, track_vals))
setup_keys = _parse_header_row(raw[5])
setup_vals = _parse_value_row(raw[6])
# drop trailing empty from the trailing comma
setup_vals = [v for v in setup_vals if v.strip() != '']
setup = dict(zip(setup_keys[:len(setup_vals)], setup_vals))
tel_keys = _parse_header_row(raw[7])
n_cols = len(tel_keys)
columns: dict[str, list[float]] = {k: [] for k in tel_keys}
for line in raw[8:]:
line = line.rstrip('\n')
if not line.strip():
continue
parts = line.split(',')
if len(parts) < n_cols:
continue
try:
vals = [float(parts[i]) for i in range(n_cols)]
except ValueError:
continue
for k, v in zip(tel_keys, vals):
columns[k].append(v)
# drop all-zero channels
ch = {k: v for k, v in columns.items() if any(x != 0.0 for x in v)}
return Lap(session=session, track=track, setup=setup, ch=ch, n=len(next(iter(columns.values()), [])))
def laptime_s(lap: Lap) -> float:
return float(lap.session.get('laptime', 0.0))
def sector_times(lap: Lap) -> tuple[float, float, float]:
s1 = float(lap.session.get('S1', 0.0))
s2 = float(lap.session.get('S2', 0.0))
s3 = float(lap.session.get('S3', 0.0))
return s1, s2, s3

84
telemetry/rename.py Normal file
View file

@ -0,0 +1,84 @@
import os
import re
from dataclasses import dataclass
from .parser import _parse_header_row, _parse_value_row
def lap_token_present(stem: str) -> bool:
return any(re.fullmatch(r'L\d+', t) for t in stem.split('_'))
def insert_lap_token(filename: str, lap: int) -> str:
base, ext = os.path.splitext(filename)
tokens = base.split('_')
token = f'L{lap}'
for i, t in enumerate(tokens):
if re.fullmatch(r'\d+(\.\d+)?', t):
tokens.insert(i, token)
return '_'.join(tokens) + ext
if len(tokens) >= 2:
tokens.insert(2, token)
else:
tokens.append(token)
return '_'.join(tokens) + ext
def read_lap_number(path: str) -> int:
with open(path, encoding='utf-8') as f:
lines = [f.readline() for _ in range(5)]
keys = _parse_header_row(lines[3])
vals = _parse_value_row(lines[4])
track = dict(zip(keys, vals))
return int(float(track['Lap']))
@dataclass(frozen=True)
class RenameResult:
old: str
new: str | None
status: str # 'renamed' | 'skipped' | 'error'
detail: str
def rename_unprocessed(targets: list[str]) -> list[RenameResult]:
paths: list[str] = []
for t in targets:
t = os.path.abspath(t)
if os.path.isdir(t):
for name in os.listdir(t):
if name.lower().endswith('.csv'):
paths.append(os.path.join(t, name))
else:
paths.append(t)
results: list[RenameResult] = []
for p in paths:
name = os.path.basename(p)
if not os.path.isfile(p):
results.append(RenameResult(name, None, 'error', 'file not found'))
continue
if not name.lower().endswith('.csv'):
results.append(RenameResult(name, None, 'error', 'not a CSV file'))
continue
stem = os.path.splitext(name)[0]
if lap_token_present(stem):
results.append(RenameResult(name, None, 'skipped', 'already has lap number'))
continue
try:
lap = read_lap_number(p)
except Exception as e:
results.append(RenameResult(name, None, 'error', str(e)))
continue
new_name = insert_lap_token(name, lap)
dest = os.path.join(os.path.dirname(p), new_name)
if os.path.exists(dest):
results.append(RenameResult(name, new_name, 'error', 'target exists'))
continue
try:
os.rename(p, dest)
except OSError as e:
results.append(RenameResult(name, new_name, 'error', str(e)))
continue
results.append(RenameResult(name, new_name, 'renamed', ''))
return results

124
telemetry/report_common.py Normal file
View file

@ -0,0 +1,124 @@
import os
import tempfile
from .parser import Lap, laptime_s, sector_times
def header_block(lap: Lap) -> str:
s = lap.session
t = lap.track
lt = laptime_s(lap)
s1, s2, s3 = sector_times(lap)
lap_n = t.get('Lap', '?')
laps_total = t.get('LapsInRace', '?')
fuel_start = t.get('Fuel at Start', '?')
try:
fuel_start = f'{float(fuel_start):.2f} kg'
except (ValueError, TypeError):
pass
lines = [
f'**Track:** {s.get("track", "?")} ',
f'**Car:** {s.get("car", "?")} ',
f'**Event:** {s.get("event", "?")} ',
f'**Laptime:** {lt:.3f} s (S1 {s1:.3f} / S2 {s2:.3f} / S3 {s3:.3f}) ',
f'**Tyre:** {t.get("Tyre", "?")} ',
f'**Weather:** {t.get("Weather", "?")} ',
f'**Track temp:** {t.get("TrackTemp", "?")} °C **Air:** {t.get("AmbientTemp", "?")} °C ',
f'**Lap:** {lap_n} / {laps_total} ',
f'**Fuel at start:** {fuel_start} ',
]
return '\n'.join(lines)
def setup_summary(lap: Lap) -> str:
s = lap.setup
return (
f'FWing {s.get("FWing","?")} / RWing {s.get("RWing","?")} | '
f'BrakeBias {s.get("BrakeBias","?")}% | '
f'FL {s.get("FLTyrePressure","?")} FR {s.get("FRTyrePressure","?")} '
f'RL {s.get("RLTyrePressure","?")} RR {s.get("RRTyrePressure","?")} PSI'
)
LEGEND_DIST = '- `dist` = LapDistance [m]'
LEGEND_TIME = '- `time` = elapsed LapTime [s]'
LEGEND_SPD = '- `spd` = Speed [km/h]'
LEGEND_THR_BRK = '- `thr` / `brk` = ThrottlePercentage / BrakePercentage [0–100 %]'
LEGEND_STEER = '- `steer` = steering lock [%], negative = left, positive = right'
LEGEND_GEAR = '- `gear` = selected gear (2–8)'
LEGEND_DRS = '- `drs` = DRS active (0 = off, 1 = on)'
LEGEND_GLAT = '- `gLat` = lateral G-force [G], positive = right corner'
LEGEND_GLON = '- `gLon` = longitudinal G-force [G], negative = braking, positive = acceleration'
LEGEND_FUEL = '- `fuel` = FuelRemaining [kg]'
LEGEND_ERS = '- `ers` = ERSSpent [MJ], cumulative energy deployed from ERS'
LEGEND_LOCK = '- LOCK flag = front relative slip (slip km/h / speed km/h × 100) < −10 % while braking (severe lockup only)'
LEGEND_SPIN = '- SPIN flag = rear relative slip > +8 % while on throttle (severe wheelspin only)'
LEGEND_TF = '- `Tf` / `Tr` = surface tyre temperature front/rear average [°C]'
LEGEND_RPM = '- `rpm` = engine revs (rounded to nearest 100)'
LEGEND_SLIP = '- `slipF` / `slipR` = relative wheel slip front/rear [%], negative front = locking tendency, positive rear = wheelspin tendency'
LEGEND_YAW = '- `yaw` = yaw rate [rad/s] (LocalAngularVelocityY); large steer + small yaw = understeer'
LEGEND_SUSP = '- Suspension positions in mm; rake = front avg − rear avg; roll = left avg − right avg per corner (positive = left side compressed)'
LEGEND_DT_SEG = '- `Δt seg` = per-segment time delta (positive = lap X slower in that zone)'
LEGEND_DT_CUM = '- `Δt cum` = cumulative delta at zone end vs lap A [s], negative = lap X faster up to that point'
LEGEND_STEER_G_CAVEAT = '- steer/g and slip balance are speed-dependent — only compare corners of similar Vmin'
def legend(entries: list[str]) -> str:
return '## Legend\n' + '\n'.join(entries)
def md_table(headers: list[str], rows: list[list[str]]) -> str:
sep = ['---'] * len(headers)
lines = [
'| ' + ' | '.join(headers) + ' |',
'| ' + ' | '.join(sep) + ' |',
]
for row in rows:
lines.append('| ' + ' | '.join(str(c) for c in row) + ' |')
return '\n'.join(lines)
def token_estimate(text: str) -> int:
return int(len(text) / 3.5)
def write_report(path: str, text: str) -> tuple[str, int]:
os.makedirs(os.path.dirname(path), exist_ok=True)
dir_ = os.path.dirname(path)
fd, tmp = tempfile.mkstemp(dir=dir_, suffix='.tmp')
try:
# close the raw fd so we can open in text mode (platform newlines, UTF-8)
os.close(fd)
with open(tmp, 'w', encoding='utf-8') as f:
f.write(text)
os.replace(tmp, path)
except Exception:
try:
os.unlink(tmp)
except OSError:
pass
raise
tokens = token_estimate(text)
return os.path.abspath(path), tokens
def load_prompt(mode: str, lang: str) -> str:
here = os.path.dirname(__file__)
p = os.path.normpath(os.path.join(here, '..', 'prompts', lang, f'{mode}.md'))
try:
with open(p, encoding='utf-8') as f:
return f.read().strip()
except FileNotFoundError:
return ''
def report_path(input_path: str, mode: str, other_stem: str = '') -> str:
d = os.path.dirname(os.path.abspath(input_path))
reports_dir = os.path.join(d, 'reports')
stem = os.path.splitext(os.path.basename(input_path))[0]
if mode == 'compare' and other_stem:
fname = f'compare_{stem}_vs_{other_stem}.md'
else:
fname = f'{stem}_{mode}.md'
return os.path.join(reports_dir, fname)

182
telemetry/report_compare.py Normal file
View file

@ -0,0 +1,182 @@
import bisect
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_DIST, LEGEND_SPD, LEGEND_DT_SEG, LEGEND_DT_CUM,
)
def _interp_laptime(lap: Lap, dist_query: float) -> float:
dist = lap.ch.get('LapDistance', [])
lt = lap.ch.get('LapTime', [])
if not dist or not lt:
return 0.0
if dist_query <= dist[0]:
return lt[0]
if dist_query >= dist[-1]:
return lt[-1]
i = bisect.bisect_left(dist, dist_query)
x0, x1 = dist[i - 1], dist[i]
y0, y1 = lt[i - 1], lt[i]
if x1 == x0:
return y0
return y0 + (y1 - y0) * (dist_query - x0) / (x1 - x0)
def _header_table(laps: list[Lap]) -> str:
headers = ['Metric'] + [f'Lap {i+1}' for i in range(len(laps))]
def _fuel_start(l: Lap) -> str:
raw = l.track.get('Fuel at Start', '?')
try:
return f'{float(raw):.2f}'
except (ValueError, TypeError):
return raw
fields = [
('Laptime', lambda l: f'{laptime_s(l):.3f}'),
('S1', lambda l: f'{sector_times(l)[0]:.3f}'),
('S2', lambda l: f'{sector_times(l)[1]:.3f}'),
('S3', lambda l: f'{sector_times(l)[2]:.3f}'),
('Tyre', lambda l: l.track.get('Tyre', '?')),
('Fuel start kg', _fuel_start),
('Wear FL%', lambda l: _delta_wear(l, 'TyreWearFrontLeft')),
('Wear FR%', lambda l: _delta_wear(l, 'TyreWearFrontRight')),
('Wear RL%', lambda l: _delta_wear(l, 'TyreWearRearLeft')),
('Wear RR%', lambda l: _delta_wear(l, 'TyreWearRearRight')),
('Weather', lambda l: l.track.get('Weather', '?')),
]
rows = []
for label, fn in fields:
rows.append([label] + [fn(lap) for lap in laps])
return md_table(headers, rows)
def _delta_wear(lap: Lap, key: str) -> str:
v = lap.ch.get(key, [])
if len(v) >= 2:
return f'{v[-1]-v[0]:.2f}'
return '?'
def _corner_table(laps: list[Lap], corners: list[Corner]) -> str:
n = len(laps)
# build column labels for each non-reference lap
seg_headers = [f'Δt seg L{k+1}−L1' for k in range(1, n)]
cum_headers = [f'Δt cum L{k+1}−L1' for k in range(1, n)]
headers = ['T'] + [f'Vmin L{i+1}' for i in range(n)] + \
[f'time L{i+1}' for i in range(n)] + \
seg_headers + cum_headers
rows = []
for c in corners:
row = [str(c.number)]
vmins = []
seg_times: list[float | None] = []
for lap in laps:
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]
if idxs and spd:
spds = [spd[i] for i in idxs if i < len(spd)]
vmins.append(f'{min(spds):.0f}')
else:
vmins.append('—')
if lap.ch.get('LapDistance') and lap.ch.get('LapTime'):
t = _interp_laptime(lap, c.end_m) - _interp_laptime(lap, c.start_m)
seg_times.append(t)
else:
seg_times.append(None)
times = [f'{t:.2f}' if t is not None else '—' for t in seg_times]
row += vmins + times
lap_a = laps[0]
seg_a = seg_times[0]
# per-segment Δt
for k in range(1, n):
if seg_times[k] is not None and seg_a is not None:
row.append(f'{seg_times[k] - seg_a:+.3f}')
else:
row.append('—')
# cumulative Δt at corner end
ta_end = _interp_laptime(lap_a, c.end_m)
for k in range(1, n):
lap_k = laps[k]
if lap_k.ch.get('LapDistance') and lap_k.ch.get('LapTime'):
tk_end = _interp_laptime(lap_k, c.end_m)
row.append(f'{tk_end - ta_end:+.3f}')
else:
row.append('—')
rows.append(row)
return md_table(headers, rows)
def _delta_trace(laps: list[Lap], corners: list[Corner]) -> tuple[str, list[float]]:
lap_a = laps[0]
dists = adaptive_points(lap_a, corners)
if not dists:
return '', []
spds = [sample_at(lap, 'Speed', dists) for lap in laps]
header_parts = ['dist'] + [f'spd_L{i+1}' for i in range(len(laps))] + \
[f'Δt_L{i+1}' for i in range(1, len(laps))]
header = ','.join(header_parts)
final_deltas: list[float] = []
rows_str: list[str] = []
for j, d in enumerate(dists):
parts = [f'{d:.0f}']
for i, lap in enumerate(laps):
parts.append(f'{spds[i][j]:.0f}')
deltas = []
for k in range(1, len(laps)):
ta = _interp_laptime(lap_a, d)
tk = _interp_laptime(laps[k], d)
deltas.append(f'{tk - ta:+.3f}')
parts += deltas
rows_str.append(','.join(parts))
if j == len(dists) - 1:
for k in range(1, len(laps)):
ta = _interp_laptime(lap_a, d)
tk = _interp_laptime(laps[k], d)
final_deltas.append(tk - ta)
block = '```\n' + header + '\n' + '\n'.join(rows_str) + '\n```'
return block, final_deltas
def generate(laps: list[Lap], out_path: str, lang: str = 'ru') -> tuple[str, int]:
lap_a = laps[0]
corners = detect_corners(lap_a)
head_table = _header_table(laps)
corner_table = _corner_table(laps, corners)
delta_block, final_deltas = _delta_trace(laps, corners)
for k in range(1, len(laps)):
lt_diff = laptime_s(laps[k]) - laptime_s(lap_a)
final_dt = final_deltas[k - 1] if (k - 1) < len(final_deltas) else float('nan')
print(f'Sanity L{k+1} vs L1: laptime diff={lt_diff:+.3f}s final cumulative dt={final_dt:+.3f}s')
parts = [
'# Lap Comparison\n',
head_table,
'\n## Corners',
corner_table,
'\n## Delta trace',
delta_block,
'\n' + legend([LEGEND_DIST, LEGEND_SPD, LEGEND_DT_SEG, LEGEND_DT_CUM]),
]
text = '\n'.join(parts)
prompt = load_prompt('compare', lang)
if prompt:
text += '\n\n---\n\n' + prompt
return write_report(out_path, text)

277
telemetry/report_race.py Normal file
View file

@ -0,0 +1,277 @@
from .parser import Lap, laptime_s, sector_times
from .report_common import md_table, write_report, load_prompt
def _fmt_laptime(s: float) -> str:
mins = int(s) // 60
secs = s - mins * 60
return f'{mins}:{secs:06.3f}'
def _fmt_sector(s: float) -> str:
return f'{s:.3f}'
def _fuel_start(lap: Lap) -> float:
raw = lap.track.get('Fuel at Start', lap.track.get('FuelAtStart', '0'))
raw = raw.strip().split()[0] if raw.strip() else '0'
try:
return float(raw)
except ValueError:
pass
v = lap.ch.get('FuelRemaining', [0.0])
return v[0] if v else 0.0
def _sort_laps(laps: list[Lap]) -> list[Lap]:
def lap_num(lap: Lap) -> int:
try:
return int(lap.track.get('Lap', '0'))
except ValueError:
return 0
return sorted(laps, key=lap_num)
def _assign_stints(laps: list[Lap]) -> tuple[list[int], list[int]]:
stints: list[int] = []
ages: list[int] = []
stint = 1
age = 1
prev_tyre = ''
prev_was_pit = False
for i, lap in enumerate(laps):
tyre = lap.track.get('Tyre', '')
if i > 0 and (prev_was_pit or tyre != prev_tyre):
stint += 1
age = 1
stints.append(stint)
ages.append(age)
prev_tyre = tyre
prev_was_pit = lap.track.get('Pitlap', 'false').lower() == 'true'
age += 1
return stints, ages
def _header(laps: list[Lap]) -> str:
if not laps:
return ''
first = laps[0]
s = first.session
t = first.track
track = s.get('track', '?')
car = s.get('car', '?')
total = len(laps)
weather = t.get('Weather', '?')
track_temp = t.get('TrackTemp', '?')
air_temp = t.get('AmbientTemp', '?')
lines = [
f'**Track:** {track} ',
f'**Car:** {car} ',
f'**Total laps:** {total} ',
f'**Weather:** {weather} ',
f'**Track temp:** {track_temp} °C **Air:** {air_temp} °C ',
]
return '\n'.join(lines)
def _lap_times_table(laps: list[Lap], stints: list[int], ages: list[int]) -> str:
# find fastest valid lap time
best_time = None
for lap in laps:
valid = lap.track.get('Valid', 'true').lower() != 'false'
pit = lap.track.get('Pitlap', 'false').lower() == 'true'
if valid and not pit:
lt = laptime_s(lap)
if lt > 0 and (best_time is None or lt < best_time):
best_time = lt
rows = []
for i, lap in enumerate(laps):
lap_n = lap.track.get('Lap', str(i + 1))
lt = laptime_s(lap)
s1, s2, s3 = sector_times(lap)
tyre = lap.track.get('Tyre', '?')
age = ages[i]
fuel = _fuel_start(lap)
valid = lap.track.get('Valid', 'true').lower() != 'false'
pit = lap.track.get('Pitlap', 'false').lower() == 'true'
notes = []
if pit:
notes.append('PIT')
if not valid:
notes.append('INV')
if valid and not pit and best_time is not None and lt == best_time:
notes.append('FAST')
rows.append([
lap_n,
_fmt_laptime(lt) if lt > 0 else '—',
_fmt_sector(s1) if s1 > 0 else '—',
_fmt_sector(s2) if s2 > 0 else '—',
_fmt_sector(s3) if s3 > 0 else '—',
tyre,
str(age),
f'{fuel:.1f}',
' '.join(notes),
])
return md_table(
['Lap', 'Time', 'S1', 'S2', 'S3', 'Tyre', 'Age', 'Fuel kg', 'Note'],
rows,
)
def _stint_summary(laps: list[Lap], stints: list[int], ages: list[int]) -> str:
# group by stint
stint_map: dict[int, list[tuple[Lap, int]]] = {}
for i, lap in enumerate(laps):
st = stints[i]
stint_map.setdefault(st, []).append((lap, ages[i]))
rows = []
for st in sorted(stint_map):
entries = stint_map[st]
tyre = entries[0][0].track.get('Tyre', '?')
laps_on = max(age for _, age in entries)
n_laps = len(entries)
# exclude pit laps from stats
valid_entries = [
(lap, age) for lap, age in entries
if lap.track.get('Valid', 'true').lower() != 'false'
and lap.track.get('Pitlap', 'false').lower() != 'true'
]
times = [laptime_s(lap) for lap, _ in valid_entries if laptime_s(lap) > 0]
if times:
avg = sum(times) / len(times)
best = min(times)
worst = max(times)
avg_s = _fmt_laptime(avg)
best_s = _fmt_laptime(best)
worst_s = _fmt_laptime(worst)
else:
avg_s = best_s = worst_s = '—'
# degradation: slope of lap time vs tyre age
if len(valid_entries) >= 2:
first_lap, first_age = valid_entries[0]
last_lap, last_age = valid_entries[-1]
ft = laptime_s(first_lap)
lt = laptime_s(last_lap)
da = last_age - first_age
if da > 0 and ft > 0 and lt > 0:
deg = (lt - ft) / da
deg_s = f'{deg:+.3f}'
else:
deg_s = '—'
else:
deg_s = '—'
rows.append([
str(st),
str(n_laps),
tyre,
str(laps_on),
avg_s,
best_s,
worst_s,
deg_s,
])
return md_table(
['Stint', 'Laps', 'Tyre', 'Laps on tyre', 'Avg pace', 'Best', 'Worst', 'Deg s/lap'],
rows,
)
def _tyre_wear_table(laps: list[Lap]) -> str:
rows = []
for i, lap in enumerate(laps):
lap_n = lap.track.get('Lap', str(i + 1))
ch = lap.ch
fl = ch.get('TyreWearFrontLeft', [])
fr = ch.get('TyreWearFrontRight', [])
rl = ch.get('TyreWearRearLeft', [])
rr = ch.get('TyreWearRearRight', [])
rows.append([
lap_n,
f'{fl[-1]:.2f}' if fl else '—',
f'{fr[-1]:.2f}' if fr else '—',
f'{rl[-1]:.2f}' if rl else '—',
f'{rr[-1]:.2f}' if rr else '—',
])
return md_table(['Lap', 'FL%', 'FR%', 'RL%', 'RR%'], rows)
def _thermal_table(laps: list[Lap]) -> str:
rows = []
for i, lap in enumerate(laps):
lap_n = lap.track.get('Lap', str(i + 1))
ch = lap.ch
tf_fl = ch.get('TyreTemperatureFrontLeft', [])
tf_fr = ch.get('TyreTemperatureFrontRight', [])
tr_rl = ch.get('TyreTemperatureRearLeft', [])
tr_rr = ch.get('TyreTemperatureRearRight', [])
tf_avg = ((max(tf_fl) if tf_fl else 0) + (max(tf_fr) if tf_fr else 0)) / 2
tr_avg = ((max(tr_rl) if tr_rl else 0) + (max(tr_rr) if tr_rr else 0)) / 2
bt_fl = ch.get('BrakeTemperatureFrontLeft', [])
bt_rr = ch.get('BrakeTemperatureRearRight', [])
rows.append([
lap_n,
f'{tf_avg:.0f}' if tf_fl or tf_fr else '—',
f'{tr_avg:.0f}' if tr_rl or tr_rr else '—',
f'{max(bt_fl):.0f}' if bt_fl else '—',
f'{max(bt_rr):.0f}' if bt_rr else '—',
])
return md_table(['Lap', 'Tf avg°C', 'Tr avg°C', 'Brk FL max°C', 'Brk RR max°C'], rows)
def _ers_table(laps: list[Lap]) -> str:
rows = []
for i, lap in enumerate(laps):
lap_n = lap.track.get('Lap', str(i + 1))
ch = lap.ch
ers = ch.get('ERSSpent', [])
mguk = ch.get('MGUKHarvested', [])
mguh = ch.get('MGUHHarvested', [])
rows.append([
lap_n,
f'{ers[-1] / 1e6:.3f}' if ers else '—',
f'{mguk[-1] / 1e6:.3f}' if mguk else '—',
f'{mguh[-1] / 1e6:.3f}' if mguh else '—',
])
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]:
laps = _sort_laps(laps)
stints, ages = _assign_stints(laps)
parts = [
_header(laps),
'\n## Lap times',
_lap_times_table(laps, stints, ages),
'\n## Stint summary',
_stint_summary(laps, stints, ages),
'\n## Tyre wear per lap',
_tyre_wear_table(laps),
'\n## Thermal summary per lap',
_thermal_table(laps),
'\n## ERS per lap',
_ers_table(laps),
]
text = '\n'.join(parts)
prompt = load_prompt('race', lang)
if prompt:
text += '\n\n---\n\n' + prompt
return write_report(out_path, text)

255
telemetry/report_setup.py Normal file
View file

@ -0,0 +1,255 @@
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,
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,
)
def _safe_avg(vals: list[float]) -> float:
return sum(vals) / len(vals) if vals else 0.0
def _safe_min(vals: list[float]) -> float:
return min(vals) if vals else 0.0
def _safe_max(vals: list[float]) -> float:
return max(vals) if vals else 0.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 _corner_balance(lap: Lap, corners: list[Corner]) -> str:
dist = lap.ch.get('LapDistance', [])
glat = lap.ch.get('GForceLatitudinal', [])
steer = lap.ch.get('Steer', [])
slip_fl = lap.ch.get('WheelSlipFrontLeft', [])
slip_fr = lap.ch.get('WheelSlipFrontRight', [])
slip_rl = lap.ch.get('WheelSlipRearLeft', [])
slip_rr = lap.ch.get('WheelSlipRearRight', [])
# lap-wide axle baselines
all_front = [abs(v) for v in slip_fl] + [abs(v) for v in slip_fr]
all_rear = [abs(v) for v in slip_rl] + [abs(v) for v in slip_rr]
front_baseline = _median(all_front)
rear_baseline = _median(all_rear)
rows = []
for c in corners:
idxs = _indices_in_range(dist, c.start_m, c.end_m)
if not idxs:
continue
glat_vals = [abs(glat[i]) for i in idxs if i < len(glat)]
steer_vals = [abs(steer[i]) for i in idxs if i < len(steer)]
glat_avg = _safe_avg(glat_vals)
steer_per_g = (_safe_avg(steer_vals) / glat_avg) if glat_avg > 0 else 0.0
front_slip = [abs(slip_fl[i]) for i in idxs if i < len(slip_fl)] + \
[abs(slip_fr[i]) for i in idxs if i < len(slip_fr)]
rear_slip = [abs(slip_rl[i]) for i in idxs if i < len(slip_rl)] + \
[abs(slip_rr[i]) for i in idxs if i < len(slip_rr)]
front_avg = _safe_avg(front_slip)
rear_avg = _safe_avg(rear_slip)
front_excess = front_avg - front_baseline
rear_excess = rear_avg - rear_baseline
diff = front_excess - rear_excess
if diff > 0.5:
balance = 'US'
elif diff < -0.5:
balance = 'OS'
else:
balance = '—'
rows.append([
str(c.number),
f'{c.v_min:.0f}',
f'{c.g_lat_max:.2f}',
f'{steer_per_g:.2f}',
f'{front_avg:.3f}',
f'{rear_avg:.3f}',
balance,
])
return md_table(
['T', 'Vmin', 'gLat_max', 'steer/g', 'front|slip|', 'rear|slip|', 'balance'],
rows,
)
def _tyre_section(lap: Lap) -> str:
ch = lap.ch
setup = lap.setup
wheels = ['FrontLeft', 'FrontRight', 'RearLeft', 'RearRight']
rows = []
for w in wheels:
wear = ch.get(f'TyreWear{w}', [])
wear_d = f'{wear[-1]-wear[0]:.2f}' if len(wear) >= 2 else '—'
surf = ch.get(f'TyreTemperature{w}', [])
if surf:
surf_min = f'{_safe_min(surf):.0f}'
surf_avg = f'{_safe_avg(surf):.0f}'
surf_max = f'{_safe_max(surf):.0f}'
else:
surf_min = surf_avg = surf_max = '—'
carc = ch.get(f'TyreCarcassTemperature{w}', [])
if carc:
carc_min = f'{_safe_min(carc):.0f}'
carc_avg = f'{_safe_avg(carc):.0f}'
carc_max = f'{_safe_max(carc):.0f}'
else:
carc_min = carc_avg = carc_max = '—'
press_ch = ch.get(f'TyrePressure{w}', [])
if press_ch and any(v != 0.0 for v in press_ch):
press = f'{_safe_avg(press_ch):.1f} PSI'
else:
key_map = {'FrontLeft': 'FLTyrePressure', 'FrontRight': 'FRTyrePressure',
'RearLeft': 'RLTyrePressure', 'RearRight': 'RRTyrePressure'}
press = setup.get(key_map.get(w, ''), '—') + ' PSI'
rows.append([w, wear_d, f'{surf_min}/{surf_avg}/{surf_max}',
f'{carc_min}/{carc_avg}/{carc_max}', press])
return md_table(['Wheel', 'wearΔ%', 'surf min/avg/max °C', 'carc min/avg/max °C', 'pressure'], rows)
def _brake_section(lap: Lap) -> str:
ch = lap.ch
wheels = ['FrontLeft', 'FrontRight', 'RearLeft', 'RearRight']
rows = []
for w in wheels:
bt = ch.get(f'BrakeTemperature{w}', [])
if bt:
rows.append([w, f'{_safe_min(bt):.0f}', f'{_safe_avg(bt):.0f}', f'{_safe_max(bt):.0f}'])
else:
rows.append([w, '—', '—', '—'])
return md_table(['Wheel', 'min °C', 'avg °C', 'max °C'], rows)
def _lap_phases(lap: Lap) -> str:
dist = lap.ch.get('LapDistance', [])
if not dist:
return ''
d_min, d_max = dist[0], dist[-1]
third = (d_max - d_min) / 3
phases = [
('Phase 1', d_min, d_min + third),
('Phase 2', d_min + third, d_min + 2 * third),
('Phase 3', d_min + 2 * third, d_max),
]
wheels = ['FrontLeft', 'FrontRight', 'RearLeft', 'RearRight']
headers = ['Phase'] + wheels
rows = []
for label, start, end in phases:
idxs = _indices_in_range(dist, start, end)
row = [label]
for w in wheels:
surf = lap.ch.get(f'TyreTemperature{w}', [])
vals = [surf[i] for i in idxs if i < len(surf)]
row.append(f'{_safe_avg(vals):.0f}' if vals else '—')
rows.append(row)
return md_table(headers, rows)
def _full_setup_table(lap: Lap) -> str:
rows = [[k, v] for k, v in lap.setup.items()]
return md_table(['Parameter', 'Value'], rows)
def _suspension_section(lap: Lap, corners: list[Corner]) -> str | None:
ch = lap.ch
susp_keys = ['SuspensionPositionFrontLeft', 'SuspensionPositionFrontRight',
'SuspensionPositionRearLeft', 'SuspensionPositionRearRight']
if not all(k in ch for k in susp_keys):
return None
fl = ch['SuspensionPositionFrontLeft']
fr = ch['SuspensionPositionFrontRight']
rl = ch['SuspensionPositionRearLeft']
rr = ch['SuspensionPositionRearRight']
front_avg_mm = _safe_avg([v * 1000 for v in fl] + [v * 1000 for v in fr])
rear_avg_mm = _safe_avg([v * 1000 for v in rl] + [v * 1000 for v in rr])
rake_mm = front_avg_mm - rear_avg_mm
rake_line = (
f'Rake: front avg {front_avg_mm:.1f} mm / rear avg {rear_avg_mm:.1f} mm'
f' / Δ (rake) {rake_mm:.1f} mm'
)
dist = ch.get('LapDistance', [])
glat = ch.get('GForceLatitudinal', [])
rows = []
for c in corners:
idxs = _indices_in_range(dist, c.start_m, c.end_m)
if not idxs:
continue
fl_z = [fl[i] * 1000 for i in idxs if i < len(fl)]
fr_z = [fr[i] * 1000 for i in idxs if i < len(fr)]
rl_z = [rl[i] * 1000 for i in idxs if i < len(rl)]
rr_z = [rr[i] * 1000 for i in idxs if i < len(rr)]
left_avg = _safe_avg(fl_z + rl_z)
right_avg = _safe_avg(fr_z + rr_z)
roll_mm = left_avg - right_avg
glat_vals = [glat[i] for i in idxs if i < len(glat)]
mean_glat = _safe_avg(glat_vals)
glat_sign = '+' if mean_glat >= 0 else '-'
rows.append([str(c.number), f'{c.v_min:.0f}', f'{roll_mm:.1f}', glat_sign])
roll_table = md_table(['T', 'Vmin', 'roll mm', 'gLat sign'], rows)
return rake_line + '\n\n' + roll_table
def generate(lap: Lap, out_path: str, lang: str = 'ru') -> tuple[str, int]:
corners = detect_corners(lap)
susp = _suspension_section(lap, corners)
parts = [
header_block(lap),
'\n## Setup',
_full_setup_table(lap),
'\n## Corner balance',
_corner_balance(lap, corners),
'\n## Tyres',
_tyre_section(lap),
'\n## Brakes',
_brake_section(lap),
]
if susp is not None:
parts += ['\n## Suspension', susp]
legend_entries = [LEGEND_GLAT, LEGEND_STEER, LEGEND_LOCK, LEGEND_SPIN, LEGEND_STEER_G_CAVEAT]
if susp is not None:
legend_entries.append(LEGEND_SUSP)
parts += [
'\n## Lap phases (avg surface temp °C)',
_lap_phases(lap),
'\n' + legend(legend_entries),
]
text = '\n'.join(parts)
prompt = load_prompt('setup', lang)
if prompt:
text += '\n\n---\n\n' + prompt
return write_report(out_path, text)

View file

@ -0,0 +1,187 @@
from .parser import Lap, laptime_s
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,
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,
)
def _corner_rows(corners: list[Corner]) -> list[list[str]]:
rows = []
for c in corners:
flag_str = '—'
flags = []
if c.lockup:
flags.append('LOCK')
if c.wheelspin:
flags.append('SPIN')
if flags:
flag_str = '/'.join(flags)
brake_m = f'{c.brake_start_m:.0f}' if c.brake_start_m is not None else '—'
ft_m = f'{c.full_throttle_m:.0f}' if c.full_throttle_m is not None else '—'
rows.append([
str(c.number),
brake_m,
f'{c.v_entry:.0f}',
f'{c.v_min:.0f}',
f'{c.apex_m:.0f}',
str(c.gear_apex),
ft_m,
f'{c.v_exit:.0f}',
f'{c.brake_max:.0f}',
f'{c.g_lat_max:.2f}',
f'{c.time_s:.2f}',
f'{c.lock_pct:.1f}',
f'{c.spin_pct:.1f}',
str(int(c.coast_m)),
str(int(c.rear_tyre_temp_peak)),
flag_str,
])
return rows
def _straight_rows(ss: list[Straight]) -> list[list[str]]:
rows = []
for s in ss:
rows.append([
f'{s.start_m:.0f}→{s.end_m:.0f}',
f'{s.v_max:.0f}',
'Y' if s.drs else 'N',
f'{s.time_s:.2f}',
])
return rows
def _driving_cost(lap: Lap, corners: list[Corner]) -> str:
ch = lap.ch
fuel = ch.get('FuelRemaining', [])
fuel_used = (fuel[0] - fuel[-1]) if len(fuel) >= 2 else 0.0
wear_keys = ['TyreWearFrontLeft', 'TyreWearFrontRight', 'TyreWearRearLeft', 'TyreWearRearRight']
wear_lines = []
for k in wear_keys:
v = ch.get(k, [])
if len(v) >= 2:
wear_lines.append(f' {k.replace("TyreWear","")}: Δ{v[-1]-v[0]:.2f}%')
ers_spent = ch.get('ERSSpent', [])
mguk_h = ch.get('MGUKHarvested', [])
mguh_h = ch.get('MGUHHarvested', [])
ers_mj = (ers_spent[-1] / 1e6) if ers_spent else 0.0
mguk_mj = (mguk_h[-1] / 1e6) if mguk_h else 0.0
mguh_mj = (mguh_h[-1] / 1e6) if mguh_h else 0.0
lock_count = sum(1 for c in corners if c.lockup)
spin_count = sum(1 for c in corners if c.wheelspin)
worst_lock = min(corners, key=lambda c: c.lock_pct, default=None)
worst_spin = max(corners, key=lambda c: c.spin_pct, default=None)
worst_lock_str = f'T{worst_lock.number} ({worst_lock.lock_pct:.1f}%)' if worst_lock else '—'
worst_spin_str = f'T{worst_spin.number} ({worst_spin.spin_pct:.1f}%)' if worst_spin else '—'
bt_keys = ['BrakeTemperatureFrontLeft', 'BrakeTemperatureFrontRight',
'BrakeTemperatureRearLeft', 'BrakeTemperatureRearRight']
bt_lines = []
for k in bt_keys:
v = ch.get(k, [])
if v:
bt_lines.append(f' {k.replace("BrakeTemperature","")}: max {max(v):.0f} °C')
lines = [
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',
f'- Corners flagged LOCK: {lock_count} | SPIN: {spin_count}',
f'- Worst lock%: {worst_lock_str} | Worst spin%: {worst_spin_str}',
'- Max brake temps:',
*bt_lines,
]
return '\n'.join(lines)
def _trace_block(lap: Lap, corners: list[Corner]) -> str:
dists = adaptive_points(lap, corners)
if not dists:
return ''
def s(channel: str) -> list[float]:
return sample_at(lap, channel, dists)
lt = s('LapTime')
spd = s('Speed')
thr = s('ThrottlePercentage')
brk = s('BrakePercentage')
steer = s('Steer')
gear = s('Gear')
drs = s('DRS')
glat = s('GForceLatitudinal')
glon = s('GForceLongitudinal')
fuel = s('FuelRemaining')
ers_raw = s('ERSSpent')
ers = [v / 1e6 for v in ers_raw]
tf_fl = s('TyreTemperatureFrontLeft')
tf_fr = s('TyreTemperatureFrontRight')
tr_rl = s('TyreTemperatureRearLeft')
tr_rr = s('TyreTemperatureRearRight')
rpm_raw = s('EngineRevs')
tf = [(tf_fl[i] + tf_fr[i]) / 2 for i in range(len(dists))]
tr = [(tr_rl[i] + tr_rr[i]) / 2 for i in range(len(dists))]
rpm = [round(rpm_raw[i] / 100) * 100 for i in range(len(dists))]
slip_fl = s('WheelSlipFrontLeft')
slip_fr = s('WheelSlipFrontRight')
slip_rl = s('WheelSlipRearLeft')
slip_rr = s('WheelSlipRearRight')
yaw_vals = s('LocalAngularVelocityY')
slip_f = [min(slip_fl[i], slip_fr[i]) / max(spd[i], 30.0) * 100.0 for i in range(len(dists))]
slip_r = [max(slip_rl[i], slip_rr[i]) / max(spd[i], 30.0) * 100.0 for i in range(len(dists))]
header = 'dist,time,spd,thr,brk,steer,gear,drs,gLat,gLon,fuel,ers,Tf,Tr,rpm,slipF,slipR,yaw'
rows = [
format_trace_row(dists[i], lt[i], spd[i], thr[i], brk[i],
steer[i], gear[i], drs[i], glat[i], glon[i], fuel[i], ers[i],
tf[i], tr[i], rpm[i], slip_f[i], slip_r[i], yaw_vals[i])
for i in range(len(dists))
]
return '```\n' + header + '\n' + '\n'.join(rows) + '\n```'
def generate(lap: Lap, out_path: str, lang: str = 'ru') -> tuple[str, int]:
corners = detect_corners(lap)
ss = straights(corners, lap)
corner_table = md_table(
['T', 'brake@m', 'Ventry', 'Vmin', 'apex@m', 'gear', 'fullThr@m', 'Vexit', 'maxBrake%', 'gLat', 'time s', 'lock%', 'spin%', 'coast m', 'Tr peak °C', 'flags'],
_corner_rows(corners),
)
straight_table = md_table(
['from→to m', 'Vmax', 'DRS', 'time s'],
_straight_rows(ss),
)
parts = [
header_block(lap),
f'\n**Setup:** {setup_summary(lap)}',
'\n## Corners',
corner_table,
'\n## Straights',
straight_table,
'\n## Driving cost (per lap)',
_driving_cost(lap, corners),
'\n## Trace',
_trace_block(lap, corners),
'\n' + legend([
LEGEND_DIST, LEGEND_TIME, LEGEND_SPD, LEGEND_THR_BRK, LEGEND_STEER,
LEGEND_GEAR, LEGEND_DRS, LEGEND_GLAT, LEGEND_GLON, LEGEND_FUEL, LEGEND_ERS,
LEGEND_TF, LEGEND_RPM, LEGEND_SLIP, LEGEND_YAW, LEGEND_LOCK, LEGEND_SPIN,
]),
]
text = '\n'.join(parts)
prompt = load_prompt('technique', lang)
if prompt:
text += '\n\n---\n\n' + prompt
return write_report(out_path, text)

145
telemetry/resample.py Normal file
View file

@ -0,0 +1,145 @@
import bisect
import statistics
from .parser import Lap
from .segments import Corner
_DISCRETE = {'Gear', 'DRS', 'ERSMode'}
def auto_steps(lap: Lap) -> tuple[float, float]:
dist = lap.ch.get('LapDistance', [])
diffs = [dist[i + 1] - dist[i] for i in range(len(dist) - 1) if dist[i + 1] - dist[i] > 0]
if not diffs:
return (4.0, 25.0)
med = statistics.median(diffs)
dense = max(round(med, 1), 1.0)
sparse = max(round(med * 8, 0), 20.0)
return (dense, sparse)
def adaptive_points(
lap: Lap,
corners: list[Corner],
dense_step: float | None = None,
sparse_step: float | None = None,
approach_m: float = 100.0,
) -> list[float]:
if dense_step is None or sparse_step is None:
auto_d, auto_s = auto_steps(lap)
if dense_step is None:
dense_step = auto_d
if sparse_step is None:
sparse_step = auto_s
dist = lap.ch.get('LapDistance', [])
if not dist:
return []
d_min = dist[0]
d_max = dist[-1]
# build dense/sparse bands
dense_ranges: list[tuple[float, float]] = []
for c in corners:
band_start = max(d_min, c.start_m - approach_m)
dense_ranges.append((band_start, c.end_m))
def in_dense(d: float) -> bool:
for a, b in dense_ranges:
if a <= d <= b:
return True
return False
points: list[float] = [d_min]
d = d_min
while d < d_max:
step = dense_step if in_dense(d) else sparse_step
d = min(d + step, d_max)
points.append(d)
if points[-1] != d_max:
points.append(d_max)
return sorted(set(points))
def _interp(xs: list[float], ys: list[float], x: float) -> float:
if x <= xs[0]:
return ys[0]
if x >= xs[-1]:
return ys[-1]
i = bisect.bisect_left(xs, x)
x0, x1 = xs[i - 1], xs[i]
y0, y1 = ys[i - 1], ys[i]
if x1 == x0:
return y0
return y0 + (y1 - y0) * (x - x0) / (x1 - x0)
def _nearest(xs: list[float], ys: list[float], x: float) -> float:
if x <= xs[0]:
return ys[0]
if x >= xs[-1]:
return ys[-1]
i = bisect.bisect_left(xs, x)
if i == 0:
return ys[0]
if abs(xs[i] - x) < abs(xs[i - 1] - x):
return ys[i]
return ys[i - 1]
def sample_at(lap: Lap, channel: str, dists: list[float]) -> list[float]:
src_dist = lap.ch.get('LapDistance', [])
src_vals = lap.ch.get(channel, [])
if not src_dist or not src_vals:
return [0.0] * len(dists)
discrete = channel in _DISCRETE
fn = _nearest if discrete else _interp
return [fn(src_dist, src_vals, d) for d in dists]
def _round_channel(ch: str, v: float) -> str:
if ch in ('LapTime', 'LapDistance'):
return f'{v:.0f}' if ch == 'LapDistance' else f'{v:.2f}'
if ch in ('FuelRemaining',):
return f'{v:.2f}'
if ch in ('Steer', 'ThrottlePercentage', 'BrakePercentage'):
return f'{v:.0f}'
if ch in ('Speed', 'GForceLatitudinal', 'GForceLongitudinal'):
return f'{v:.0f}'
if ch in ('Gear', 'DRS', 'ERSMode'):
return f'{int(v)}'
return f'{v:.1f}'
def format_trace_row(
dist: float,
laptime: float,
speed: float,
throttle: float,
brake: float,
steer: float,
gear: float,
drs: float,
g_lat: float,
g_lon: float,
fuel: float,
ers: float = 0.0,
tf: float = 0.0,
tr: float = 0.0,
rpm: float = 0.0,
slip_f: float = 0.0,
slip_r: float = 0.0,
yaw: float = 0.0,
) -> str:
steer_norm = 0.0 if steer == 0.0 or steer == -0.0 else steer
return (
f'{dist:.0f},{laptime:.2f},{speed:.0f},{throttle:.0f},'
f'{brake:.0f},{steer_norm:.0f},{int(gear)},{int(drs)},'
f'{g_lat:.1f},{g_lon:.1f},{fuel:.2f},{ers:.3f},'
f'{tf:.0f},{tr:.0f},{rpm:.0f},'
f'{slip_f:.1f},{slip_r:.1f},{yaw:.2f}'
)

152
telemetry/segments.py Normal file
View file

@ -0,0 +1,152 @@
from dataclasses import dataclass
from .parser import Lap
STEER_ON = 6.0
BRAKE_ON = 5.0
MERGE_GAP_M = 40.0
MIN_LEN_M = 15.0
BRAKE_MARK = 20.0
FULL_THROTTLE = 98.0
LOCKUP_REL_PCT = -10.0
SPIN_REL_PCT = 8.0
@dataclass(frozen=True)
class Corner:
number: int
start_m: float
end_m: float
brake_start_m: float | None
v_entry: float
v_min: float
apex_m: float
gear_apex: int
full_throttle_m: float | None
v_exit: float
brake_max: float
g_lat_max: float
time_s: float
lock_pct: float
spin_pct: float
coast_m: float
rear_tyre_temp_peak: float
lockup: bool
wheelspin: bool
@dataclass(frozen=True)
class Straight:
start_m: float
end_m: float
v_max: float
drs: bool
time_s: float
def _get(lap: Lap, ch: str) -> list[float]:
return lap.ch.get(ch, [])
def _indices_in_range(dist: list[float], start: float, end: float) -> list[int]:
return [i for i, d in enumerate(dist) if start <= d <= end]
def detect_corners(lap: Lap) -> list[Corner]:
from .corner_metrics import corner_from_zone
dist = _get(lap, 'LapDistance')
steer = _get(lap, 'Steer')
brake = _get(lap, 'BrakePercentage')
speed = _get(lap, 'Speed')
gear = _get(lap, 'Gear')
throttle = _get(lap, 'ThrottlePercentage')
glat = _get(lap, 'GForceLatitudinal')
laptime = _get(lap, 'LapTime')
slip_rl = _get(lap, 'WheelSlipRearLeft')
slip_rr = _get(lap, 'WheelSlipRearRight')
slip_fl = _get(lap, 'WheelSlipFrontLeft')
slip_fr = _get(lap, 'WheelSlipFrontRight')
tyre_temp_rl = _get(lap, 'TyreTemperatureRearLeft')
tyre_temp_rr = _get(lap, 'TyreTemperatureRearRight')
n = len(dist)
if n < 2:
return []
cornering = [
(abs(steer[i]) > STEER_ON if i < len(steer) else False) or
(brake[i] > BRAKE_ON if i < len(brake) else False)
for i in range(n)
]
zones: list[tuple[int, int]] = []
in_zone = False
z_start = 0
for i, c in enumerate(cornering):
if c and not in_zone:
in_zone = True
z_start = i
elif not c and in_zone:
in_zone = False
zones.append((z_start, i - 1))
if in_zone:
zones.append((z_start, n - 1))
merged: list[tuple[int, int]] = []
for z in zones:
if merged and (dist[z[0]] - dist[merged[-1][1]]) < MERGE_GAP_M:
merged[-1] = (merged[-1][0], z[1])
else:
merged.append(list(z))
valid = [(a, b) for a, b in merged if (dist[b] - dist[a]) >= MIN_LEN_M]
return [
corner_from_zone(
lap, ia, ib, num,
dist, speed, brake, throttle, gear, glat, laptime,
slip_fl, slip_fr, slip_rl, slip_rr,
tyre_temp_rl, tyre_temp_rr,
)
for num, (ia, ib) in enumerate(valid, 1)
]
def straights(corners: list[Corner], lap: Lap) -> list[Straight]:
dist = _get(lap, 'LapDistance')
speed = _get(lap, 'Speed')
drs_ch = _get(lap, 'DRS')
laptime = _get(lap, 'LapTime')
if not dist:
return []
track_start = dist[0]
track_end = dist[-1]
gaps: list[tuple[float, float]] = []
if not corners:
gaps = [(track_start, track_end)]
else:
if corners[0].start_m > track_start + MIN_LEN_M:
gaps.append((track_start, corners[0].start_m))
for i in range(len(corners) - 1):
gaps.append((corners[i].end_m, corners[i + 1].start_m))
if corners[-1].end_m < track_end - MIN_LEN_M:
gaps.append((corners[-1].end_m, track_end))
result: list[Straight] = []
for start, end in gaps:
idxs = _indices_in_range(dist, start, end)
if not idxs:
continue
spds = [speed[i] for i in idxs if i < len(speed)]
v_max = max(spds) if spds else 0.0
drs_used = any(drs_ch[i] == 1.0 for i in idxs if i < len(drs_ch))
time_zone = 0.0
if laptime and idxs:
time_zone = laptime[idxs[-1]] - laptime[idxs[0]]
result.append(Straight(start_m=start, end_m=end, v_max=v_max, drs=drs_used, time_s=time_zone))
return result

0
tests/__init__.py Normal file
View file

20
tests/conftest.py Normal file
View file

@ -0,0 +1,20 @@
import os
import pytest
from telemetry.parser import load_lap
_FIXTURES = os.path.join(os.path.dirname(__file__), "fixtures")
@pytest.fixture(scope="session")
def sample_path():
return os.path.join(_FIXTURES, "sample_lap.csv")
@pytest.fixture(scope="session")
def truncated_path():
return os.path.join(_FIXTURES, "truncated.csv")
@pytest.fixture(scope="session")
def sample_lap(sample_path):
return load_lap(sample_path)

1802
tests/fixtures/sample_lap.csv vendored Normal file

File diff suppressed because it is too large Load diff

18
tests/fixtures/truncated.csv vendored Normal file
View file

@ -0,0 +1,18 @@
player,v8,По умолчанию Игрок,80,2026061217003103
Game,version,date,track,car,event,laptime [s],S1 [s],S2 [s],S3 [s],S4+ [s]
F1 22,1.5,2026-06-12 14:26:16,Zandvoort,Ferrari,P1,74.097,25.856,25.757,22.484
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 [ ]
26,4257.009765625,1,1,Overcast,1,C2,true,false,false,9,30,14.32532787322998,9,13000.0,3499.0,25.0,17.0,0.0,0.0
FWing,RWing,OnThrottle,OffThrottle,FrontCamber,RearCamber,FrontToe,RearToe,FrontSusp,RearSusp,FrontAntiRoll,RearAntiRoll,FrontSuspH,RearSuspH,BrakePressure,BrakeBias,FLTyrePressure,FRTyrePressure,RLTyrePressure,RRTyrePressure,Ballast,FuelLoad
29.0,36.0,62.0,50.0,-2.7,-1.4,0.07,0.32,6.0,1.0,5.0,2.0,4.0,6.0,100.0,50.0,23.75,23.75,21.25,21.25,6.0,20.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 [kg],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 []
1.4414062,34060.637,0.016,0.0,0.0,300.92316,10391.0,100.0,0.0,-0.0,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.325328,0.0,0.0,-336.26627,4.7674646,-94.26318,0.0028072752,0.10456319,-0.011351893,304.12772,304.15137,301.32764,301.32925,488.0,487.0,276.0,275.0,89.0,88.0,84.0,84.0,15.557972,14.019221,14.54948,14.577653,0.044921756,0.044959016,0.05015564,0.04989115,13.324485,2.273029,6.3153086,-1.5265082,-209.01431,-76.48049,-174.01189,-530.8796,339711.06,1181101.8,1522291.8,2.0,1.0,2.7680078,0.0024861791,0.0027934313,97.0,94.0,101.0,100.0,21.548914,21.363981,24.308775,24.247131,1.0649066,1.0727661,0.13441011,0.1349476,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,-30.501799,-77.82602,77.82602,-0.0041499594,-0.00121027,-0.01847571,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
5.6367188,34064.832,0.066,0.0,0.0,301.04617,10395.0,100.0,0.0,-0.0,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.324385,0.0,0.0,-335.2478,4.766578,-96.861435,0.003899289,0.10467015,-0.07305323,304.24542,304.27036,301.45407,301.45566,486.0,486.0,275.0,274.0,89.0,88.0,84.0,84.0,15.557972,14.019221,14.54948,14.577653,0.044754434,0.04498487,0.05009188,0.04989038,-6.3436623,-3.7458177,-1.7301099,-0.943879,-159.48404,-541.7071,21.575508,-110.34301,0.0,0.0,0.0,2.0,1.0,2.7679937,0.0028006455,0.0032486916,97.0,94.0,101.0,100.0,21.548693,21.363857,24.30833,24.246716,1.062713,1.0709951,0.135496,0.13602313,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,-30.516088,-77.85711,77.85711,-0.009516132,-0.0020480293,-0.009078983,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
8.4375,34067.633,0.1,0.0,0.0,301.22943,10402.0,100.0,0.0,-0.0,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.322968,0.0,0.0,-333.71753,4.7632313,-100.76466,0.008000466,0.1016143,-0.120484605,304.4287,304.45337,301.63458,301.63666,485.0,484.0,273.0,272.0,89.0,88.0,84.0,84.0,15.557972,14.019221,14.54948,14.577653,0.04449996,0.044746444,0.04983968,0.0496384,-2.8749735,-1.54717,-5.5946274,-1.8339309,244.08078,109.90586,165.17023,323.069,0.0,0.0,0.0,2.0,1.0,2.7679603,0.0033536605,0.0039516687,97.0,94.0,101.0,100.0,21.54835,21.36367,24.30766,24.24609,1.0620694,1.0702553,0.13449462,0.13518351,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,-30.536188,-77.90386,77.90386,-0.0102846,-0.0018831207,-0.011283251,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
12.636719,34071.832,0.15,0.0,0.0,301.3515,10405.0,100.0,0.0,-0.0,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.322022,0.0,0.0,-332.69583,4.759495,-103.37078,-0.0033098636,0.10347223,-0.07439932,304.5412,304.57172,301.74704,301.75604,484.0,483.0,272.0,271.0,89.0,88.0,84.0,84.0,15.557972,14.019221,14.54948,14.577653,0.04448764,0.044727005,0.050004426,0.049797866,0.18876602,3.3808506,2.2792327,6.1550646,66.62639,473.83743,-316.5041,165.91231,0.0,0.0,0.0,2.0,1.0,2.7679493,0.003717031,0.004365802,97.0,94.0,101.0,100.0,21.548122,21.363546,24.307213,24.24567,1.0584617,1.0685886,0.13125251,0.13423994,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,-30.551609,-77.9342,77.9342,0.003345168,-0.0013961294,-0.009906127,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
15.433594,34074.63,0.183,0.0,0.0,301.53012,10412.0,100.0,0.0,-0.0,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.320605,0.0,0.0,-331.16367,4.7529416,-107.278366,0.0054911603,0.10222452,0.01249686,304.73245,304.75507,301.93707,301.94464,482.0,481.0,270.0,269.0,89.0,88.0,84.0,84.0,15.557972,14.019221,14.54948,14.577653,0.044988412,0.04496178,0.050339177,0.05027435,5.356213,1.2023151,15.818361,10.042119,-484.30725,-142.46996,501.65784,-47.178104,0.0,0.0,0.0,2.0,1.0,2.7679214,0.0038654762,0.0045741796,97.0,94.0,101.0,100.0,21.547775,21.363361,24.306541,24.245045,1.0620271,1.0695267,0.13496232,0.13747232,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,-30.571978,-77.97949,77.97949,0.0051841117,-0.0018579913,-0.0045912713,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
19.636719,34078.832,0.233,0.0,0.0,301.64865,10416.0,100.0,0.0,-0.0,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.319661,0.0,0.0,-330.14267,4.7486176,-109.8822,-0.018588213,0.09899262,0.0044467063,304.8604,304.88666,302.0442,302.06235,481.0,480.0,269.0,268.0,89.0,88.0,84.0,84.0,15.557972,14.019221,14.54948,14.577653,0.045223925,0.045222133,0.050232194,0.050355557,12.730604,5.94785,-7.3358607,-0.54328966,680.08307,-221.74287,-494.99445,-356.54153,0.0,0.0,0.0,2.0,1.0,2.7679126,0.003786463,0.0044716597,97.0,94.0,101.0,100.0,21.547544,21.363237,24.306095,24.244629,1.0647341,1.0734347,0.13112372,0.1371433,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,-30.587973,-78.00859,78.00859,-0.010520617,-2.3241832E-5,0.007820944,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
22.4375,34081.633,0.267,0.0,0.0,301.82678,10422.0,100.0,0.0,-0.0,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.318245,0.0,0.0,-328.60983,4.741858,-113.79107,-0.0069472967,0.10128305,-0.07116786,305.02832,305.04843,302.21487,302.2431,479.0,479.0,267.0,266.0,89.0,88.0,84.0,84.0,15.557972,14.019221,14.54948,14.577653,0.045175612,0.044876844,0.049928077,0.050548933,-4.7920732,-7.4434476,-10.124221,2.1159775,40.29982,59.541367,-298.7022,-281.96964,0.0,0.0,0.0,2.0,1.0,2.7679057,0.0030984546,0.0039668083,97.0,94.0,101.0,100.0,21.547203,21.363049,24.305424,24.244003,1.0607195,1.0673826,0.12858017,0.13793279,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,-30.608608,-78.05361,78.05361,-0.012827186,-7.5455604E-4,0.02776532,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
26.644531,34085.84,0.317,0.0,0.0,301.94205,10426.0,100.0,0.0,-0.0,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.3173,0.0,0.0,-327.58688,4.736678,-116.39905,-0.02532983,0.09749339,0.009995961,305.1486,305.16315,302.33847,302.37128,478.0,478.0,266.0,265.0,89.0,88.0,84.0,85.0,15.557972,14.019221,14.54948,14.577653,0.04545699,0.044665035,0.049997028,0.050893392,9.305988,-4.5580263,10.266149,14.061531,105.73201,213.421,982.1091,449.053,0.0,0.0,0.0,2.0,1.0,2.767913,0.001903173,0.00283885,97.0,94.0,101.0,100.0,21.546976,21.362925,24.304976,24.243586,1.0619756,1.0667967,0.13129376,0.14215888,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,-30.623955,-78.08197,78.08197,-0.0035325303,0.001580156,0.037603874,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
29.449219,34088.645,0.35,0.0,0.0,302.11148,10433.0,100.0,0.0,-0.042444706,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.315882,0.0,0.0,-326.05133,4.730385,-120.314224,-0.0048558465,0.093519635,0.22681257,305.34058,305.37436,302.50824,302.5387,477.0,476.0,264.0,264.0,89.0,88.0,84.0,85.0,15.557972,14.019221,14.54948,14.577653,0.04621003,0.04581786,0.051095285,0.05139819,20.420458,31.740784,16.691376,11.387646,171.71814,263.5816,-493.2345,193.50179,0.0,0.0,0.0,2.0,1.0,2.7679648,4.8967556E-4,0.0010998249,97.0,94.0,101.0,100.0,21.546635,21.36274,24.304302,24.24296,1.068846,1.0800282,0.13133223,0.14141345,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,-30.639637,-78.12649,78.12649,0.0101466235,0.0016523275,0.02976934,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
33.65625,34092.85,0.4,0.0,0.0,302.2241,10437.0,100.0,0.0,-0.043604743,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.314938,0.0,0.0,-325.0279,4.729065,-122.92378,7.542996E-4,0.095818505,0.20318285,305.4571,305.4961,302.63306,302.658,476.0,475.0,263.0,262.0,89.0,89.0,85.0,85.0,15.560709,14.022046,14.54948,14.577653,0.04659386,0.046193358,0.05145239,0.051487185,3.0004277,7.5285163,7.44506,-1.6512841,-1017.33124,-445.06854,-389.09763,-516.9904,0.0,0.0,0.0,2.0,1.0,2.7680137,1.4408563E-4,6.15716E-4,97.0,94.0,101.0,100.0,21.546415,21.362616,24.30386,24.24255,1.0697355,1.0826403,0.13531771,0.1435675,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,-30.6465,-78.15744,78.15744,0.008225428,0.0029687465,0.00101771,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
1 player,v8,По умолчанию Игрок,80,2026061217003103
2 Game,version,date,track,car,event,laptime [s],S1 [s],S2 [s],S3 [s],S4+ [s]
3 F1 22,1.5,2026-06-12 14:26:16,Zandvoort,Ferrari,P1,74.097,25.856,25.757,22.484
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 26,4257.009765625,1,1,Overcast,1,C2,true,false,false,9,30,14.32532787322998,9,13000.0,3499.0,25.0,17.0,0.0,0.0
6 FWing,RWing,OnThrottle,OffThrottle,FrontCamber,RearCamber,FrontToe,RearToe,FrontSusp,RearSusp,FrontAntiRoll,RearAntiRoll,FrontSuspH,RearSuspH,BrakePressure,BrakeBias,FLTyrePressure,FRTyrePressure,RLTyrePressure,RRTyrePressure,Ballast,FuelLoad
7 29.0,36.0,62.0,50.0,-2.7,-1.4,0.07,0.32,6.0,1.0,5.0,2.0,4.0,6.0,100.0,50.0,23.75,23.75,21.25,21.25,6.0,20.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 [kg],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 1.4414062,34060.637,0.016,0.0,0.0,300.92316,10391.0,100.0,0.0,-0.0,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.325328,0.0,0.0,-336.26627,4.7674646,-94.26318,0.0028072752,0.10456319,-0.011351893,304.12772,304.15137,301.32764,301.32925,488.0,487.0,276.0,275.0,89.0,88.0,84.0,84.0,15.557972,14.019221,14.54948,14.577653,0.044921756,0.044959016,0.05015564,0.04989115,13.324485,2.273029,6.3153086,-1.5265082,-209.01431,-76.48049,-174.01189,-530.8796,339711.06,1181101.8,1522291.8,2.0,1.0,2.7680078,0.0024861791,0.0027934313,97.0,94.0,101.0,100.0,21.548914,21.363981,24.308775,24.247131,1.0649066,1.0727661,0.13441011,0.1349476,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,-30.501799,-77.82602,77.82602,-0.0041499594,-0.00121027,-0.01847571,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
10 5.6367188,34064.832,0.066,0.0,0.0,301.04617,10395.0,100.0,0.0,-0.0,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.324385,0.0,0.0,-335.2478,4.766578,-96.861435,0.003899289,0.10467015,-0.07305323,304.24542,304.27036,301.45407,301.45566,486.0,486.0,275.0,274.0,89.0,88.0,84.0,84.0,15.557972,14.019221,14.54948,14.577653,0.044754434,0.04498487,0.05009188,0.04989038,-6.3436623,-3.7458177,-1.7301099,-0.943879,-159.48404,-541.7071,21.575508,-110.34301,0.0,0.0,0.0,2.0,1.0,2.7679937,0.0028006455,0.0032486916,97.0,94.0,101.0,100.0,21.548693,21.363857,24.30833,24.246716,1.062713,1.0709951,0.135496,0.13602313,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,-30.516088,-77.85711,77.85711,-0.009516132,-0.0020480293,-0.009078983,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
11 8.4375,34067.633,0.1,0.0,0.0,301.22943,10402.0,100.0,0.0,-0.0,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.322968,0.0,0.0,-333.71753,4.7632313,-100.76466,0.008000466,0.1016143,-0.120484605,304.4287,304.45337,301.63458,301.63666,485.0,484.0,273.0,272.0,89.0,88.0,84.0,84.0,15.557972,14.019221,14.54948,14.577653,0.04449996,0.044746444,0.04983968,0.0496384,-2.8749735,-1.54717,-5.5946274,-1.8339309,244.08078,109.90586,165.17023,323.069,0.0,0.0,0.0,2.0,1.0,2.7679603,0.0033536605,0.0039516687,97.0,94.0,101.0,100.0,21.54835,21.36367,24.30766,24.24609,1.0620694,1.0702553,0.13449462,0.13518351,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,-30.536188,-77.90386,77.90386,-0.0102846,-0.0018831207,-0.011283251,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
12 12.636719,34071.832,0.15,0.0,0.0,301.3515,10405.0,100.0,0.0,-0.0,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.322022,0.0,0.0,-332.69583,4.759495,-103.37078,-0.0033098636,0.10347223,-0.07439932,304.5412,304.57172,301.74704,301.75604,484.0,483.0,272.0,271.0,89.0,88.0,84.0,84.0,15.557972,14.019221,14.54948,14.577653,0.04448764,0.044727005,0.050004426,0.049797866,0.18876602,3.3808506,2.2792327,6.1550646,66.62639,473.83743,-316.5041,165.91231,0.0,0.0,0.0,2.0,1.0,2.7679493,0.003717031,0.004365802,97.0,94.0,101.0,100.0,21.548122,21.363546,24.307213,24.24567,1.0584617,1.0685886,0.13125251,0.13423994,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,-30.551609,-77.9342,77.9342,0.003345168,-0.0013961294,-0.009906127,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
13 15.433594,34074.63,0.183,0.0,0.0,301.53012,10412.0,100.0,0.0,-0.0,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.320605,0.0,0.0,-331.16367,4.7529416,-107.278366,0.0054911603,0.10222452,0.01249686,304.73245,304.75507,301.93707,301.94464,482.0,481.0,270.0,269.0,89.0,88.0,84.0,84.0,15.557972,14.019221,14.54948,14.577653,0.044988412,0.04496178,0.050339177,0.05027435,5.356213,1.2023151,15.818361,10.042119,-484.30725,-142.46996,501.65784,-47.178104,0.0,0.0,0.0,2.0,1.0,2.7679214,0.0038654762,0.0045741796,97.0,94.0,101.0,100.0,21.547775,21.363361,24.306541,24.245045,1.0620271,1.0695267,0.13496232,0.13747232,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,-30.571978,-77.97949,77.97949,0.0051841117,-0.0018579913,-0.0045912713,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
14 19.636719,34078.832,0.233,0.0,0.0,301.64865,10416.0,100.0,0.0,-0.0,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.319661,0.0,0.0,-330.14267,4.7486176,-109.8822,-0.018588213,0.09899262,0.0044467063,304.8604,304.88666,302.0442,302.06235,481.0,480.0,269.0,268.0,89.0,88.0,84.0,84.0,15.557972,14.019221,14.54948,14.577653,0.045223925,0.045222133,0.050232194,0.050355557,12.730604,5.94785,-7.3358607,-0.54328966,680.08307,-221.74287,-494.99445,-356.54153,0.0,0.0,0.0,2.0,1.0,2.7679126,0.003786463,0.0044716597,97.0,94.0,101.0,100.0,21.547544,21.363237,24.306095,24.244629,1.0647341,1.0734347,0.13112372,0.1371433,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,-30.587973,-78.00859,78.00859,-0.010520617,-2.3241832E-5,0.007820944,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
15 22.4375,34081.633,0.267,0.0,0.0,301.82678,10422.0,100.0,0.0,-0.0,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.318245,0.0,0.0,-328.60983,4.741858,-113.79107,-0.0069472967,0.10128305,-0.07116786,305.02832,305.04843,302.21487,302.2431,479.0,479.0,267.0,266.0,89.0,88.0,84.0,84.0,15.557972,14.019221,14.54948,14.577653,0.045175612,0.044876844,0.049928077,0.050548933,-4.7920732,-7.4434476,-10.124221,2.1159775,40.29982,59.541367,-298.7022,-281.96964,0.0,0.0,0.0,2.0,1.0,2.7679057,0.0030984546,0.0039668083,97.0,94.0,101.0,100.0,21.547203,21.363049,24.305424,24.244003,1.0607195,1.0673826,0.12858017,0.13793279,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,-30.608608,-78.05361,78.05361,-0.012827186,-7.5455604E-4,0.02776532,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
16 26.644531,34085.84,0.317,0.0,0.0,301.94205,10426.0,100.0,0.0,-0.0,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.3173,0.0,0.0,-327.58688,4.736678,-116.39905,-0.02532983,0.09749339,0.009995961,305.1486,305.16315,302.33847,302.37128,478.0,478.0,266.0,265.0,89.0,88.0,84.0,85.0,15.557972,14.019221,14.54948,14.577653,0.04545699,0.044665035,0.049997028,0.050893392,9.305988,-4.5580263,10.266149,14.061531,105.73201,213.421,982.1091,449.053,0.0,0.0,0.0,2.0,1.0,2.767913,0.001903173,0.00283885,97.0,94.0,101.0,100.0,21.546976,21.362925,24.304976,24.243586,1.0619756,1.0667967,0.13129376,0.14215888,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,-30.623955,-78.08197,78.08197,-0.0035325303,0.001580156,0.037603874,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
17 29.449219,34088.645,0.35,0.0,0.0,302.11148,10433.0,100.0,0.0,-0.042444706,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.315882,0.0,0.0,-326.05133,4.730385,-120.314224,-0.0048558465,0.093519635,0.22681257,305.34058,305.37436,302.50824,302.5387,477.0,476.0,264.0,264.0,89.0,88.0,84.0,85.0,15.557972,14.019221,14.54948,14.577653,0.04621003,0.04581786,0.051095285,0.05139819,20.420458,31.740784,16.691376,11.387646,171.71814,263.5816,-493.2345,193.50179,0.0,0.0,0.0,2.0,1.0,2.7679648,4.8967556E-4,0.0010998249,97.0,94.0,101.0,100.0,21.546635,21.36274,24.304302,24.24296,1.068846,1.0800282,0.13133223,0.14141345,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,-30.639637,-78.12649,78.12649,0.0101466235,0.0016523275,0.02976934,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0
18 33.65625,34092.85,0.4,0.0,0.0,302.2241,10437.0,100.0,0.0,-0.043604743,0.0,8.0,1.0,0.0,0.0,117.0,1.0,0.0,0.0,14.314938,0.0,0.0,-325.0279,4.729065,-122.92378,7.542996E-4,0.095818505,0.20318285,305.4571,305.4961,302.63306,302.658,476.0,475.0,263.0,262.0,89.0,89.0,85.0,85.0,15.560709,14.022046,14.54948,14.577653,0.04659386,0.046193358,0.05145239,0.051487185,3.0004277,7.5285163,7.44506,-1.6512841,-1017.33124,-445.06854,-389.09763,-516.9904,0.0,0.0,0.0,2.0,1.0,2.7680137,1.4408563E-4,6.15716E-4,97.0,94.0,101.0,100.0,21.546415,21.362616,24.30386,24.24255,1.0697355,1.0826403,0.13531771,0.1435675,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,-30.6465,-78.15744,78.15744,0.008225428,0.0029687465,0.00101771,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,50.0

46
tests/test_parser.py Normal file
View file

@ -0,0 +1,46 @@
import pytest
from telemetry.parser import load_lap, laptime_s, sector_times
def test_sample_metadata(sample_lap):
assert sample_lap.session["track"] == "Zandvoort"
assert sample_lap.session["car"] == "Ferrari"
assert sample_lap.track["Lap"] == "7"
assert sample_lap.track["Tyre"] == "C2"
assert sample_lap.n > 1000
def test_all_zero_channels_dropped(sample_lap):
assert "Torque" not in sample_lap.ch
assert "IcePower" not in sample_lap.ch
def test_live_channel_present(sample_lap):
assert "Speed" in sample_lap.ch
def test_setup_trailing_comma_handled(sample_lap):
assert "FuelLoad" in sample_lap.setup
assert "" not in sample_lap.setup
def test_utf8_loads_cleanly(sample_path):
# implicit — load_lap opens with encoding='utf-8'; raises on decode error
lap = load_lap(sample_path)
assert lap.n > 0
def test_truncated_file_loads(truncated_path):
lap = load_lap(truncated_path)
assert 1 <= lap.n <= 20
def test_laptime_s(sample_lap):
lt = laptime_s(sample_lap)
assert 70.0 < lt < 80.0
def test_sector_times_sum_to_laptime(sample_lap):
lt = laptime_s(sample_lap)
s1, s2, s3 = sector_times(sample_lap)
assert abs((s1 + s2 + s3) - lt) < 0.1

87
tests/test_rename.py Normal file
View file

@ -0,0 +1,87 @@
import os
import shutil
import pytest
from telemetry.rename import lap_token_present, insert_lap_token, read_lap_number, rename_unprocessed
# --- insert_lap_token ---
@pytest.mark.parametrize("filename,lap,expected", [
# float token found → insert before it
("zandvoort_P_74.152_ferrari.csv", 8, "zandvoort_P_L8_74.152_ferrari.csv"),
# single non-float token → append
("weirdname.csv", 3, "weirdname_L3.csv"),
# two non-float tokens, no float → insert at index 2 (after 2nd token)
("track_session.csv", 5, "track_session_L5.csv"),
# extension preserved
("a_b_1.5.csv", 2, "a_b_L2_1.5.csv"),
])
def test_insert_lap_token(filename, lap, expected):
assert insert_lap_token(filename, lap) == expected
# --- lap_token_present ---
@pytest.mark.parametrize("stem,expected", [
("a_L8_b", True),
("a_L8_b.csv", True), # stem may include extension (function splits on '_')
("a_b", False),
("a_b.csv", False),
("a_L_b", False), # L without digits
("L8", True),
("notoken", False),
])
def test_lap_token_present(stem, expected):
assert lap_token_present(stem) == expected
# --- read_lap_number ---
def test_read_lap_number_sample(sample_path):
assert read_lap_number(sample_path) == 7
def test_read_lap_number_truncated(truncated_path):
assert read_lap_number(truncated_path) == 9
# --- rename_unprocessed ---
def test_rename_unprocessed_renames(tmp_path, sample_path):
src = str(tmp_path / "zandvoort_P_74.074_ferrari.csv")
shutil.copy(sample_path, src)
results = rename_unprocessed([src])
assert len(results) == 1
r = results[0]
assert r.status == "renamed"
assert r.new is not None and "L7" in r.new
assert os.path.exists(tmp_path / r.new)
def test_rename_unprocessed_skips_already_tagged(tmp_path, sample_path):
# rename first so it has L7 in name
src = str(tmp_path / "zandvoort_P_74.074_ferrari.csv")
shutil.copy(sample_path, src)
rename_unprocessed([src])
# now run on the directory — only the renamed file is present, should be skipped
results = rename_unprocessed([str(tmp_path)])
assert all(r.status == "skipped" for r in results)
def test_rename_unprocessed_error_target_exists(tmp_path, sample_path):
src = str(tmp_path / "zandvoort_P_74.074_ferrari.csv")
shutil.copy(sample_path, src)
# pre-create the destination so rename sees a conflict
dest = str(tmp_path / "zandvoort_P_L7_74.074_ferrari.csv")
shutil.copy(sample_path, dest)
results = rename_unprocessed([src])
assert len(results) == 1
assert results[0].status == "error"
def test_rename_unprocessed_dir_target(tmp_path, sample_path):
src = str(tmp_path / "zandvoort_P_74.074_ferrari.csv")
shutil.copy(sample_path, src)
results = rename_unprocessed([str(tmp_path)])
assert len(results) == 1
assert results[0].status == "renamed"

73
tests/test_reports.py Normal file
View file

@ -0,0 +1,73 @@
import os
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
def _check(result, path):
abs_path, tokens = result
assert tokens > 0
assert os.path.isabs(abs_path)
assert os.path.isfile(abs_path)
assert os.path.getsize(abs_path) > 0
assert abs_path == os.path.abspath(path)
def test_technique_generate(sample_lap, tmp_path):
out = str(tmp_path / "out_technique.md")
result = rt.generate(sample_lap, out)
_check(result, out)
def test_setup_generate(sample_lap, tmp_path):
out = str(tmp_path / "out_setup.md")
result = rs.generate(sample_lap, out)
_check(result, out)
def test_compare_generate_same_lap(sample_lap, tmp_path):
out = str(tmp_path / "out_compare.md")
result = rc.generate([sample_lap, sample_lap], out)
_check(result, out)
def test_race_generate_same_lap(sample_lap, tmp_path):
out = str(tmp_path / "out_race.md")
result = rr.generate([sample_lap, sample_lap], out)
_check(result, out)
def test_technique_output_contains_corners(sample_lap, tmp_path):
out = str(tmp_path / "tech_corners.md")
rt.generate(sample_lap, out)
with open(out, encoding="utf-8") as f:
text = f.read()
assert "## Corners" in text
assert "## Trace" in text
def test_setup_output_contains_setup(sample_lap, tmp_path):
out = str(tmp_path / "setup_check.md")
rs.generate(sample_lap, out)
with open(out, encoding="utf-8") as f:
text = f.read()
assert "## Setup" in text
assert "## Tyres" in text
class TestRegressionReports:
"""Regression: report generators must not crash on valid single-lap input."""
def test_technique_no_exception(self, sample_lap, tmp_path):
rt.generate(sample_lap, str(tmp_path / "r_tech.md"))
def test_setup_no_exception(self, sample_lap, tmp_path):
rs.generate(sample_lap, str(tmp_path / "r_setup.md"))
def test_compare_no_exception(self, sample_lap, tmp_path):
rc.generate([sample_lap], str(tmp_path / "r_cmp.md"))
def test_race_no_exception(self, sample_lap, tmp_path):
rr.generate([sample_lap], str(tmp_path / "r_race.md"))

52
tests/test_resample.py Normal file
View file

@ -0,0 +1,52 @@
import pytest
from telemetry.resample import auto_steps, adaptive_points, sample_at
from telemetry.segments import detect_corners
def test_auto_steps_ranges(sample_lap):
dense, sparse = auto_steps(sample_lap)
assert 1.0 <= dense <= 5.0
assert sparse >= 20.0
assert dense < sparse
def test_adaptive_points_monotone(sample_lap):
corners = detect_corners(sample_lap)
pts = adaptive_points(sample_lap, corners)
assert all(pts[i] < pts[i + 1] for i in range(len(pts) - 1))
def test_adaptive_points_bounds(sample_lap):
corners = detect_corners(sample_lap)
dist = sample_lap.ch["LapDistance"]
pts = adaptive_points(sample_lap, corners)
assert pts[0] == dist[0]
assert pts[-1] == dist[-1]
assert all(dist[0] <= p <= dist[-1] for p in pts)
def test_sample_at_length(sample_lap):
corners = detect_corners(sample_lap)
dists = adaptive_points(sample_lap, corners)
vals = sample_at(sample_lap, "Speed", dists)
assert len(vals) == len(dists)
def test_sample_at_endpoint_clamping(sample_lap):
dist = sample_lap.ch["LapDistance"]
speed = sample_lap.ch["Speed"]
# query exactly at and beyond the recorded endpoints
before = dist[0] - 10.0
after = dist[-1] + 10.0
vals = sample_at(sample_lap, "Speed", [before, dist[0], dist[-1], after])
assert vals[0] == speed[0]
assert vals[1] == speed[0]
assert vals[2] == speed[-1]
assert vals[3] == speed[-1]
def test_sample_at_missing_channel_returns_zeros(sample_lap):
dist = sample_lap.ch["LapDistance"]
dists = [dist[0], dist[-1]]
vals = sample_at(sample_lap, "NonExistentChannel", dists)
assert vals == [0.0, 0.0]

57
tests/test_segments.py Normal file
View file

@ -0,0 +1,57 @@
import dataclasses
import pytest
from telemetry.parser import load_lap
from telemetry.segments import detect_corners, straights
def test_zandvoort_corner_count(sample_lap):
corners = detect_corners(sample_lap)
assert 10 <= len(corners) <= 13
def test_corners_numbered_sequentially(sample_lap):
corners = detect_corners(sample_lap)
assert [c.number for c in corners] == list(range(1, len(corners) + 1))
def test_corner_start_m_strictly_increasing(sample_lap):
corners = detect_corners(sample_lap)
starts = [c.start_m for c in corners]
assert all(starts[i] < starts[i + 1] for i in range(len(starts) - 1))
def test_corner_vmin_le_ventry(sample_lap):
corners = detect_corners(sample_lap)
for c in corners:
assert c.v_min <= c.v_entry + 1.0, f"T{c.number}: v_min={c.v_min} > v_entry={c.v_entry}"
def test_corner_lock_pct_nonpositive(sample_lap):
corners = detect_corners(sample_lap)
for c in corners:
assert c.lock_pct <= 0.0, f"T{c.number}: lock_pct={c.lock_pct}"
def test_corner_spin_pct_nonnegative(sample_lap):
corners = detect_corners(sample_lap)
for c in corners:
assert c.spin_pct >= 0.0, f"T{c.number}: spin_pct={c.spin_pct}"
def test_corner_is_frozen(sample_lap):
corners = detect_corners(sample_lap)
with pytest.raises((dataclasses.FrozenInstanceError, AttributeError)):
corners[0].number = 99
def test_straights_nonempty(sample_lap):
corners = detect_corners(sample_lap)
ss = straights(corners, sample_lap)
assert len(ss) > 0
def test_truncated_lap_corners_empty(truncated_path):
lap = load_lap(truncated_path)
corners = detect_corners(lap)
# very short lap — may have few or no detectable corners; must not crash
assert isinstance(corners, list)