Compare commits

..

No commits in common. "master" and "v1.2.0" have entirely different histories.

26 changed files with 229 additions and 1811 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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