f1-telemetry/telemetry/resample.py
2026-06-13 18:53:52 +03:00

145 lines
3.8 KiB
Python

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