Performance¶
RoboMetrics keeps safety metrics NumPy-first so they remain practical in local
CI. lane_departure_rate() and offroad_rate() use vectorized polygon
containment through points_in_polygon(). collision_rate_obb() evaluates all
timesteps for each actor with a batched separating-axis test instead of a Python
loop per timestep.
For repeatable JSON timing baselines, run:
python scripts/benchmark_timing_baselines.py --repeat 3 --output timing_baseline.json
The output contains schema_version, robometrics_version, per-case run
durations, best time, mean time, and deterministic case metadata. Commit a
baseline only when the machine class and workload size are meaningful for your
project; timing values are expected to vary across laptops and CI runners.
Use this smoke benchmark when changing geometry kernels:
import time
import numpy as np
from robometrics import collision_rate_obb, lane_departure_rate
n = 10_000
ego = np.column_stack([np.linspace(0.0, 100.0, n), np.zeros(n)])
lane = np.array([[-1.0, -5.0], [101.0, -5.0], [101.0, 5.0], [-1.0, 5.0]])
start = time.perf_counter()
lane_departure_rate(ego, lane)
print("lane_departure_rate", time.perf_counter() - start)
actors = [ego + np.array([0.2, 0.0]) for _ in range(10)]
yaws = np.zeros(n)
dims = np.array([4.5, 2.0])
start = time.perf_counter()
collision_rate_obb(
ego_traj=ego,
ego_dims=dims,
ego_yaws=yaws,
actor_trajs=actors,
actor_dims=[dims for _ in actors],
actor_yaws=[yaws for _ in actors],
)
print("collision_rate_obb", time.perf_counter() - start)
The launch target is that 10 Hz, 10-second scenario checks with 10 actors stay comfortably below one second on a typical developer laptop.
Dataset-Scale Driving¶
Use this benchmark when changing evaluator aggregation, prediction metrics, or driving safety metrics. It keeps the data synthetic and deterministic while exercising the same path a CI smoke gate would use.
import time
import numpy as np
from robometrics import Evaluator
rng = np.random.default_rng(7)
samples = 200
timesteps = 20
base = np.stack(
[
np.column_stack(
[
np.linspace(0.0, 20.0, timesteps),
np.full(timesteps, lane),
]
)
for lane in np.linspace(-1.0, 1.0, samples)
]
)
ground_truths = [trajectory for trajectory in base]
predictions = [
trajectory + rng.normal(scale=0.15, size=trajectory.shape)
for trajectory in ground_truths
]
start = time.perf_counter()
result = Evaluator().evaluate_dataset(
predictions=predictions,
ground_truths=ground_truths,
metrics=["ade", "fde"],
thresholds={"ade": 0.5, "fde": 1.0},
bootstrap_ci=1000,
)
print("dataset_driving", time.perf_counter() - start)
print(result.summary())
For release checks, the useful signal is trend and order of magnitude. Keep the same sample count and random seed when comparing two commits.
Manipulation Workloads¶
Manipulation metrics are scalar and vectorized, so a dataset-scale check should
batch many short episodes through evaluate_dataset() or through direct metric
calls in a loop. The example below exercises end-effector tracking and
force/joint-limit style signals without requiring a simulator.
import time
import numpy as np
from robometrics import (
contact_richness,
end_effector_tracking_error,
force_limit_compliance,
joint_limit_violation_rate,
)
rng = np.random.default_rng(11)
episodes = 500
timesteps = 30
lower_limits = np.array([-1.0, -1.0, -1.0])
upper_limits = np.array([1.0, 1.0, 1.0])
start = time.perf_counter()
errors = []
compliance = []
violations = []
contacts = []
for _ in range(episodes):
target = rng.normal(scale=0.2, size=(timesteps, 3)).cumsum(axis=0)
executed = target + rng.normal(scale=0.03, size=target.shape)
forces = rng.normal(scale=0.4, size=(timesteps, 3))
joints = rng.uniform(-1.1, 1.1, size=(timesteps, 3))
errors.append(end_effector_tracking_error(executed, target))
compliance.append(force_limit_compliance(forces, max_force=1.5))
violations.append(joint_limit_violation_rate(joints, lower_limits, upper_limits))
contacts.append(contact_richness(forces, threshold=0.1))
print("manipulation_dataset", time.perf_counter() - start)
print("mean_ee_error", float(np.mean(errors)))
print("mean_force_compliance", float(np.mean(compliance)))
print("mean_joint_violation_rate", float(np.mean(violations)))
print("mean_contact_richness", float(np.mean(contacts)))
These benchmarks are documentation smoke tests, not hard performance contracts. Use them to catch accidental algorithmic regressions before adding stricter machine-specific timing gates.