f1-telemetry/telemetry/report_race.py

286 lines
8.5 KiB
Python

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