|
|
|
@ -8,6 +8,7 @@ import pprint |
|
|
|
|
from collections import defaultdict |
|
|
|
|
from pathlib import Path |
|
|
|
|
import matplotlib.pyplot as plt |
|
|
|
|
from tabulate import tabulate |
|
|
|
|
|
|
|
|
|
from openpilot.tools.lib.logreader import LogReader |
|
|
|
|
from openpilot.system.hardware.hw import Paths |
|
|
|
@ -22,17 +23,21 @@ def report(platform, route, _description, CP, maneuvers): |
|
|
|
|
output_fn = output_path / f"{platform}_{route.replace('/', '_')}.html" |
|
|
|
|
output_path.mkdir(exist_ok=True) |
|
|
|
|
target_cross_times = defaultdict(list) |
|
|
|
|
with open(output_fn, "w") as f: |
|
|
|
|
f.write("<h1>Longitudinal maneuver report</h1>\n") |
|
|
|
|
f.write(f"<h3>{platform}</h3>\n") |
|
|
|
|
f.write(f"<h3>{route}</h3>\n") |
|
|
|
|
|
|
|
|
|
builder = [ |
|
|
|
|
"<style>summary { cursor: pointer; }\n td, th { padding: 8px; } </style>\n", |
|
|
|
|
"<h1>Longitudinal maneuver report</h1>\n", |
|
|
|
|
f"<h3>{platform}</h3>\n", |
|
|
|
|
f"<h3>{route}</h3>\n" |
|
|
|
|
] |
|
|
|
|
if _description is not None: |
|
|
|
|
f.write(f"<h3>Description: {_description}</h3>\n") |
|
|
|
|
f.write(f"<details><summary><h3 style='display: inline-block;'>CarParams</h3></summary><pre>{format_car_params(CP)}</pre></details>\n") |
|
|
|
|
builder.append(f"<h3>Description: {_description}</h3>\n") |
|
|
|
|
builder.append(f"<details><summary><h3 style='display: inline-block;'>CarParams</h3></summary><pre>{format_car_params(CP)}</pre></details>\n") |
|
|
|
|
builder.append('{ summary }') # to be replaced below |
|
|
|
|
for description, runs in maneuvers: |
|
|
|
|
print(f'plotting maneuver: {description}, runs: {len(runs)}') |
|
|
|
|
f.write("<div style='border-top: 1px solid #000; margin: 20px 0;'></div>\n") |
|
|
|
|
f.write(f"<h2>{description}</h2>\n") |
|
|
|
|
builder.append("<div style='border-top: 1px solid #000; margin: 20px 0;'></div>\n") |
|
|
|
|
builder.append(f"<h2>{description}</h2>\n") |
|
|
|
|
for run, msgs in enumerate(runs): |
|
|
|
|
t_carControl, carControl = zip(*[(m.logMonoTime, m.carControl) for m in msgs if m.which() == 'carControl'], strict=True) |
|
|
|
|
t_carOutput, carOutput = zip(*[(m.logMonoTime, m.carOutput) for m in msgs if m.which() == 'carOutput'], strict=True) |
|
|
|
@ -54,30 +59,30 @@ def report(platform, route, _description, CP, maneuvers): |
|
|
|
|
_open = 'open' if maneuver_valid else '' |
|
|
|
|
title = f'Run #{int(run)+1}' + (' <span style="color: red">(invalid maneuver!)</span>' if not maneuver_valid else '') |
|
|
|
|
|
|
|
|
|
f.write(f"<details {_open}><summary><h3 style='display: inline-block;'>{title}</h3></summary>\n") |
|
|
|
|
builder.append(f"<details {_open}><summary><h3 style='display: inline-block;'>{title}</h3></summary>\n") |
|
|
|
|
|
|
|
|
|
# get first acceleration target and first intersection |
|
|
|
|
aTarget = longitudinalPlan[0].aTarget |
|
|
|
|
target_cross_time = None |
|
|
|
|
f.write(f'<h3 style="font-weight: normal">Initial aTarget: {aTarget} m/s^2') |
|
|
|
|
builder.append(f'<h3 style="font-weight: normal">Initial aTarget: {aTarget} m/s^2') |
|
|
|
|
|
|
|
|
|
# Localizer is noisy, require two consecutive 20Hz frames above threshold |
|
|
|
|
prev_crossed = False |
|
|
|
|
for t, lp in zip(t_livePose, livePose, strict=True): |
|
|
|
|
crossed = (0 < aTarget < lp.accelerationDevice.x) or (0 > aTarget > lp.accelerationDevice.x) |
|
|
|
|
if crossed and prev_crossed: |
|
|
|
|
f.write(f', <strong>crossed in {t:.3f}s</strong>') |
|
|
|
|
builder.append(f', <strong>crossed in {t:.3f}s</strong>') |
|
|
|
|
target_cross_time = t |
|
|
|
|
if maneuver_valid: |
|
|
|
|
target_cross_times[description].append(t) |
|
|
|
|
break |
|
|
|
|
prev_crossed = crossed |
|
|
|
|
else: |
|
|
|
|
f.write(', <strong>not crossed</strong>') |
|
|
|
|
f.write('</h3>') |
|
|
|
|
builder.append(', <strong>not crossed</strong>') |
|
|
|
|
builder.append('</h3>') |
|
|
|
|
|
|
|
|
|
pitches = [math.degrees(m.orientationNED[1]) for m in carControl] |
|
|
|
|
f.write(f'<h3 style="font-weight: normal">Average pitch: <strong>{sum(pitches) / len(pitches):0.2f} degrees</strong></h3>') |
|
|
|
|
builder.append(f'<h3 style="font-weight: normal">Average pitch: <strong>{sum(pitches) / len(pitches):0.2f} degrees</strong></h3>') |
|
|
|
|
|
|
|
|
|
plt.rcParams['font.size'] = 40 |
|
|
|
|
fig = plt.figure(figsize=(30, 26)) |
|
|
|
@ -116,16 +121,25 @@ def report(platform, route, _description, CP, maneuvers): |
|
|
|
|
buffer = io.BytesIO() |
|
|
|
|
fig.savefig(buffer, format='png') |
|
|
|
|
buffer.seek(0) |
|
|
|
|
f.write(f"<img src='data:image/png;base64,{base64.b64encode(buffer.getvalue()).decode()}' style='width:100%; max-width:800px;'>\n") |
|
|
|
|
f.write("</details>\n") |
|
|
|
|
builder.append(f"<img src='data:image/png;base64,{base64.b64encode(buffer.getvalue()).decode()}' style='width:100%; max-width:800px;'>\n") |
|
|
|
|
builder.append("</details>\n") |
|
|
|
|
|
|
|
|
|
f.write("<h2>Summary</h2>\n") |
|
|
|
|
summary = ["<h2>Summary</h2>\n"] |
|
|
|
|
cols = ['maneuver', 'crossed', 'runs', 'mean', 'min', 'max'] |
|
|
|
|
table = [] |
|
|
|
|
for description, runs in maneuvers: |
|
|
|
|
times = target_cross_times[description] |
|
|
|
|
f.write(f"<h3>{description}</h3>\n") |
|
|
|
|
f.write(f"<p>Target crossed {len(times)} out of {len(runs)} runs</p>\n") |
|
|
|
|
l = [description, len(times), len(runs)] |
|
|
|
|
if len(times): |
|
|
|
|
f.write(f"<p>Mean time to cross: {sum(times) / len(times):.3f}s, min: {min(times):.3f}s, max: {max(times):.3f}s</p>\n") |
|
|
|
|
l.extend([round(sum(times) / len(times), 2), round(min(times), 2), round(max(times), 2)]) |
|
|
|
|
table.append(l) |
|
|
|
|
summary.append(tabulate(table, headers=cols, tablefmt='html', numalign='left') + '\n') |
|
|
|
|
|
|
|
|
|
sum_idx = builder.index('{ summary }') |
|
|
|
|
builder[sum_idx:sum_idx + 1] = summary |
|
|
|
|
|
|
|
|
|
with open(output_fn, "w") as f: |
|
|
|
|
f.write(''.join(builder)) |
|
|
|
|
|
|
|
|
|
print(f"\nReport written to {output_fn}\n") |
|
|
|
|
|
|
|
|
|