From 31f2d0f612a10db93e7c438aa9725494886afcc9 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:46:02 -0700 Subject: [PATCH] Add offline route-richness reporting separate from music judging --- roadscore/tools/characterize_routes.py | 99 ++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 roadscore/tools/characterize_routes.py diff --git a/roadscore/tools/characterize_routes.py b/roadscore/tools/characterize_routes.py new file mode 100644 index 0000000000..2236e1f78d --- /dev/null +++ b/roadscore/tools/characterize_routes.py @@ -0,0 +1,99 @@ +"""Offline route richness only. Never imported by generation or playback.""" +import argparse +from collections import Counter +import json +from pathlib import Path +import numpy as np +from openpilot.tools.lib.logreader import LogReader + + +def characterize(folder): + services = Counter() + segments = [] + speeds, curvatures = [], [] + moving_seconds = 0 + first = last = previous_car = None + previous_signals = (False, False) + previous_standstill = None + nav_key = previous_nav_distance = previous_lane = None + speed = 0 + counts = Counter() + active_curve = False + strongest = [] + for segment in sorted(folder.glob('*--*'), key=lambda p: int(p.name.rsplit('--', 1)[1])): + files = sorted(segment.glob('rlog*')) or sorted(segment.glob('qlog*')) + if not files: + continue + row = {'segment': int(segment.name.rsplit('--', 1)[1]), 'readable': False} + try: + for event in LogReader(str(files[0])): + kind = event.which() + services[kind] += 1 + t = event.logMonoTime / 1e9 + first = t if first is None else min(first, t) + last = t if last is None else max(last, t) + if kind == 'carState': + car = event.carState + speed = float(car.vEgo) + speeds.append(speed) + if previous_car is not None and speed > .5: + moving_seconds += max(0, min(.2, t-previous_car)) + previous_car = t + signals = (bool(car.leftBlinker), bool(car.rightBlinker)) + counts['turn_signal_activations'] += sum(now and not before for now, before in zip(signals, previous_signals)) + previous_signals = signals + stopped = bool(car.standstill) + if previous_standstill is not None and stopped != previous_standstill: + counts['stop_events' if stopped else 'resume_events'] += 1 + previous_standstill = stopped + elif kind == 'modelV2': + model = event.modelV2 + samples = [abs(float(y))/max(speed, 3) for dt, y in zip(model.orientationRate.t, model.orientationRate.z) if 1 <= dt <= 5] + if samples and speed > 3: + curvature = max(samples) + curvatures.append(curvature) + if curvature > 1/150 and not active_curve: + counts['curve_proxy_events'] += 1 + strongest.append({'log_monotonic_seconds': t, 'max_curvature_per_meter': curvature}) + active_curve = True + elif curvature < 1/300: + active_curve = False + lane = str(model.meta.laneChangeState) + if lane != previous_lane and lane not in ('off', '0'): + counts['lane_change_state_transitions'] += 1 + previous_lane = lane + elif kind == 'navInstruction' and event.valid: + nav = event.navInstruction + key = (str(nav.maneuverType), str(nav.maneuverModifier)) + distance = float(nav.maneuverDistance) + if key != nav_key or (previous_nav_distance is not None and distance > previous_nav_distance+100): + counts['navigation_maneuvers'] += 1 + counts['arrival_messages'] += nav.maneuverType == 'arrive' + nav_key, previous_nav_distance = key, distance + row['readable'] = True + except Exception as error: + row.update(error_type=type(error).__name__, error=str(error)) + segments.append(row) + def distribution(values): + return dict(zip(['min', 'p50', 'p90', 'p99', 'max'], map(float, np.percentile(values, [0, 50, 90, 99, 100])))) if values else None + return {'purpose': 'Offline route richness; never influences runtime generation or human musical ranking', + 'duration_seconds': None if first is None else last-first, + 'moving_seconds': moving_seconds, 'speed_meters_per_second': distribution(speeds), + 'curvature_proxy_per_meter': distribution(curvatures), + 'curve_proxy_policy': 'Model predicted max abs yaw-rate / current speed over 1–5 s; speed >3 m/s; enter radius<150m, release radius>300m. Proxy, not annotated road truth.', + 'events': dict(counts), 'strongest_curve_proxy_onsets': sorted(strongest, key=lambda x: -x['max_curvature_per_meter'])[:5], + 'services': dict(services), 'segments': segments, + 'required_messages_present': all(services[x] > 0 for x in ['modelV2', 'carState', 'roadEncodeIdx']), + 'navigation_present': services['navInstruction'] > 0, + 'all_available_logs_readable': bool(segments) and all(s['readable'] for s in segments)} + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('root', type=Path) + args = parser.parse_args() + for folder in sorted(args.root.iterdir()): + if folder.is_dir(): + result = characterize(folder) + (folder / 'characterization.json').write_text(json.dumps(result, indent=2)) + print(folder.name, result['duration_seconds'], result['all_available_logs_readable'], flush=True)