mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 17:13:54 +08:00
694b460f80
Provide laptop and device build entry points, cache compatibility checks and developer tooling. Keep distribution assets and model packaging available before runtime consumers. Co-authored-by: Danny <dannyirwin31@yahoo.com> Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu> Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com> Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Co-authored-by: prabhaavp <prabhaav@msu.edu>
172 lines
6.6 KiB
Python
172 lines
6.6 KiB
Python
"""Offline longitudinal command and estimated-motion trace summary."""
|
|
|
|
import argparse
|
|
from collections import Counter
|
|
import hashlib
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
|
|
from tools.diagnostics.performance import Distribution
|
|
|
|
|
|
SERVICES = {"longitudinalPlan", "carControl", "carState", "selfdriveState"}
|
|
SIGNALS = {"longitudinalPlan": "plannerATarget", "carControl": "commandAccel",
|
|
"carState": "aEgoEstimate"}
|
|
FIELDS = {"source": "longitudinalPlan", "experimental": "selfdriveState",
|
|
"longActive": "carControl", "gasPressed": "carState", "brakePressed": "carState"}
|
|
|
|
|
|
def _field(body, name):
|
|
try:
|
|
return getattr(body, name)
|
|
except (AttributeError, RuntimeError):
|
|
return None
|
|
|
|
|
|
def _finite(value):
|
|
return type(value) in (int, float) and math.isfinite(value)
|
|
|
|
|
|
def _value(service, body):
|
|
if service == "longitudinalPlan":
|
|
return _field(body, "aTarget")
|
|
if service == "carControl":
|
|
return _field(_field(body, "actuators"), "accel")
|
|
return _field(body, "aEgo")
|
|
|
|
|
|
def _sha(path):
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as source:
|
|
while chunk := source.read(1024 * 1024):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def analyze(messages, *, max_gap_s=0.25, context_age_s=0.5):
|
|
if not 0 < max_gap_s <= context_age_s:
|
|
raise ValueError("Expected 0 < max_gap_s <= context_age_s")
|
|
contexts = {}
|
|
previous = {}
|
|
transitions = Counter()
|
|
rejected = Counter()
|
|
counts = Counter()
|
|
samples = {name: Distribution() for name in SIGNALS.values()}
|
|
intervals = {name: Distribution() for name in SIGNALS.values()}
|
|
delta_rates = {name: Distribution() for name in SIGNALS.values()}
|
|
pedal_context = Counter()
|
|
|
|
for message in messages:
|
|
try:
|
|
service = message.which()
|
|
except (AttributeError, RuntimeError):
|
|
continue
|
|
if service not in SERVICES:
|
|
continue
|
|
counts[service] += 1
|
|
timestamp = _field(message, "logMonoTime")
|
|
if not _finite(timestamp) or timestamp < 0 or _field(message, "valid") is not True:
|
|
rejected[f"{service}:invalid_envelope"] += 1
|
|
previous.clear()
|
|
for name, owner in FIELDS.items():
|
|
if owner == service:
|
|
contexts.pop(name, None)
|
|
continue
|
|
timestamp = timestamp / 1e9
|
|
body = _field(message, service)
|
|
if body is None:
|
|
rejected[f"{service}:missing_body"] += 1
|
|
previous.clear()
|
|
for name, owner in FIELDS.items():
|
|
if owner == service:
|
|
contexts.pop(name, None)
|
|
continue
|
|
updates = {}
|
|
if service == "longitudinalPlan":
|
|
updates["source"] = str(_field(body, "longitudinalPlanSource"))
|
|
elif service == "selfdriveState":
|
|
updates["experimental"] = _field(body, "experimentalMode")
|
|
elif service == "carControl":
|
|
updates["longActive"] = _field(body, "longActive")
|
|
else:
|
|
updates["gasPressed"] = _field(body, "gasPressed")
|
|
updates["brakePressed"] = _field(body, "brakePressed")
|
|
if any(name in contexts and timestamp < contexts[name][0] for name in updates):
|
|
rejected[f"{service}:timestamp_regression"] += 1
|
|
previous.clear()
|
|
continue
|
|
for name, value in updates.items():
|
|
prior = contexts.get(name)
|
|
if prior is not None and prior[1] != value:
|
|
transitions[name] += 1
|
|
previous.clear()
|
|
contexts[name] = (timestamp, value)
|
|
if service not in SIGNALS:
|
|
continue
|
|
signal = SIGNALS[service]
|
|
value = _value(service, body)
|
|
if not _finite(value):
|
|
samples[signal].add(None, "missing_or_nonfinite")
|
|
previous.pop(service, None)
|
|
continue
|
|
samples[signal].add(value)
|
|
if any(name not in contexts or not 0 <= timestamp - contexts[name][0] <= context_age_s for name in FIELDS):
|
|
rejected[f"{service}:missing_or_stale_mode"] += 1
|
|
previous.pop(service, None)
|
|
continue
|
|
mode = tuple(contexts[name][1] for name in FIELDS)
|
|
if mode[0] not in ("cruise", "lead0", "lead1", "lead2", "e2e") or any(type(mode[index]) is not bool for index in (1, 2, 3, 4)):
|
|
rejected[f"{service}:invalid_mode"] += 1
|
|
previous.pop(service, None)
|
|
continue
|
|
pedal_context[f"gas={mode[3]},brake={mode[4]}"] += 1
|
|
prior = previous.get(service)
|
|
if prior is not None:
|
|
elapsed = timestamp - prior[0]
|
|
if elapsed <= 0:
|
|
rejected[f"{service}:timestamp_regression"] += 1
|
|
elif elapsed > max_gap_s:
|
|
rejected[f"{service}:gap"] += 1
|
|
elif mode != prior[2]:
|
|
rejected[f"{service}:mode_changed"] += 1
|
|
else:
|
|
intervals[signal].add(elapsed)
|
|
delta_rates[signal].add((value - prior[1]) / elapsed)
|
|
previous[service] = (timestamp, value, mode)
|
|
|
|
return {"limits": {"maxContinuousGapSeconds": max_gap_s, "maxContextAgeSeconds": context_age_s},
|
|
"messageCounts": dict(counts), "transitions": dict(transitions), "pedalContextSamples": dict(pedal_context),
|
|
"rejectedPairsOrSamples": dict(rejected),
|
|
"signals": {name: {"valuesMps2": samples[name].summary(),
|
|
"continuousFrameDeltaSeconds": intervals[name].summary(),
|
|
"continuousDeltaRateMps3": delta_rates[name].summary()}
|
|
for name in SIGNALS.values()}}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("rlog", type=Path, help="Existing local rlog or qlog segment")
|
|
parser.add_argument("--max-gap-s", type=float, default=0.25)
|
|
parser.add_argument("--context-age-s", type=float, default=0.5)
|
|
args = parser.parse_args()
|
|
path = args.rlog.resolve(strict=True)
|
|
if not path.is_file() or path.suffix not in (".zst", ".bz2", ".rlog", ".qlog"):
|
|
parser.error("Expected a local log file")
|
|
from openpilot.tools.lib.logreader import LogReader
|
|
report = analyze(LogReader(str(path)), max_gap_s=args.max_gap_s, context_age_s=args.context_age_s)
|
|
report["input"] = {"path": str(path), "sha256": _sha(path), "sizeBytes": path.stat().st_size,
|
|
"kind": "qlog_decimated" if "qlog" in path.name else "rlog_segment"}
|
|
root = Path(__file__).resolve().parents[2]
|
|
report["sourceHashes"] = {name: _sha(root / name) for name in (
|
|
"tools/diagnostics/longitudinal.py", "openpilot/cereal/log.capnp", "opendbc_repo/opendbc/car/car.capnp")}
|
|
report["provenance"] = {"plannerATarget": "longitudinalPlan.aTarget planner target, m/s²",
|
|
"commandAccel": "carControl.actuators.accel command, m/s²",
|
|
"aEgoEstimate": "carState.aEgo speed-derived vehicle estimate, m/s²",
|
|
"deltaRate": "sample-to-sample signal derivative, not physical jerk or publisher rate"}
|
|
print(json.dumps(report, sort_keys=True, allow_nan=False))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|