Files
StarPilot/starpilot/system/the_galaxy/flm_workspace.py
T
firestar5683 4a98785aa3 FLM
2026-07-13 14:56:23 -05:00

2605 lines
109 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import json
import math
import os
import shutil
import signal
import subprocess
import sys
import threading
import time
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any
import numpy as np
from cereal import car
from opendbc.car.hyundai.values import HyundaiFlags
from openpilot.common.params import Params
from openpilot.selfdrive.controls.lib.latcontrol_torque import KP
from openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes import (
FLM_FRICTION_SPEED_KNOTS,
get_flm_capabilities,
get_flm_rich_profile_key,
get_flm_supported_vehicle_knobs,
get_gm_base_friction_threshold,
get_hkg_canfd_base_friction_threshold,
get_standard_friction_threshold,
normalize_flm_overrides,
)
from openpilot.starpilot.common.lateral_delay import full_lateral_delay
from openpilot.system.hardware import PC
from openpilot.system.hardware.hw import Paths
from openpilot.tools.lib.logreader import LogReader
from openpilot.starpilot.system.the_galaxy import utilities
FLM_STATUS_PATH = Path("/tmp/galaxy_flm_status.json")
FLM_LOG_PATH = Path("/tmp/galaxy_flm.log")
FLM_STATUS_MAX_AGE_SECONDS = 3600.0
FLM_ANALYZER_ROUTE_LIMIT = 8
FLM_ANALYZER_PROCESS = None
FLM_ANALYZER_LOCK = threading.Lock()
TRIAL_PARAM_SPECS = {
"AdvancedLateralTune": "bool",
"ForceAutoTune": "bool",
"ForceAutoTuneOff": "bool",
"UseAutoSteerDelay": "bool",
"SteerDelay": "float",
"SteerFriction": "float",
"SteerKP": "float",
"SteerLatAccel": "float",
"SteerRatio": "float",
"FLMActiveProfileId": "string",
"FLMActiveOverrides": "json",
"FLMTrialApplied": "bool",
}
FLM_ADVANCED_LATERAL_PARAM_KEYS = {
"AdvancedLateralTune",
"ForceAutoTune",
"ForceAutoTuneOff",
"UseAutoSteerDelay",
"SteerDelay",
"SteerFriction",
"SteerKP",
"SteerLatAccel",
"SteerRatio",
}
FLM_TRIAL_BASELINE_PARAM = "FLMTrialBaseline"
GENERIC_PARAM_METADATA = {
"SteerDelay": {"min": 0.01, "max": 1.0, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
"SteerFriction": {"min": 0.0, "max": 1.0, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
"SteerKP": {"min": 0.1, "max": 1.5, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
"SteerLatAccel": {"min": 0.5, "max": 5.0, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
"SteerRatio": {"min": 5.0, "max": 25.0, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
}
FLM_REFERENCE_MODEL = {
"version": 1,
"families": {
"turn_in_boost": {
"reason": "Used when the car waits too long to initiate torque even though desired lateral accel is already rising.",
"too_low": "Turn starts late or feels unwilling.",
"too_high": "Car dives into the curve too early or spikes past the plan on entry.",
},
"unwind_taper": {
"reason": "Used when the car holds steering too long or drops it too quickly on exit.",
"too_low": "Unwind drags and the car keeps steering past the intended release.",
"too_high": "Unwind snaps back and the wheel releases too aggressively.",
},
"friction_threshold_curve": {
"reason": "Used to calm chatter or wake up low-speed response without pretending the whole torque map is wrong.",
"too_low": "Tiny errors create twitch and correction chatter.",
"too_high": "Controller feels reluctant near center and can hesitate at low speed.",
},
"center_taper": {
"reason": "Used only when the problem is calm-road or near-center nibbling, not general curve response.",
"too_low": "Straight-road wheel activity stays busy.",
"too_high": "Car feels lazy around center and can miss light corrections.",
},
},
}
FLM_PATH_SPECS = {
"baseline_fix": {
"title": "Baseline Fix",
"description": "Use the broad knobs first to get the car into the right zip code before touching narrower cleanup layers.",
"whenToUse": "Use this when the car is broadly wrong: repeated line riding, multi-band under/oversteer, saturation, or obvious whole-car mismatch.",
"alternateHint": "If the car is already mostly good and only one band is bothering you, switch to Cleanup Pass instead.",
},
"cleanup_pass": {
"title": "Cleanup Pass",
"description": "Use the narrow band-specific knobs first so you can clean up one behavior without disturbing the rest of the tune.",
"whenToUse": "Use this when the car is already mostly in the right zip code and the misses are localized to one phase or speed band.",
"alternateHint": "If the car is still broadly wrong after this, step back and run Baseline Fix first.",
},
}
FLM_DRIVER_OVERRIDE_PRE_BUFFER_S = 0.35
FLM_DRIVER_OVERRIDE_POST_BUFFER_S = 1.0
@dataclass
class RouteSource:
route: str
footage_path: str
segment: str
segment_num: int
log_path: str
used_qlog: bool
@dataclass
class FLMSample:
route: str
segment: int
t: float
v_ego: float
lat_active: bool
steering_pressed: bool
saturated: bool
actual_la: float
desired_la: float
desired_jerk: float
error: float
error_rate: float
p: float
i: float
d: float
f: float
output: float
steering_angle_deg: float
steering_torque: float
cmd_torque: float
out_torque: float
roll_deg: float
def _get_galaxy_dir() -> Path:
return Path(Paths.comma_home()) / "starpilot" / "data" / "galaxy" if PC else Path("/data/galaxy")
def get_flm_workspace_root() -> Path:
return _get_galaxy_dir() / "flm"
def _legacy_workspace_root() -> Path:
return _get_galaxy_dir() / "".join(("f", "t", "m"))
def _migrate_legacy_payload(value):
legacy_upper = "".join(("F", "T", "M"))
legacy_lower = legacy_upper.lower()
if isinstance(value, dict):
migrated = {}
for key, item in value.items():
migrated_key = str(key)
if migrated_key.startswith(legacy_upper):
migrated_key = f"FLM{migrated_key[len(legacy_upper):]}"
elif migrated_key.startswith(legacy_lower):
migrated_key = f"flm{migrated_key[len(legacy_lower):]}"
migrated[migrated_key] = _migrate_legacy_payload(item)
return migrated
if isinstance(value, list):
return [_migrate_legacy_payload(item) for item in value]
if isinstance(value, str):
legacy_method_name = "Firestar " + "Tuning Method"
return value.replace(legacy_method_name, "Firestar Lateral Method").replace(legacy_upper, "FLM")
return value
def _migrate_legacy_workspace(root: Path) -> None:
marker = root / ".flm_rebrand_v1"
if marker.exists():
return
legacy_root = _legacy_workspace_root()
if legacy_root.is_dir() and legacy_root != root:
root.parent.mkdir(parents=True, exist_ok=True)
if root.exists():
shutil.copytree(legacy_root, root, dirs_exist_ok=True)
shutil.rmtree(legacy_root)
else:
legacy_root.replace(root)
if not root.exists():
return
legacy_lower = "".join(("f", "t", "m"))
legacy_reference = root / "reference" / f"{legacy_lower}_reference.json"
current_reference = root / "reference" / "flm_reference.json"
if legacy_reference.is_file() and not current_reference.exists():
legacy_reference.replace(current_reference)
for path in root.rglob("*.json"):
try:
payload = json.loads(path.read_text(encoding="utf-8"))
migrated = _migrate_legacy_payload(payload)
if migrated != payload:
path.write_text(json.dumps(migrated, indent=2, sort_keys=True), encoding="utf-8")
except Exception:
continue
legacy_upper = legacy_lower.upper()
for path in root.rglob("*.html"):
try:
content = path.read_text(encoding="utf-8")
content = content.replace(legacy_upper, "FLM").replace(legacy_lower, "flm")
path.write_text(content, encoding="utf-8")
except Exception:
continue
marker.touch()
def _workspace_paths() -> dict[str, Path]:
root = get_flm_workspace_root()
return {
"root": root,
"reports": root / "reports",
"profiles": root / "profiles",
"feedback": root / "feedback",
"snapshots": root / "snapshots",
"reference": root / "reference",
}
def ensure_flm_workspace() -> dict[str, Path]:
_migrate_legacy_workspace(get_flm_workspace_root())
paths = _workspace_paths()
for key, path in paths.items():
if key != "root":
path.mkdir(parents=True, exist_ok=True)
reference_path = paths["reference"] / "flm_reference.json"
if not reference_path.exists():
reference_path.write_text(json.dumps(FLM_REFERENCE_MODEL, indent=2, sort_keys=True), encoding="utf-8")
return paths
def _read_json(path: Path, default):
try:
return json.loads(path.read_text(encoding="utf-8"))
except Exception:
return default
def _write_json(path: Path, payload) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
tmp_path = path.with_suffix(path.suffix + ".tmp")
tmp_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
tmp_path.replace(path)
def _worker_env(repo_root: Path) -> dict[str, str]:
env = os.environ.copy()
pythonpath = [
"/usr/local/venv/lib/python3.12/site-packages",
str(repo_root / "starpilot" / "third_party"),
str(repo_root),
]
if env.get("PYTHONPATH"):
pythonpath.append(env["PYTHONPATH"])
env["PYTHONPATH"] = os.pathsep.join(pythonpath)
env.setdefault("OPENBLAS_NUM_THREADS", "1")
env.setdefault("OMP_NUM_THREADS", "1")
env.setdefault("MKL_NUM_THREADS", "1")
env.setdefault("NUMEXPR_NUM_THREADS", "1")
return env
def read_flm_status() -> dict[str, Any]:
data = _read_json(FLM_STATUS_PATH, {})
return data if isinstance(data, dict) else {}
def _write_flm_status(payload: dict[str, Any]) -> None:
payload = dict(payload)
payload["updatedAt"] = time.time()
tmp_path = FLM_STATUS_PATH.with_suffix(".tmp")
tmp_path.write_text(json.dumps(payload, separators=(",", ":")), encoding="utf-8")
tmp_path.replace(FLM_STATUS_PATH)
def clear_flm_status() -> None:
try:
FLM_STATUS_PATH.unlink()
except FileNotFoundError:
pass
except OSError:
pass
def flm_analyzer_running() -> bool:
process = FLM_ANALYZER_PROCESS
if process is not None and process.poll() is None:
return True
status = read_flm_status()
pid = int(status.get("pid") or 0)
started_at = float(status.get("startedAt") or 0.0)
if pid <= 0 or started_at <= 0:
return False
if (time.time() - started_at) > FLM_STATUS_MAX_AGE_SECONDS:
clear_flm_status()
return False
try:
os.kill(pid, 0)
except ProcessLookupError:
clear_flm_status()
return False
except PermissionError:
return True
except OSError:
return False
return True
def stop_flm_background_analysis() -> bool:
global FLM_ANALYZER_PROCESS
with FLM_ANALYZER_LOCK:
process = FLM_ANALYZER_PROCESS
status = read_flm_status()
pid = int(status.get("pid") or 0)
if process is not None and process.poll() is None:
process.terminate()
try:
process.wait(timeout=2.0)
except subprocess.TimeoutExpired:
process.kill()
FLM_ANALYZER_PROCESS = None
clear_flm_status()
return True
if pid > 0:
try:
os.kill(pid, signal.SIGTERM)
except ProcessLookupError:
pass
except OSError:
return False
clear_flm_status()
return True
return False
def start_flm_background_analysis(route_names: list[str], footage_paths: list[str]) -> bool:
global FLM_ANALYZER_PROCESS
route_names = [str(route) for route in route_names if str(route).strip()]
if not route_names:
return False
ensure_flm_workspace()
with FLM_ANALYZER_LOCK:
if flm_analyzer_running():
return True
repo_root = Path(__file__).resolve().parents[3]
command = [
"nice",
"-n",
"19",
sys.executable or "python3",
str(Path(__file__).resolve()),
"worker",
json.dumps({
"routes": route_names[:FLM_ANALYZER_ROUTE_LIMIT],
"footagePaths": [str(path) for path in footage_paths],
}),
]
log_file = None
try:
log_file = open(FLM_LOG_PATH, "ab")
FLM_ANALYZER_PROCESS = subprocess.Popen(
command,
cwd=str(repo_root),
env=_worker_env(repo_root),
stdout=log_file,
stderr=log_file,
start_new_session=True,
)
_write_flm_status({
"pid": FLM_ANALYZER_PROCESS.pid,
"startedAt": time.time(),
"running": True,
"state": "queued",
"routes": route_names[:FLM_ANALYZER_ROUTE_LIMIT],
"progress": 0,
"total": len(route_names[:FLM_ANALYZER_ROUTE_LIMIT]),
})
except Exception:
FLM_ANALYZER_PROCESS = None
return False
finally:
if log_file is not None:
log_file.close()
return flm_analyzer_running()
def _parse_segment_num(segment_name: str) -> int:
try:
return int(str(segment_name).rsplit("--", 1)[1])
except Exception:
return 0
def resolve_route_sources(route_names: list[str], footage_paths: list[str]) -> tuple[list[RouteSource], list[str]]:
sources: list[RouteSource] = []
warnings: list[str] = []
for route in route_names:
route_added = False
for footage_path in footage_paths:
segments = utilities.get_segments_in_route(route, footage_path)
if not segments:
continue
for segment in segments:
segment_path = Path(footage_path) / segment
rlog_path = None
qlog_path = None
for candidate in ("rlog.zst", "rlog.bz2", "rlog"):
candidate_path = segment_path / candidate
if candidate_path.exists():
rlog_path = candidate_path
break
for candidate in ("qlog.zst", "qlog.bz2", "qlog"):
candidate_path = segment_path / candidate
if candidate_path.exists():
qlog_path = candidate_path
break
log_path = rlog_path or qlog_path
if log_path is None:
continue
if rlog_path is None and qlog_path is not None:
warnings.append(f"{route} segment {segment} fell back to qlog.")
sources.append(RouteSource(
route=route,
footage_path=str(footage_path),
segment=segment,
segment_num=_parse_segment_num(segment),
log_path=str(log_path),
used_qlog=rlog_path is None,
))
route_added = True
if route_added:
break
if not route_added:
warnings.append(f"{route} could not be resolved to a local route with logs.")
sources.sort(key=lambda source: (source.route, source.segment_num))
return sources, warnings
def _speed_band_label(v_ego: float) -> str:
if v_ego < 6.0:
return "low"
if v_ego < 15.0:
return "mid"
if v_ego < 25.0:
return "fast"
return "highway"
def _route_label(route: str, segment: int) -> str:
return f"{route}/{segment}"
def _event_direction(samples: list[FLMSample]) -> str:
mean_desired = float(np.mean([sample.desired_la for sample in samples]))
if mean_desired > 0.02:
return "left"
if mean_desired < -0.02:
return "right"
return "center"
def _group_masked_events(samples: list[FLMSample], mask: list[bool], score_series: list[float], min_points: int = 5) -> list[dict[str, Any]]:
events: list[dict[str, Any]] = []
start_idx = None
for idx, active in enumerate(mask + [False]):
if active and start_idx is None:
start_idx = idx
continue
if active:
continue
if start_idx is None:
continue
end_idx = idx - 1
event_samples = samples[start_idx:end_idx + 1]
if len(event_samples) >= min_points:
event_scores = score_series[start_idx:end_idx + 1]
peak_offset = int(np.argmax(event_scores))
peak_idx = start_idx + peak_offset
peak_sample = samples[peak_idx]
direction = _event_direction(event_samples)
events.append({
"startIdx": start_idx,
"endIdx": end_idx,
"peakIdx": peak_idx,
"peakScore": float(event_scores[peak_offset]),
"route": peak_sample.route,
"segment": peak_sample.segment,
"speedBand": _speed_band_label(float(np.mean([sample.v_ego for sample in event_samples]))),
"direction": direction,
"supportCount": len(event_samples),
})
start_idx = None
return events
def _analysis_eligibility_mask(samples: list[FLMSample]) -> list[bool]:
eligible = [bool(sample.lat_active) for sample in samples]
group_start = 0
while group_start < len(samples):
group_key = (samples[group_start].route, samples[group_start].segment)
group_end = group_start + 1
while group_end < len(samples) and (samples[group_end].route, samples[group_end].segment) == group_key:
group_end += 1
last_override = -math.inf
for idx in range(group_start, group_end):
sample = samples[idx]
if sample.steering_pressed:
last_override = sample.t
if sample.steering_pressed or (sample.t - last_override) <= FLM_DRIVER_OVERRIDE_POST_BUFFER_S:
eligible[idx] = False
next_override = math.inf
for idx in range(group_end - 1, group_start - 1, -1):
sample = samples[idx]
if sample.steering_pressed:
next_override = sample.t
if sample.steering_pressed or (next_override - sample.t) <= FLM_DRIVER_OVERRIDE_PRE_BUFFER_S:
eligible[idx] = False
# Force an event boundary between route segments even when lateral control stays active.
eligible[group_start] = False
eligible[group_end - 1] = False
group_start = group_end
return eligible
def _build_plot_data(samples: list[FLMSample], event: dict[str, Any], eligibility: list[bool] | None = None) -> dict[str, Any]:
start_idx = int(event["startIdx"])
end_idx = int(event["endIdx"])
event_route = samples[start_idx].route
event_segment = samples[start_idx].segment
# Add a small amount of context without crossing an intervention buffer or
# segment boundary. The highlighted region remains the classified event.
for _ in range(12):
candidate = start_idx - 1
if candidate < 0 or (eligibility is not None and not eligibility[candidate]):
break
if samples[candidate].route != event_route or samples[candidate].segment != event_segment:
break
start_idx = candidate
for _ in range(12):
candidate = end_idx + 1
if candidate >= len(samples) or (eligibility is not None and not eligibility[candidate]):
break
if samples[candidate].route != event_route or samples[candidate].segment != event_segment:
break
end_idx = candidate
window = samples[start_idx:end_idx + 1]
if len(window) < 2:
return {}
# Keep reports lightweight on unusually long windows while preserving both ends.
if len(window) > 160:
indices = np.linspace(0, len(window) - 1, 160, dtype=int)
window = [window[int(idx)] for idx in indices]
times = np.array([sample.t for sample in window], dtype=float)
desired = np.array([sample.desired_la for sample in window], dtype=float)
actual = np.array([sample.actual_la for sample in window], dtype=float)
relative_times = times - float(times[0])
event_start_time = max(float(samples[int(event["startIdx"])].t - times[0]), 0.0)
event_end_time = max(float(samples[int(event["endIdx"])].t - times[0]), event_start_time)
return {
"times": [round(float(value), 3) for value in relative_times],
"desired": [round(float(value), 4) for value in desired],
"actual": [round(float(value), 4) for value in actual],
"windowDurationSec": round(float(relative_times[-1]), 2),
"eventStartSec": round(event_start_time, 2),
"eventEndSec": round(event_end_time, 2),
"eventDurationSec": round(max(event_end_time - event_start_time, 0.0), 2),
"meanSpeedMph": round(float(np.mean([sample.v_ego for sample in window])) * 2.236936, 1),
"route": event_route,
"segment": event_segment,
"segmentLabel": _route_label(event_route, event_segment),
"direction": str(event.get("direction", "center")),
"speedBand": str(event.get("speedBand", "mixed")),
"driverOverrideFree": bool(eligibility is None or all(eligibility[start_idx:end_idx + 1])),
}
def _build_plot_svg(plot_data: dict[str, Any]) -> str:
times = np.array(plot_data.get("times", []), dtype=float)
desired = np.array(plot_data.get("desired", []), dtype=float)
actual = np.array(plot_data.get("actual", []), dtype=float)
if len(times) < 2 or len(desired) != len(times) or len(actual) != len(times):
return ""
time_span = max(float(times.max()), 1e-3)
y_min = float(min(np.min(desired), np.min(actual)))
y_max = float(max(np.max(desired), np.max(actual)))
y_pad = max((y_max - y_min) * 0.10, 0.1)
y_min -= y_pad
y_max += y_pad
y_span = max(y_max - y_min, 1e-3)
def _points(series):
coords = []
for t_val, y_val in zip(times, series, strict=True):
x = (float(t_val) / time_span) * 380.0
y = 120.0 - (((float(y_val) - y_min) / y_span) * 120.0)
coords.append(f"{x:.1f},{y:.1f}")
return " ".join(coords)
return (
"<svg viewBox='0 0 380 140' class='flm-plot' preserveAspectRatio='none'>"
"<rect x='0' y='0' width='380' height='140' rx='8' ry='8' fill='#0f172a'/>"
"<line x1='0' y1='120' x2='380' y2='120' stroke='#334155' stroke-width='1'/>"
f"<polyline fill='none' stroke='#ef4444' stroke-width='2' points='{_points(desired)}'/>"
f"<polyline fill='none' stroke='#38bdf8' stroke-width='2' points='{_points(actual)}'/>"
"</svg>"
)
def _segment_samples(segment_source: RouteSource) -> tuple[list[FLMSample], car.CarParams | None, dict[str, str]]:
samples: list[FLMSample] = []
car_params = None
init_data: dict[str, str] = {}
latest: dict[str, Any] = {}
for msg in LogReader(segment_source.log_path, sort_by_time=True):
which = msg.which()
if which == "carParams" and car_params is None:
car_params = msg.carParams
continue
if which == "initData":
init = msg.initData
init_data = {
"gitCommit": str(getattr(init, "gitCommit", "") or ""),
"gitBranch": str(getattr(init, "gitBranch", "") or ""),
}
continue
if which == "carState":
latest["carState"] = msg.carState
continue
if which == "carControl":
latest["carControl"] = msg.carControl
continue
if which == "carOutput":
latest["carOutput"] = msg.carOutput
continue
if which == "liveParameters":
latest["liveParameters"] = msg.liveParameters
continue
if which != "controlsState" or "carState" not in latest or "carControl" not in latest:
continue
controls_state = msg.controlsState
lateral_state = controls_state.lateralControlState
if lateral_state.which() != "torqueState":
continue
torque_state = lateral_state.torqueState
car_state = latest["carState"]
car_control = latest["carControl"]
live_parameters = latest.get("liveParameters")
car_output = latest.get("carOutput")
roll_deg = math.degrees(float(getattr(live_parameters, "roll", 0.0) or 0.0)) if live_parameters is not None else 0.0
out_torque = float(getattr(getattr(car_output, "actuatorsOutput", None), "torque", 0.0) or 0.0) if car_output is not None else 0.0
samples.append(FLMSample(
route=segment_source.route,
segment=segment_source.segment_num,
t=float(msg.logMonoTime) / 1e9,
v_ego=float(getattr(car_state, "vEgo", 0.0) or 0.0),
lat_active=bool(getattr(car_control, "latActive", False)),
steering_pressed=bool(getattr(car_state, "steeringPressed", False)),
saturated=bool(getattr(torque_state, "saturated", False)),
actual_la=float(getattr(torque_state, "actualLateralAccel", 0.0) or 0.0),
desired_la=float(getattr(torque_state, "desiredLateralAccel", 0.0) or 0.0),
desired_jerk=float(getattr(torque_state, "desiredLateralJerk", 0.0) or 0.0),
error=float(getattr(torque_state, "error", 0.0) or 0.0),
error_rate=float(getattr(torque_state, "errorRate", 0.0) or 0.0),
p=float(getattr(torque_state, "p", 0.0) or 0.0),
i=float(getattr(torque_state, "i", 0.0) or 0.0),
d=float(getattr(torque_state, "d", 0.0) or 0.0),
f=float(getattr(torque_state, "f", 0.0) or 0.0),
output=float(getattr(torque_state, "output", 0.0) or 0.0),
steering_angle_deg=float(getattr(car_state, "steeringAngleDeg", 0.0) or 0.0),
steering_torque=float(getattr(car_state, "steeringTorque", 0.0) or 0.0),
cmd_torque=float(getattr(getattr(car_control, "actuators", None), "torque", 0.0) or 0.0),
out_torque=out_torque,
roll_deg=roll_deg,
))
return samples, car_params, init_data
def _current_param_state(CP, params: Params) -> dict[str, Any]:
advanced_enabled = params.get_bool("AdvancedLateralTune")
torque_tune = CP.lateralTuning.torque if CP.lateralTuning.which() == "torque" else None
stock_delay = full_lateral_delay(float(getattr(CP, "steerActuatorDelay", 0.0) or 0.0))
stock_ratio = float(getattr(CP, "steerRatio", 0.0) or 0.0)
stock_friction = float(getattr(torque_tune, "friction", 0.0) or 0.0) if torque_tune is not None else 0.0
stock_lat_accel = float(getattr(torque_tune, "latAccelFactor", 0.0) or 0.0) if torque_tune is not None else 0.0
return {
"AdvancedLateralTune": advanced_enabled,
"ForceAutoTune": params.get_bool("ForceAutoTune"),
"ForceAutoTuneOff": params.get_bool("ForceAutoTuneOff"),
"UseAutoSteerDelay": params.get_bool("UseAutoSteerDelay"),
"SteerDelay": params.get_float("SteerDelay", return_default=True, default=stock_delay) if advanced_enabled else stock_delay,
"SteerFriction": params.get_float("SteerFriction", return_default=True, default=stock_friction) if advanced_enabled else stock_friction,
"SteerKP": params.get_float("SteerKP", return_default=True, default=KP) if advanced_enabled else KP,
"SteerLatAccel": params.get_float("SteerLatAccel", return_default=True, default=stock_lat_accel) if advanced_enabled else stock_lat_accel,
"SteerRatio": params.get_float("SteerRatio", return_default=True, default=stock_ratio) if advanced_enabled else stock_ratio,
"FLMActiveProfileId": params.get("FLMActiveProfileId", encoding="utf-8") or "",
"FLMActiveOverrides": normalize_flm_overrides(params.get("FLMActiveOverrides", encoding="utf-8") or "{}"),
"FLMTrialApplied": params.get_bool("FLMTrialApplied"),
}
def _stock_param_state(CP, capabilities: dict[str, Any]) -> dict[str, Any]:
torque_tune = CP.lateralTuning.torque if CP.lateralTuning.which() == "torque" else None
friction_family = str(capabilities.get("frictionFamily", "standard"))
rich_profile = capabilities.get("richProfileKey")
rich_knobs = {
symbol: float(meta["defaultValue"])
for symbol, meta in get_flm_supported_vehicle_knobs().items()
if rich_profile and meta.get("profile") == rich_profile
}
return {
"UseAutoSteerDelay": True,
"SteerDelay": full_lateral_delay(float(getattr(CP, "steerActuatorDelay", 0.0) or 0.0)),
"SteerFriction": float(getattr(torque_tune, "friction", 0.0) or 0.0) if torque_tune is not None else 0.0,
"SteerKP": float(KP),
"SteerLatAccel": float(getattr(torque_tune, "latAccelFactor", 0.0) or 0.0) if torque_tune is not None else 0.0,
"SteerRatio": float(getattr(CP, "steerRatio", 0.0) or 0.0),
"FLMBaseFrictionThresholds": {
friction_family: {
"speedKnots": list(FLM_FRICTION_SPEED_KNOTS),
"values": _baseline_family_curve(friction_family),
},
} if torque_tune is not None else {},
"FLMVehicleKnobs": rich_knobs,
}
def _nonlinear_torque_map(CP) -> dict[str, Any]:
if str(getattr(CP, "brand", "") or "") != "gm":
return {}
try:
from opendbc.car.gm.interface import NON_LINEAR_TORQUE_PARAMS
except (ImportError, AttributeError):
return {}
raw_params = NON_LINEAR_TORQUE_PARAMS.get(CP.carFingerprint)
if raw_params is None:
return {}
if isinstance(raw_params, dict):
left = [float(value) for value in raw_params.get("left", [])]
right = [float(value) for value in raw_params.get("right", [])]
else:
left = [float(value) for value in raw_params]
right = list(left)
if len(left) != 4 or len(right) != 4:
return {}
return {
"type": "siglin",
"left": left,
"right": right,
"asymmetric": any(not math.isclose(left[idx], right[idx], abs_tol=1e-9) for idx in range(4)),
"learnedByLiveTorque": False,
}
def _baseline_family_curve(family: str) -> list[float]:
getter = {
"gm": get_gm_base_friction_threshold,
"standard": get_standard_friction_threshold,
"hkg_canfd": get_hkg_canfd_base_friction_threshold,
}.get(family, get_standard_friction_threshold)
return [round(float(getter(knot)), 4) for knot in FLM_FRICTION_SPEED_KNOTS]
def _current_family_curve(family: str, current: dict[str, Any]) -> list[float]:
active_overrides = current.get("FLMActiveOverrides", {}) if isinstance(current, dict) else {}
payload = active_overrides.get("baseFrictionThresholds", {}).get(family, {}) if isinstance(active_overrides, dict) else {}
values = payload.get("values", []) if isinstance(payload, dict) else []
if isinstance(values, list) and len(values) == len(FLM_FRICTION_SPEED_KNOTS):
try:
return [round(float(value), 4) for value in values]
except Exception:
pass
return _baseline_family_curve(family)
def _clamp(value: float, lower: float, upper: float) -> float:
return min(max(float(value), lower), upper)
def _round_to_precision(value: float, precision: float) -> float:
if precision <= 0:
return float(value)
steps = round(float(value) / precision)
return round(steps * precision, 6)
def _current_vehicle_knob_value(symbol: str, current: dict[str, Any]) -> float | None:
knob = get_flm_supported_vehicle_knobs().get(symbol)
if knob is None:
return None
active_overrides = current.get("FLMActiveOverrides", {}) if isinstance(current, dict) else {}
vehicle_knobs = active_overrides.get("vehicleKnobs", {}) if isinstance(active_overrides, dict) else {}
try:
return float(vehicle_knobs.get(symbol, knob["defaultValue"]))
except Exception:
return float(knob["defaultValue"])
def _vehicle_knob_adjustment(symbol: str, delta: float, current: dict[str, Any] | None = None) -> dict[str, Any] | None:
knob = get_flm_supported_vehicle_knobs().get(symbol)
if knob is None:
return None
current_value = _current_vehicle_knob_value(symbol, current or {})
if current_value is None:
return None
suggested_value = _round_to_precision(_clamp(current_value + delta, knob["min"], knob["max"]), knob["precision"])
if math.isclose(current_value, suggested_value, abs_tol=max(float(knob["precision"]) / 2.0, 1e-6)):
return None
return {
"type": "vehicle_knob",
"symbol": symbol,
"current": current_value,
"suggested": suggested_value,
"delta": round(suggested_value - current_value, 4),
}
def _rich_profile_supports_knob(capabilities: dict[str, Any], suffix: str) -> bool:
rich_profile = capabilities.get("richProfileKey")
if not rich_profile:
return False
return f"{rich_profile}.{suffix}" in get_flm_supported_vehicle_knobs()
def _build_event_summaries(samples: list[FLMSample]) -> tuple[list[dict[str, Any]], dict[str, Any]]:
eligibility = _analysis_eligibility_mask(samples)
active_samples = [sample for sample, allowed in zip(samples, eligibility, strict=True) if allowed]
if not active_samples:
return [], {"sampleCount": 0}
over_error = [abs(sample.actual_la) - abs(sample.desired_la) for sample in samples]
desired = [sample.desired_la for sample in samples]
jerk = [sample.desired_jerk for sample in samples]
angle = [sample.steering_angle_deg for sample in active_samples]
output = [sample.output for sample in active_samples]
entry_phase = [(abs(d) > 0.30 and abs(j) > 0.25 and d * j > 0.0) for d, j in zip(desired, jerk, strict=True)]
unwind_phase = [(abs(d) > 0.25 and abs(j) > 0.20 and d * j < 0.0) for d, j in zip(desired, jerk, strict=True)]
steady_curve_phase = [(abs(d) > 0.35 and abs(j) < 0.18) for d, j in zip(desired, jerk, strict=True)]
saturation_phase = [
bool(allowed and sample.saturated and abs(sample.desired_la) > 0.30 and (abs(sample.desired_jerk) > 0.16 or abs(sample.actual_la) > 0.45))
for sample, allowed in zip(samples, eligibility, strict=True)
]
base_masks = {
"understeer": [(allowed and phase and (ov < -0.20)) for allowed, phase, ov in zip(eligibility, steady_curve_phase, over_error, strict=True)],
"oversteer": [(allowed and phase and (ov > 0.20)) for allowed, phase, ov in zip(eligibility, steady_curve_phase, over_error, strict=True)],
"late_turn_in": [(allowed and phase and ov < -0.16) for allowed, phase, ov in zip(eligibility, entry_phase, over_error, strict=True)],
"early_turn_in": [(allowed and phase and ov > 0.16) for allowed, phase, ov in zip(eligibility, entry_phase, over_error, strict=True)],
"unwind_too_slow": [(allowed and phase and ov > 0.14) for allowed, phase, ov in zip(eligibility, unwind_phase, over_error, strict=True)],
"unwind_too_fast": [(allowed and phase and ov < -0.14) for allowed, phase, ov in zip(eligibility, unwind_phase, over_error, strict=True)],
"low_speed_unwillingness": [
bool(allowed and sample.v_ego < 6.0 and abs(sample.desired_la) > 0.30 and abs(sample.desired_jerk) > 0.18 and
(abs(sample.actual_la) + 0.18) < abs(sample.desired_la))
for sample, allowed in zip(samples, eligibility, strict=True)
],
"saturation_limited": saturation_phase,
}
score_map = {
"understeer": [max((-ov), 0.0) for ov in over_error],
"oversteer": [max(ov, 0.0) for ov in over_error],
"late_turn_in": [max((-ov), 0.0) + abs(j) * 0.1 for ov, j in zip(over_error, jerk, strict=True)],
"early_turn_in": [max(ov, 0.0) + abs(j) * 0.1 for ov, j in zip(over_error, jerk, strict=True)],
"unwind_too_slow": [max(ov, 0.0) for ov in over_error],
"unwind_too_fast": [max((-ov), 0.0) for ov in over_error],
"low_speed_unwillingness": [max(abs(d) - abs(sample.actual_la), 0.0) for d, sample in zip(desired, samples, strict=True)],
"saturation_limited": [1.0 if sample.saturated else 0.0 for sample in samples],
}
# Straight-road chatter detection uses a simple 4-second window.
straight_windows = []
for start_idx in range(0, max(len(samples) - 20, 1), 10):
window = samples[start_idx:start_idx + 40]
if len(window) < 20:
continue
if not all(eligibility[start_idx:start_idx + len(window)]):
continue
if float(np.mean([sample.v_ego for sample in window])) < 20.0:
continue
if float(np.mean([abs(sample.desired_la) for sample in window])) > 0.12:
continue
centered_angles = np.array([sample.steering_angle_deg for sample in window]) - float(np.mean([sample.steering_angle_deg for sample in window]))
sign_changes = int(np.sum(np.sign(centered_angles[1:]) != np.sign(centered_angles[:-1])))
amplitude = float(np.max(centered_angles) - np.min(centered_angles))
chatter_score = (amplitude * 0.25) + (sign_changes * 0.04)
if amplitude > 0.45 and sign_changes >= 6:
straight_windows.append({
"startIdx": start_idx,
"endIdx": start_idx + len(window) - 1,
"peakIdx": start_idx + int(len(window) / 2),
"peakScore": chatter_score,
"route": window[0].route,
"segment": window[0].segment,
"speedBand": "highway",
"direction": "center",
"supportCount": len(window),
})
curve_windows = []
for start_idx in range(0, max(len(samples) - 20, 1), 8):
window = samples[start_idx:start_idx + 36]
if len(window) < 18:
continue
if not all(eligibility[start_idx:start_idx + len(window)]):
continue
if float(np.mean([sample.v_ego for sample in window])) < 15.0:
continue
desired_sign = float(np.mean([sample.desired_la for sample in window]))
if abs(desired_sign) < 0.35:
continue
if any((sample.desired_la * desired_sign) < 0.0 for sample in window):
continue
error_series = np.array([sample.actual_la - sample.desired_la for sample in window])
sign_changes = int(np.sum(np.sign(error_series[1:]) != np.sign(error_series[:-1])))
amplitude = float(np.max(error_series) - np.min(error_series))
if amplitude > 0.22 and sign_changes >= 4:
curve_windows.append({
"startIdx": start_idx,
"endIdx": start_idx + len(window) - 1,
"peakIdx": start_idx + int(np.argmax(np.abs(error_series))),
"peakScore": amplitude + sign_changes * 0.03,
"route": window[0].route,
"segment": window[0].segment,
"speedBand": _speed_band_label(float(np.mean([sample.v_ego for sample in window]))),
"direction": "left" if desired_sign > 0.0 else "right",
"supportCount": len(window),
})
summaries: list[dict[str, Any]] = []
for bucket, mask in base_masks.items():
events = _group_masked_events(samples, mask, score_map[bucket])
if events:
summaries.extend(_summaries_from_events(bucket, samples, events, eligibility))
if straight_windows:
summaries.extend(_summaries_from_events("center_chatter", samples, straight_windows, eligibility))
if curve_windows:
summaries.extend(_summaries_from_events("notchy_mid_curve", samples, curve_windows, eligibility))
left_errors = [abs(sample.actual_la) - abs(sample.desired_la) for sample in active_samples if sample.desired_la > 0.25]
right_errors = [abs(sample.actual_la) - abs(sample.desired_la) for sample in active_samples if sample.desired_la < -0.25]
summary_stats = {
"sampleCount": len(active_samples),
"excludedDriverOverrideSamples": sum(1 for sample, allowed in zip(samples, eligibility, strict=True) if sample.lat_active and not allowed),
"qlogFallback": False,
"meanDesiredAbs": round(float(np.mean(np.abs([sample.desired_la for sample in active_samples]))), 4),
"meanErrorAbs": round(float(np.mean(np.abs([sample.actual_la - sample.desired_la for sample in active_samples]))), 4),
"leftBias": round(float(np.mean(left_errors)), 4) if left_errors else 0.0,
"rightBias": round(float(np.mean(right_errors)), 4) if right_errors else 0.0,
"highwayStraightAngleP2P": round(float(np.percentile(np.abs(angle), 95) - np.percentile(np.abs(angle), 5)), 4) if angle else 0.0,
"meanOutputAbs": round(float(np.mean(np.abs(output))), 4) if output else 0.0,
}
if summary_stats["meanErrorAbs"] < 0.08 and not any(summary["severity"] > 0.65 for summary in summaries):
summaries.append({
"bucket": "model_limited",
"dimensionId": "model_limited:overall",
"direction": "center",
"speedBand": "mixed",
"count": 1,
"severity": 0.25,
"evidence": {
"speedBand": "mixed",
"directionBias": "center",
"eventCount": 1,
"segments": [],
},
"events": [],
"plotSvg": "",
"plotData": {},
})
return sorted(summaries, key=lambda item: item["severity"], reverse=True), summary_stats
def _summaries_from_events(bucket: str, samples: list[FLMSample], events: list[dict[str, Any]],
eligibility: list[bool] | None = None) -> list[dict[str, Any]]:
grouped: dict[tuple[str, str], list[dict[str, Any]]] = {}
for event in events:
key = (bucket, event["direction"])
grouped.setdefault(key, []).append(event)
summaries = []
for (bucket_name, direction), grouped_events in grouped.items():
grouped_events.sort(key=lambda item: item["peakScore"], reverse=True)
strongest = grouped_events[:3]
strongest_labels = [
{
"route": event["route"],
"segment": event["segment"],
"label": _route_label(event["route"], event["segment"]),
"score": round(float(event["peakScore"]), 3),
}
for event in strongest
]
top_event = strongest[0]
top_speed_band = top_event["speedBand"]
plot_data = _build_plot_data(samples, top_event, eligibility)
summaries.append({
"bucket": bucket_name,
"dimensionId": f"{bucket_name}:{direction}:{top_speed_band}",
"direction": direction,
"speedBand": top_speed_band,
"count": len(grouped_events),
"severity": round(float(min(1.5, np.mean([event["peakScore"] for event in strongest]))), 3),
"evidence": {
"speedBand": top_speed_band,
"directionBias": direction,
"eventCount": len(grouped_events),
"segments": strongest_labels,
},
"events": grouped_events,
"plotSvg": _build_plot_svg(plot_data),
"plotData": plot_data,
})
return summaries
def classify_torque_samples(samples: list[FLMSample]) -> tuple[list[dict[str, Any]], dict[str, Any]]:
return _build_event_summaries(samples)
def _primary_delta_from_summary(summary: dict[str, Any], capabilities: dict[str, Any], current: dict[str, Any],
strategy: str = "cleanup") -> dict[str, Any] | None:
bucket = summary["bucket"]
direction = summary["direction"]
severity = max(float(summary["severity"]), 0.2)
speed_band = str(summary.get("speedBand", "mixed"))
rich_profile = capabilities.get("richProfileKey")
family = capabilities.get("frictionFamily", "standard")
side = "left" if direction != "right" else "right"
curvy_band = speed_band in ("mid", "fast")
supports_low_speed_assist = _rich_profile_supports_knob(capabilities, "low_speed_angle_assist_max_torque")
supports_crawl_turn_in = _rich_profile_supports_knob(capabilities, f"crawl_turn_in_ff_boost_{side}")
supports_turn_in_boost = _rich_profile_supports_knob(capabilities, f"turn_in_boost_{side}")
supports_unwind_taper = _rich_profile_supports_knob(capabilities, f"unwind_taper_{side}")
supports_curvy_turn_in_trim = _rich_profile_supports_knob(capabilities, f"curvy_turn_in_trim_{side}")
supports_curvy_turn_in_speed = _rich_profile_supports_knob(capabilities, "curvy_turn_in_trim_speed_max")
supports_curvy_speed_max = _rich_profile_supports_knob(capabilities, "curvy_speed_max")
supports_curvy_unwind_extra = _rich_profile_supports_knob(capabilities, f"curvy_unwind_extra_reduction_{side}")
supports_curvy_unwind_floor = _rich_profile_supports_knob(capabilities, f"curvy_unwind_floor_relief_{side}")
supports_ff_gain = _rich_profile_supports_knob(capabilities, f"ff_gain_{side}")
nonlinear_map = capabilities.get("nonlinearTorqueMap", {})
asymmetric_nonlinear_map = bool(isinstance(nonlinear_map, dict) and nonlinear_map.get("asymmetric"))
if bucket == "model_limited":
return None
if strategy == "baseline":
if bucket in ("center_chatter", "notchy_mid_curve"):
current_curve = _current_family_curve(family, current)
deltas = [0.0, 0.01, 0.02, 0.025, 0.03] if bucket == "center_chatter" else [0.0, 0.0, 0.015, 0.02, 0.02]
scale = min(max(severity, 0.4), 1.2)
suggested = [round(current_curve[idx] + (delta * scale), 4) for idx, delta in enumerate(deltas)]
return {
"type": "friction_curve",
"symbol": f"base_friction_threshold.{family}",
"family": family,
"current": current_curve,
"suggested": suggested,
"delta": [round(suggested[idx] - current_curve[idx], 4) for idx in range(len(current_curve))],
}
if bucket == "low_speed_unwillingness":
current_curve = _current_family_curve(family, current)
deltas = [-0.03, -0.025, -0.015, -0.005, 0.0]
scale = min(max(severity, 0.5), 1.2)
suggested = [round(max(0.05, current_curve[idx] + (delta * scale)), 4) for idx, delta in enumerate(deltas)]
return {
"type": "friction_curve",
"symbol": f"base_friction_threshold.{family}",
"family": family,
"current": current_curve,
"suggested": suggested,
"delta": [round(suggested[idx] - current_curve[idx], 4) for idx in range(len(current_curve))],
}
if bucket in ("understeer", "late_turn_in", "saturation_limited"):
if asymmetric_nonlinear_map and direction in ("left", "right") and supports_ff_gain:
adjustment = _vehicle_knob_adjustment(f"{rich_profile}.ff_gain_{side}", 0.025 * severity, current)
if adjustment is not None:
return adjustment
current_value = float(current["SteerLatAccel"])
scale = 0.04 if bucket == "saturation_limited" else 0.03
suggested_value = round(_clamp(current_value + max(scale, current_value * scale * severity), 0.5, 5.0), 4)
return {"type": "generic_param", "paramKey": "SteerLatAccel", "current": current_value, "suggested": suggested_value, "delta": round(suggested_value - current_value, 4)}
if bucket in ("oversteer", "early_turn_in"):
if asymmetric_nonlinear_map and direction in ("left", "right") and supports_ff_gain:
adjustment = _vehicle_knob_adjustment(f"{rich_profile}.ff_gain_{side}", -0.025 * severity, current)
if adjustment is not None:
return adjustment
current_value = float(current["SteerLatAccel"])
suggested_value = round(_clamp(current_value - max(0.03, current_value * 0.03 * severity), 0.5, 5.0), 4)
return {"type": "generic_param", "paramKey": "SteerLatAccel", "current": current_value, "suggested": suggested_value, "delta": round(suggested_value - current_value, 4)}
if bucket in ("unwind_too_slow", "unwind_too_fast"):
current_value = float(current["SteerFriction"])
direction_mult = -1.0 if bucket == "unwind_too_slow" else 1.0
suggested_value = round(_clamp(current_value + (0.015 * severity * direction_mult), 0.0, 1.0), 4)
return {"type": "generic_param", "paramKey": "SteerFriction", "current": current_value, "suggested": suggested_value, "delta": round(suggested_value - current_value, 4)}
if bucket in ("center_chatter", "notchy_mid_curve"):
current_curve = _current_family_curve(family, current)
if bucket == "center_chatter":
deltas = [0.0, 0.01, 0.02, 0.025, 0.03]
else:
deltas = [0.0, 0.0, 0.015, 0.02, 0.02]
scale = min(max(severity, 0.4), 1.2)
suggested = [round(current_curve[idx] + (delta * scale), 4) for idx, delta in enumerate(deltas)]
return {
"type": "friction_curve",
"symbol": f"base_friction_threshold.{family}",
"family": family,
"current": current_curve,
"suggested": suggested,
"delta": [round(suggested[idx] - current_curve[idx], 4) for idx in range(len(current_curve))],
}
if bucket == "low_speed_unwillingness":
current_curve = _current_family_curve(family, current)
deltas = [-0.03, -0.025, -0.015, -0.005, 0.0]
scale = min(max(severity, 0.5), 1.2)
if supports_low_speed_assist:
adjustment = _vehicle_knob_adjustment(f"{rich_profile}.low_speed_angle_assist_max_torque", 0.04 * scale, current)
if adjustment is not None:
return adjustment
if supports_crawl_turn_in:
adjustment = _vehicle_knob_adjustment(f"{rich_profile}.crawl_turn_in_ff_boost_{side}", 0.03 * scale, current)
if adjustment is not None:
return adjustment
if rich_profile and supports_turn_in_boost:
adjustment = _vehicle_knob_adjustment(f"{rich_profile}.turn_in_boost_{side}", 0.025 * scale, current)
if adjustment is not None:
return adjustment
suggested = [round(max(0.05, current_curve[idx] + (delta * scale)), 4) for idx, delta in enumerate(deltas)]
return {
"type": "friction_curve",
"symbol": f"base_friction_threshold.{family}",
"family": family,
"current": current_curve,
"suggested": suggested,
"delta": [round(suggested[idx] - current_curve[idx], 4) for idx in range(len(current_curve))],
}
if bucket in ("understeer", "late_turn_in"):
if curvy_band and speed_band == "fast" and bucket == "late_turn_in" and supports_curvy_turn_in_speed:
adjustment = _vehicle_knob_adjustment(f"{rich_profile}.curvy_turn_in_trim_speed_max", 1.6 * severity, current)
if adjustment is not None:
return adjustment
if curvy_band and supports_curvy_turn_in_trim:
adjustment = _vehicle_knob_adjustment(f"{rich_profile}.curvy_turn_in_trim_{side}", -0.018 * severity, current)
if adjustment is not None:
return adjustment
if rich_profile and supports_turn_in_boost:
adjustment = _vehicle_knob_adjustment(f"{rich_profile}.turn_in_boost_{side}", 0.02 * severity, current)
if adjustment is not None:
return adjustment
current_value = float(current["SteerLatAccel"])
suggested_value = round(_clamp(current_value + max(0.03, current_value * 0.03 * severity), 0.5, 5.0), 4)
return {"type": "generic_param", "paramKey": "SteerLatAccel", "current": current_value, "suggested": suggested_value, "delta": round(suggested_value - current_value, 4)}
if bucket in ("oversteer", "early_turn_in"):
if curvy_band and supports_curvy_turn_in_trim:
adjustment = _vehicle_knob_adjustment(f"{rich_profile}.curvy_turn_in_trim_{side}", 0.018 * severity, current)
if adjustment is not None:
return adjustment
if rich_profile and supports_turn_in_boost:
adjustment = _vehicle_knob_adjustment(f"{rich_profile}.turn_in_boost_{side}", -0.02 * severity, current)
if adjustment is not None:
return adjustment
current_value = float(current["SteerLatAccel"])
suggested_value = round(_clamp(current_value - max(0.03, current_value * 0.03 * severity), 0.5, 5.0), 4)
return {"type": "generic_param", "paramKey": "SteerLatAccel", "current": current_value, "suggested": suggested_value, "delta": round(suggested_value - current_value, 4)}
if bucket in ("unwind_too_slow", "unwind_too_fast"):
if curvy_band and supports_curvy_unwind_extra:
if bucket == "unwind_too_slow" and speed_band == "fast" and supports_curvy_speed_max:
adjustment = _vehicle_knob_adjustment(f"{rich_profile}.curvy_speed_max", 1.8 * severity, current)
if adjustment is not None:
return adjustment
direction_mult = 1.0 if bucket == "unwind_too_slow" else -1.0
adjustment = _vehicle_knob_adjustment(f"{rich_profile}.curvy_unwind_extra_reduction_{side}", 0.03 * severity * direction_mult, current)
if adjustment is not None:
return adjustment
if rich_profile and supports_unwind_taper:
direction_mult = 1.0 if bucket == "unwind_too_slow" else -1.0
adjustment = _vehicle_knob_adjustment(f"{rich_profile}.unwind_taper_{side}", 0.08 * severity * direction_mult, current)
if adjustment is not None:
return adjustment
current_value = float(current["SteerFriction"])
direction_mult = -1.0 if bucket == "unwind_too_slow" else 1.0
suggested_value = round(_clamp(current_value + (0.015 * severity * direction_mult), 0.0, 1.0), 4)
return {"type": "generic_param", "paramKey": "SteerFriction", "current": current_value, "suggested": suggested_value, "delta": round(suggested_value - current_value, 4)}
if bucket == "saturation_limited":
if curvy_band and supports_curvy_unwind_floor:
adjustment = _vehicle_knob_adjustment(f"{rich_profile}.curvy_unwind_floor_relief_{side}", 0.04 * severity, current)
if adjustment is not None:
return adjustment
current_value = float(current["SteerLatAccel"])
suggested_value = round(_clamp(current_value + max(0.04, current_value * 0.04 * severity), 0.5, 5.0), 4)
return {"type": "generic_param", "paramKey": "SteerLatAccel", "current": current_value, "suggested": suggested_value, "delta": round(suggested_value - current_value, 4)}
return None
def _observed_behavior(summary: dict[str, Any]) -> str:
bucket = summary["bucket"]
direction = summary["direction"]
speed_band = summary["speedBand"]
direction_text = "" if direction == "center" else f" on {direction} {speed_band} inputs"
mapping = {
"understeer": f"The car is not matching requested lateral accel{direction_text}; it stays wider than plan before recovery.",
"oversteer": f"The car is exceeding requested lateral accel{direction_text}; it is stepping past plan before correcting back.",
"late_turn_in": f"Turn-in is late{direction_text}; desired lateral accel is already building while actual response lags.",
"early_turn_in": f"Turn-in is too eager{direction_text}; actual response jumps ahead of the plan during entry.",
"unwind_too_slow": f"Unwind is hanging on too long{direction_text}; the car keeps steering after the plan starts releasing.",
"unwind_too_fast": f"Unwind is releasing too quickly{direction_text}; the wheel gives back steering sooner than the plan wants.",
"center_chatter": "The car is doing repeated micro-corrections on straights or very light highway arcs.",
"notchy_mid_curve": "Mid-curve tracking is correcting in steps instead of flowing through the same steering band cleanly.",
"low_speed_unwillingness": "At low speed the controller is slow to wake up even though the turn request is already there.",
"saturation_limited": "The controller is spending meaningful time at or near its steering authority ceiling.",
"model_limited": "The controller is largely matching the commanded path; this sample does not show a strong tuning mismatch.",
}
return mapping.get(bucket, "The controller is showing a repeatable mismatch against the requested path.")
def _likely_interpretation(summary: dict[str, Any], adjustment: dict[str, Any]) -> str:
bucket = summary["bucket"]
if adjustment["type"] == "friction_curve":
if bucket == "low_speed_unwillingness":
return "The near-center friction threshold is too high in the crawl-speed band, so small requests are being muted."
return "This looks more like a friction-threshold problem than a whole-tune problem; the controller is busy around center and needs a calmer deadzone slope."
if adjustment["type"] == "vehicle_knob":
symbol = adjustment["symbol"]
if "ff_gain_" in symbol:
return "This car has a directional nonlinear torque map, and the mismatch is concentrated on one side. Correct that side's feedforward layer before moving global authority."
if "low_speed_angle_assist_max_torque" in symbol:
return "The main torque path is waking up too late below about 8 mph, so the low-speed assist layer needs a little more authority."
if "crawl_turn_in_ff_boost" in symbol:
return "The crawl-speed turn-in band is still too lazy, even before the broader tune needs to move."
if "curvy_speed_max" in symbol:
return "The curvy unwind band is dropping out too early at higher speed, so the curve-specific release cleanup is not staying active long enough."
if "curvy_turn_in_trim_speed_max" in symbol:
return "The fast-curve entry trim band is fading out too early, so the controller is falling back to the base path before the curve is done."
if "curvy_turn_in_trim" in symbol:
return "This is a curve-band entry problem, not a whole-car turn-in problem; the mid-speed trim needs to move without touching the rest of the tune."
if "curvy_unwind" in symbol:
return "This is a curve-band release problem, not a global unwind problem; the mid-speed unwind cleanup needs to move on its own."
if bucket in ("understeer", "late_turn_in", "low_speed_unwillingness", "saturation_limited"):
return "Primary turn-in authority is too low for the way this car is reacting in that band."
if bucket in ("oversteer", "early_turn_in"):
return "Entry authority is too aggressive for the speed band being hit here."
if bucket == "unwind_too_slow":
return "Exit steering is being held too long after the plan starts backing out of the curve."
if bucket == "unwind_too_fast":
return "Exit steering is tapering away too quickly once unwind starts."
return "The mismatch is consistent enough to justify a direct tuning pass."
def _why_this_knob(adjustment: dict[str, Any]) -> str:
if adjustment["type"] == "friction_curve":
return "This changes the threshold that maps small lateral-accel error into friction compensation without pretending the whole torque slope is wrong."
if adjustment["type"] == "vehicle_knob":
symbol = adjustment["symbol"]
if "ff_gain_" in symbol:
return "This compensates the affected side without flattening the car's separate left/right nonlinear torque response into one global value."
if "low_speed_angle_assist_max_torque" in symbol:
return "This directly raises the crawl-speed assist ceiling that fills the gap before the normal torque path wakes up."
if "crawl_turn_in_ff_boost" in symbol:
return "This only touches the crawl-speed turn-in band instead of disturbing normal-speed behavior."
if "curvy_speed_max" in symbol:
return "This keeps the dedicated curvy unwind helper alive deeper into faster curves instead of globally changing the whole unwind map."
if "curvy_turn_in_trim_speed_max" in symbol:
return "This extends the fast-curve trim band instead of making the whole car more eager to turn everywhere."
if "curvy_turn_in_trim" in symbol:
return "This trims entry only in the dedicated curvy speed band instead of flattening turn-in everywhere."
if "curvy_unwind" in symbol:
return "This cleans up release only in the dedicated curvy speed band instead of changing global unwind behavior."
if "turn_in_boost" in symbol:
return "This targets entry behavior directly instead of disturbing the whole tune."
if "unwind_taper" in symbol:
return "This targets release behavior directly instead of flattening the whole response."
if "threshold" in symbol:
return "This adjusts the transition deadzone for the specific phase that is misbehaving."
return "This is the closest car-specific knob to the symptom being shown."
return "This is the smallest generic user-facing change that moves the car in the right direction without inventing a new code path."
def _render_adjustment_line(adjustment: dict[str, Any]) -> str:
if adjustment["type"] == "friction_curve":
curve = ", ".join(f"{value:.3f}" for value in adjustment["suggested"])
return f"Adjust {adjustment['family']} friction threshold curve at {FLM_FRICTION_SPEED_KNOTS} m/s to [{curve}]."
if adjustment["type"] == "vehicle_knob":
return f"Move `{adjustment['symbol']}` from {adjustment['current']:.3f} to {adjustment['suggested']:.3f}."
return f"Move `{adjustment['paramKey']}` from {adjustment['current']:.3f} to {adjustment['suggested']:.3f}."
def _what_not_to_touch_yet(summary: dict[str, Any], adjustment: dict[str, Any] | None, strategy: str) -> str:
if strategy == "baseline":
if adjustment and adjustment.get("type") in ("generic_param", "friction_curve"):
return "Do not jump straight into phase-specific cleanup knobs yet. Get the broad authority and friction behavior into the right zip code first."
return "Do not start layering narrow cleanup knobs onto a car that is still broadly wrong."
if adjustment and adjustment.get("type") == "generic_param":
return "Do not widen this into a whole-car ratio or delay change first. This symptom can usually be cleaned up without global geometry edits."
return "Do not change unrelated center-taper or steer-ratio behavior first. This symptom has a narrower cause than that."
def _if_that_was_wrong(summary: dict[str, Any], adjustment: dict[str, Any], strategy: str) -> str:
if strategy == "baseline":
return f"If this gets the car broadly closer but leaves one specific phase ugly, stop here and switch to Cleanup Pass for that band. {_why_this_knob(adjustment)}"
return f"If this cleans up the main symptom but introduces the opposite behavior, keep half the change and move to the next phase-specific knob. {_why_this_knob(adjustment)}"
def build_suggestions(summaries: list[dict[str, Any]], capabilities: dict[str, Any], current: dict[str, Any],
strategy: str = "cleanup") -> list[dict[str, Any]]:
suggestions = []
for summary in summaries:
adjustment = _primary_delta_from_summary(summary, capabilities, current, strategy=strategy)
evidence = summary.get("evidence", {})
if adjustment is None:
suggestions.append({
"dimensionId": summary["dimensionId"],
"bucket": summary["bucket"],
"severity": float(summary.get("severity", 0.0)),
"evidence": evidence,
"currentVsSuggested": None,
"observedBehavior": _observed_behavior(summary),
"likelyInterpretation": _likely_interpretation(summary, {"type": "generic_param", "paramKey": "none"}),
"primaryAdjustment": "Do not change the tune yet.",
"whatNotToTouchYet": "Do not start cutting or adding turn-in. This sample does not show a clean controller-side miss.",
"ifThatWasWrong": "If a stronger sample later shows actual lateral accel lagging or overshooting the plan, revisit with that route.",
"strategy": strategy,
"plotSvg": summary.get("plotSvg", ""),
"plotData": summary.get("plotData", {}),
})
continue
if adjustment["type"] == "friction_curve":
current_vs_suggested = {
"type": "friction_curve",
"family": adjustment["family"],
"current": adjustment["current"],
"suggested": adjustment["suggested"],
}
elif adjustment["type"] == "vehicle_knob":
current_vs_suggested = {
"type": "vehicle_knob",
"symbol": adjustment["symbol"],
"current": adjustment["current"],
"suggested": adjustment["suggested"],
}
else:
current_vs_suggested = {
"type": "generic_param",
"paramKey": adjustment["paramKey"],
"current": adjustment["current"],
"suggested": adjustment["suggested"],
}
suggestions.append({
"dimensionId": summary["dimensionId"],
"bucket": summary["bucket"],
"severity": float(summary.get("severity", 0.0)),
"evidence": evidence,
"currentVsSuggested": current_vs_suggested,
"primaryAdjustmentRaw": adjustment,
"strategy": strategy,
"observedBehavior": _observed_behavior(summary),
"likelyInterpretation": _likely_interpretation(summary, adjustment),
"primaryAdjustment": _render_adjustment_line(adjustment),
"whatNotToTouchYet": _what_not_to_touch_yet(summary, adjustment, strategy),
"ifThatWasWrong": _if_that_was_wrong(summary, adjustment, strategy),
"driverFeel": _observed_behavior(summary),
"logSupport": f"Matched in {evidence.get('eventCount', 0)} event(s); strongest samples: {', '.join(item['label'] for item in evidence.get('segments', [])[:3]) or 'none'}",
"whyThisKnob": _why_this_knob(adjustment),
"plotSvg": summary.get("plotSvg", ""),
"plotData": summary.get("plotData", {}),
})
return suggestions
def _clamp_generic_param(param_key: str, value: float) -> float:
meta = GENERIC_PARAM_METADATA[param_key]
return _round_to_precision(_clamp(value, meta["min"], meta["max"]), meta["precision"])
def _merge_primary_adjustments(suggestions: list[dict[str, Any]], multiplier: float) -> tuple[dict[str, Any], dict[str, Any], bool]:
params_delta: dict[str, Any] = {"AdvancedLateralTune": True}
requires_force_auto_tune_off = False
generic_targets: dict[str, dict[str, Any]] = {}
vehicle_targets: dict[str, dict[str, Any]] = {}
friction_targets: dict[str, dict[str, Any]] = {}
for suggestion in suggestions:
adjustment = suggestion.get("primaryAdjustmentRaw")
if not isinstance(adjustment, dict):
continue
weight = max(float(suggestion.get("severity", 0.0)), 0.25)
if adjustment["type"] == "generic_param":
param_key = adjustment["paramKey"]
bucket = generic_targets.setdefault(param_key, {
"current": float(adjustment["current"]),
"weightedDelta": 0.0,
"weight": 0.0,
})
bucket["weightedDelta"] += float(adjustment["delta"]) * weight
bucket["weight"] += weight
if param_key in ("SteerFriction", "SteerLatAccel", "SteerKP", "SteerDelay", "SteerRatio"):
requires_force_auto_tune_off = True
elif adjustment["type"] == "vehicle_knob":
symbol = adjustment["symbol"]
bucket = vehicle_targets.setdefault(symbol, {
"current": float(adjustment["current"]),
"weightedDelta": 0.0,
"weight": 0.0,
})
bucket["weightedDelta"] += float(adjustment["delta"]) * weight
bucket["weight"] += weight
requires_force_auto_tune_off = True
elif adjustment["type"] == "friction_curve":
family = adjustment["family"]
delta_curve = [float(value) for value in adjustment["delta"]]
bucket = friction_targets.setdefault(family, {
"current": [float(value) for value in adjustment["current"]],
"weightedDelta": [0.0] * len(delta_curve),
"weight": 0.0,
})
for idx, value in enumerate(delta_curve):
bucket["weightedDelta"][idx] += value * weight
bucket["weight"] += weight
requires_force_auto_tune_off = True
overrides: dict[str, Any] = {"schemaVersion": 1, "baseFrictionThresholds": {}, "vehicleKnobs": {}}
for param_key, bucket in generic_targets.items():
avg_delta = (bucket["weightedDelta"] / bucket["weight"]) * multiplier if bucket["weight"] > 0 else 0.0
next_value = _clamp_generic_param(param_key, float(bucket["current"]) + avg_delta)
precision = float(GENERIC_PARAM_METADATA[param_key]["precision"])
if not math.isclose(float(bucket["current"]), next_value, abs_tol=max(precision / 2.0, 1e-6)):
params_delta[param_key] = next_value
if "SteerDelay" in params_delta:
params_delta["UseAutoSteerDelay"] = False
supported_knobs = get_flm_supported_vehicle_knobs()
for symbol, bucket in vehicle_targets.items():
meta = supported_knobs.get(symbol)
if meta is None or bucket["weight"] <= 0:
continue
avg_delta = (bucket["weightedDelta"] / bucket["weight"]) * multiplier
next_value = _round_to_precision(_clamp(float(bucket["current"]) + avg_delta, meta["min"], meta["max"]), meta["precision"])
if not math.isclose(float(bucket["current"]), next_value, abs_tol=max(float(meta["precision"]) / 2.0, 1e-6)):
overrides["vehicleKnobs"][symbol] = next_value
for family, bucket in friction_targets.items():
if bucket["weight"] <= 0:
continue
avg_delta_curve = [value / bucket["weight"] for value in bucket["weightedDelta"]]
values = [
round(max(0.05, float(bucket["current"][idx]) + (avg_delta_curve[idx] * multiplier)), 4)
for idx in range(len(bucket["current"]))
]
if any(not math.isclose(float(bucket["current"][idx]), values[idx], abs_tol=1e-6) for idx in range(len(values))):
overrides["baseFrictionThresholds"][family] = {"speedKnots": list(FLM_FRICTION_SPEED_KNOTS), "values": values}
overrides = normalize_flm_overrides(overrides)
return params_delta, overrides, requires_force_auto_tune_off
def _resolve_conflicting_actionable_suggestions(suggestions: list[dict[str, Any]]) -> list[dict[str, Any]]:
families = {
"understeer": ("turn_in", "more"),
"late_turn_in": ("turn_in", "more"),
"oversteer": ("turn_in", "less"),
"early_turn_in": ("turn_in", "less"),
"unwind_too_slow": ("unwind", "more"),
"unwind_too_fast": ("unwind", "less"),
}
grouped: dict[tuple[str, str, str], dict[str, list[dict[str, Any]]]] = {}
passthrough: list[dict[str, Any]] = []
for suggestion in suggestions:
bucket = str(suggestion.get("bucket", ""))
family_info = families.get(bucket)
if family_info is None:
passthrough.append(suggestion)
continue
evidence = suggestion.get("evidence", {})
key = (
family_info[0],
str(evidence.get("directionBias", "center")),
str(evidence.get("speedBand", "mixed")),
)
grouped.setdefault(key, {"more": [], "less": []})[family_info[1]].append(suggestion)
resolved = list(passthrough)
for polarities in grouped.values():
more = polarities["more"]
less = polarities["less"]
if not more or not less:
resolved.extend(more or less)
continue
def score(items: list[dict[str, Any]]) -> float:
total = 0.0
for item in items:
severity = max(float(item.get("severity", 0.0)), 0.25)
event_count = max(int(item.get("evidence", {}).get("eventCount", 0)), 1)
total += severity * math.log1p(event_count)
return total
more_score = score(more)
less_score = score(less)
if more_score >= less_score * 1.2:
resolved.extend(more)
elif less_score >= more_score * 1.2:
resolved.extend(less)
return sorted(resolved, key=lambda item: float(item.get("severity", 0.0)), reverse=True)
def _bucket_tuning_family(bucket: str) -> str:
if bucket in ("understeer", "late_turn_in", "oversteer", "early_turn_in", "saturation_limited", "low_speed_unwillingness"):
return "authority"
if bucket in ("unwind_too_slow", "unwind_too_fast"):
return "release"
if bucket in ("center_chatter", "notchy_mid_curve"):
return "stability"
return "other"
def select_primary_tuning_path(summaries: list[dict[str, Any]], summary_stats: dict[str, Any]) -> dict[str, Any]:
actionable = [
summary for summary in summaries
if summary.get("bucket") not in ("model_limited", "angle_control_diagnostic")
and float(summary.get("severity", 0.0)) >= 0.4
]
if not actionable:
return {
"primaryPathKey": "cleanup_pass",
"alternatePathKey": "baseline_fix",
"reason": "This sample does not show a broad controller-side miss. Start with the narrower cleanup path if you test anything.",
"baselineScore": 0,
}
mean_error = float(summary_stats.get("meanErrorAbs", 0.0) or 0.0)
families = {_bucket_tuning_family(str(summary.get("bucket", ""))) for summary in actionable}
major_events = [summary for summary in actionable if float(summary.get("severity", 0.0)) >= 0.8]
severe_global = [
summary for summary in actionable
if summary.get("bucket") in ("understeer", "oversteer", "late_turn_in", "early_turn_in", "saturation_limited")
and float(summary.get("severity", 0.0)) >= 0.85
]
severe_global_bands = {
(str(summary.get("direction", "center")), str(summary.get("speedBand", "mixed")))
for summary in severe_global
}
severe_global_segments = {
str(segment.get("label", ""))
for summary in severe_global
for segment in summary.get("evidence", {}).get("segments", [])
if segment.get("label")
}
severe_saturation = any(
summary.get("bucket") == "saturation_limited" and float(summary.get("severity", 0.0)) >= 0.85
for summary in actionable
)
if mean_error < 0.08 and not severe_saturation and not (
len(severe_global_bands) >= 2 and len(severe_global_segments) >= 2
):
return {
"primaryPathKey": "cleanup_pass",
"alternatePathKey": "baseline_fix",
"reason": "Overall lateral-accel tracking is already strong. The remaining misses are isolated enough that changing the base tune would disturb more good behavior than it fixes.",
"baselineScore": 0,
}
baseline_score = 0
if mean_error >= 0.14:
baseline_score += 2
elif mean_error >= 0.11:
baseline_score += 1
if len(actionable) >= 4:
baseline_score += 1
if len(families - {"other"}) >= 3:
baseline_score += 1
if len(major_events) >= 2:
baseline_score += 1
if severe_global:
baseline_score += 1
if baseline_score >= 3:
return {
"primaryPathKey": "baseline_fix",
"alternatePathKey": "cleanup_pass",
"reason": "This route looks broadly wrong across enough bands that the right first move is to fix base authority and friction behavior before touching narrower cleanup layers.",
"baselineScore": baseline_score,
}
return {
"primaryPathKey": "cleanup_pass",
"alternatePathKey": "baseline_fix",
"reason": "This route is already close enough overall that the better first move is a narrow cleanup pass instead of a broad whole-car reset.",
"baselineScore": baseline_score,
}
def build_trial_profiles(report_id: str, suggestions: list[dict[str, Any]], feedback: dict[str, Any], capabilities: dict[str, Any],
path_key: str = "cleanup_pass", path_label: str = "Cleanup Pass") -> list[dict[str, Any]]:
ignored = set(str(item) for item in feedback.get("ignoredDimensions", []))
accepted = set(str(item) for item in feedback.get("acceptedDimensions", []))
has_feedback_decisions = bool(ignored or accepted)
considered = [
suggestion for suggestion in suggestions
if suggestion.get("dimensionId") not in ignored and (
not accepted or suggestion.get("dimensionId") in accepted
)
]
if not considered and not has_feedback_decisions:
considered = [suggestion for suggestion in suggestions if suggestion.get("primaryAdjustmentRaw")]
actionable = [
suggestion for suggestion in considered
if suggestion.get("primaryAdjustmentRaw")
]
actionable = _resolve_conflicting_actionable_suggestions(actionable)
profiles = []
profile_defs = [
("conservative", "Conservative", 0.6),
("recommended", "Recommended", 1.0),
("assertive", "Assertive", 1.35),
]
for suffix, label, multiplier in profile_defs:
params_delta, overrides, force_auto_tune_off = _merge_primary_adjustments(actionable, multiplier)
if not overrides and len(params_delta) <= 1:
continue
profile = {
"id": f"{report_id}:{path_key}:{suffix}",
"reportId": report_id,
"label": label,
"pathKey": path_key,
"pathLabel": path_label,
"description": f"{label} {path_label.lower()} trial generated from {len(actionable)} confirmed symptom dimension(s).",
"genericParams": params_delta,
"flmOverrides": overrides,
"requiresForceAutoTuneOff": bool(force_auto_tune_off),
"capabilities": capabilities,
}
if force_auto_tune_off:
profile["genericParams"]["ForceAutoTuneOff"] = True
profile["genericParams"]["ForceAutoTune"] = False
profiles.append(profile)
return profiles[:3]
def _add_parameters_start_here(capabilities: dict[str, Any], suggestions: list[dict[str, Any]], primary_path_key: str) -> list[str]:
lines = ["Turn on Advanced Lateral Tune before trying any suggested profile."]
if primary_path_key == "baseline_fix":
lines.append("This route looks broadly wrong enough that the first move should be a baseline fix, not a surgical cleanup pass.")
lines.append("Start with the broad knobs this report suggests. Once the car is in the right zip code, re-run analysis and switch to Cleanup Pass for the leftovers.")
else:
lines.append("This route is already mostly in the right zip code, so start with cleanup changes before reaching for broader whole-car adjustments.")
if any(suggestion.get("primaryAdjustmentRaw", {}).get("type") == "generic_param" for suggestion in suggestions if suggestion.get("primaryAdjustmentRaw")):
lines.append("Generic advanced lateral params are in play on this pass, so apply those first before deciding you need deeper code-level changes.")
if any(suggestion.get("primaryAdjustmentRaw", {}).get("type") == "friction_curve" for suggestion in suggestions if suggestion.get("primaryAdjustmentRaw")):
lines.append("Friction-threshold changes are active in this pass because the logs point to small-signal steering behavior, not just whole-tune authority.")
if not capabilities.get("richProfileKey"):
lines.append("This car does not expose richer live FLM knobs yet, so generic advanced params and friction-threshold trials are the first code-level moves to test.")
return lines
def build_recommendation_paths(report_id: str, summaries: list[dict[str, Any]], summary_stats: dict[str, Any],
capabilities: dict[str, Any], current: dict[str, Any],
feedback: dict[str, Any]) -> tuple[list[dict[str, Any]], dict[str, Any]]:
decision = select_primary_tuning_path(summaries, summary_stats)
all_suggestions = {
"baseline_fix": build_suggestions(summaries, capabilities, current, strategy="baseline"),
"cleanup_pass": build_suggestions(summaries, capabilities, current, strategy="cleanup"),
}
ordered_keys = [decision["primaryPathKey"], decision["alternatePathKey"]]
paths = []
for path_key in ordered_keys:
spec = FLM_PATH_SPECS[path_key]
suggestions = all_suggestions[path_key]
profiles = build_trial_profiles(report_id, suggestions, feedback, capabilities, path_key=path_key, path_label=spec["title"])
paths.append({
"key": path_key,
"title": spec["title"],
"description": spec["description"],
"whenToUse": spec["whenToUse"],
"alternateHint": spec["alternateHint"],
"isPrimary": path_key == decision["primaryPathKey"],
"whySelected": decision["reason"] if path_key == decision["primaryPathKey"] else spec["alternateHint"],
"suggestions": suggestions,
"profiles": profiles,
})
return paths, decision
def _render_report_html(report: dict[str, Any]) -> str:
report_paths = [path for path in report.get("paths", []) if isinstance(path, dict)]
selected_path_key = str(report.get("selectedPathKey") or report.get("primaryPathKey") or "")
primary_path = next((path for path in report_paths if path.get("key") == selected_path_key), None)
if primary_path is None:
primary_path = next((path for path in report_paths if path.get("isPrimary")), report_paths[0] if report_paths else {})
findings_html = []
for suggestion in report.get("suggestions", []):
evidence = suggestion.get("evidence", {})
current_vs_suggested = suggestion.get("currentVsSuggested")
if current_vs_suggested is None:
delta_html = "<p class='flm-muted'>No trial adjustment suggested for this dimension.</p>"
elif current_vs_suggested["type"] == "friction_curve":
delta_html = (
f"<p><strong>Current:</strong> {current_vs_suggested['current']}</p>"
f"<p><strong>Suggested:</strong> {current_vs_suggested['suggested']}</p>"
)
else:
label = current_vs_suggested.get("paramKey") or current_vs_suggested.get("symbol")
delta_html = (
f"<p><strong>{label}</strong>: {float(current_vs_suggested['current']):.3f} -> {float(current_vs_suggested['suggested']):.3f}</p>"
)
findings_html.append(
"<section class='flm-card'>"
f"<h3>{suggestion['bucket'].replace('_', ' ').title()}</h3>"
f"<p><strong>Observed behavior:</strong> {suggestion['observedBehavior']}</p>"
f"<p><strong>Likely interpretation:</strong> {suggestion['likelyInterpretation']}</p>"
f"<p><strong>Primary adjustment:</strong> {suggestion['primaryAdjustment']}</p>"
f"<p><strong>What not to touch yet:</strong> {suggestion['whatNotToTouchYet']}</p>"
f"<p><strong>If that was wrong, next thing to try:</strong> {suggestion['ifThatWasWrong']}</p>"
f"<p><strong>Evidence:</strong> speed={evidence.get('speedBand', 'mixed')}, direction={evidence.get('directionBias', 'center')}, events={evidence.get('eventCount', 0)}</p>"
f"<p><strong>Strongest segments:</strong> {', '.join(item['label'] for item in evidence.get('segments', [])[:3]) or 'none'}</p>"
f"{delta_html}"
f"{suggestion.get('plotSvg', '')}"
"</section>"
)
path_html = []
for path in report_paths:
badges = []
if path.get("isPrimary"):
badges.append("Analyzer recommended")
if path.get("key") == selected_path_key:
badges.append("Active")
badge = " / ".join(badges) or "Alternate"
path_html.append(
"<section class='flm-card'>"
f"<h3>{path.get('title', 'Path')}</h3>"
f"<p><strong>{badge}:</strong> {path.get('description', '')}</p>"
f"<p><strong>Why this path:</strong> {path.get('whySelected', '')}</p>"
f"<p><strong>When to use it:</strong> {path.get('whenToUse', '')}</p>"
"</section>"
)
profile_html = []
for path in report_paths:
path_profiles = path.get("profiles", [])
cards = []
for profile in path_profiles:
generic_lines = []
for key, value in profile.get("genericParams", {}).items():
if key == "AdvancedLateralTune":
continue
generic_lines.append(f"<li><code>{key}</code>: {value}</li>")
override_lines = []
for family, payload in profile.get("flmOverrides", {}).get("baseFrictionThresholds", {}).items():
override_lines.append(f"<li><code>{family}</code> curve: {payload.get('values', [])}</li>")
for key, value in profile.get("flmOverrides", {}).get("vehicleKnobs", {}).items():
override_lines.append(f"<li><code>{key}</code>: {value}</li>")
cards.append(
"<section class='flm-card'>"
f"<h3>{profile['label']}</h3>"
f"<p>{profile['description']}</p>"
f"<p><strong>Generic params:</strong></p><ul>{''.join(generic_lines) or '<li>None</li>'}</ul>"
f"<p><strong>FLM overrides:</strong></p><ul>{''.join(override_lines) or '<li>None</li>'}</ul>"
"</section>"
)
path_profiles_html = "".join(cards) or "<p class='flm-muted'>No trial profiles generated for this path.</p>"
profile_html.append(
f"<h3>{path.get('title', 'Path')} Profiles</h3>"
f"{path_profiles_html}"
)
start_here_lines = "".join(f"<li>{line}</li>" for line in report.get("addTheseParametersAndStartHere", []))
start_here_html = f"<section class='flm-card'><h3>Add These Parameters And Start Here</h3><ul>{start_here_lines}</ul></section>" if start_here_lines else ""
findings_block = "".join(findings_html) or "<p class='flm-muted'>No strong findings.</p>"
profiles_block = "".join(profile_html) or "<p class='flm-muted'>No trial profiles generated.</p>"
return (
"<!doctype html><html><head><meta charset='utf-8'>"
"<title>FLM Tuning Report</title>"
"<style>"
"body{font-family:system-ui,sans-serif;background:#020617;color:#e2e8f0;margin:0;padding:24px;}"
".flm-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;}"
".flm-card{background:#0f172a;border:1px solid #1e293b;border-radius:12px;padding:16px;margin-bottom:16px;}"
".flm-muted{color:#94a3b8;}"
"code{background:#111827;padding:2px 6px;border-radius:6px;}"
".flm-plot{width:100%;height:140px;margin-top:12px;}"
"</style></head><body>"
f"<h1>FLM Tuning Report</h1>"
f"<p>{report['car']['carFingerprint']} | {report['car'].get('gitBranch', '')} {report['car'].get('gitCommit', '')}</p>"
f"<div class='flm-grid'><section class='flm-card'><h3>Routes</h3><p>{', '.join(report.get('routeNames', []))}</p></section>"
f"<section class='flm-card'><h3>Control Path</h3><p>{report['car'].get('controlPath', 'unknown')}</p></section>"
f"<section class='flm-card'><h3>Friction Family</h3><p>{report['capabilities'].get('frictionFamily', 'standard')}</p></section>"
f"<section class='flm-card'><h3>Nonlinear Torque Map</h3><p>{'Asymmetric left/right siglin' if report['capabilities'].get('nonlinearTorqueMap', {}).get('asymmetric') else ('Symmetric siglin' if report['capabilities'].get('nonlinearTorqueMap') else 'Not detected')}</p></section></div>"
f"{''.join(path_html)}"
f"{start_here_html}"
f"<h2>Active Findings: {primary_path.get('title', 'Recommendations')}</h2>"
f"{findings_block}"
"<h2>Trial Profiles</h2>"
f"{profiles_block}"
"</body></html>"
)
def analyze_routes(route_names: list[str], footage_paths: list[str], feedback: dict[str, Any] | None = None, report_id: str | None = None) -> dict[str, Any]:
ensure_flm_workspace()
params = Params(return_defaults=True)
report_id = report_id or f"flm-{int(time.time())}"
feedback = feedback or {}
sources, warnings = resolve_route_sources(route_names, footage_paths)
if not sources:
raise RuntimeError("No local routes with qlogs or rlogs were found for the selected routes.")
all_samples: list[FLMSample] = []
car_params = None
init_data: dict[str, str] = {}
used_qlog = False
processed_segments = 0
for idx, source in enumerate(sources, start=1):
_write_flm_status({
"pid": os.getpid(),
"startedAt": time.time(),
"running": True,
"state": "analyzing",
"routes": route_names,
"progress": idx - 1,
"total": len(sources),
"currentSegment": source.segment,
})
segment_samples, segment_car_params, segment_init = _segment_samples(source)
if car_params is None and segment_car_params is not None:
car_params = segment_car_params
if segment_init and not init_data:
init_data = segment_init
if source.used_qlog:
used_qlog = True
all_samples.extend(segment_samples)
processed_segments += 1
if car_params is None:
raise RuntimeError("No carParams were found in the selected routes.")
torque_control = car_params.lateralTuning.which() == "torque"
hyundai_canfd = bool(getattr(car_params, "flags", 0) & HyundaiFlags.CANFD)
capabilities = get_flm_capabilities(
car_params.carFingerprint,
brand=str(getattr(car_params, "brand", "") or ""),
hyundai_canfd=hyundai_canfd,
torque_control=torque_control,
)
capabilities = dict(capabilities)
capabilities["nonlinearTorqueMap"] = _nonlinear_torque_map(car_params)
current_params = _current_param_state(car_params, params)
stock_params = _stock_param_state(car_params, capabilities)
if torque_control:
raw_summaries, summary_stats = classify_torque_samples(all_samples)
summaries = _resolve_conflicting_actionable_suggestions(raw_summaries)
paths_payload, path_decision = build_recommendation_paths(report_id, summaries, summary_stats, capabilities, current_params, feedback)
primary_path = next((path for path in paths_payload if path.get("isPrimary")), paths_payload[0] if paths_payload else {})
suggestions = list(primary_path.get("suggestions", []))
profiles = [profile for path in paths_payload for profile in path.get("profiles", [])]
else:
summary_stats = {"sampleCount": len(all_samples), "qlogFallback": used_qlog}
summaries = [{
"bucket": "angle_control_diagnostic",
"dimensionId": "angle_control_diagnostic:overall",
"direction": "center",
"speedBand": "mixed",
"count": 1,
"severity": 0.0,
"evidence": {"speedBand": "mixed", "directionBias": "center", "eventCount": 1, "segments": []},
"events": [],
"plotSvg": "",
"plotData": {},
}]
suggestions = [{
"dimensionId": "angle_control_diagnostic:overall",
"bucket": "angle_control_diagnostic",
"evidence": summaries[0]["evidence"],
"currentVsSuggested": None,
"observedBehavior": "This route is using an angle-control path, so the torque-specific FLM trial system stays in diagnostic mode.",
"likelyInterpretation": "You can still inspect lane behavior here, but torque-controller trial profiles do not apply.",
"primaryAdjustment": "Do not apply an FLM torque profile to this car.",
"whatNotToTouchYet": "Do not write torque-controller override blobs for an angle-control path.",
"ifThatWasWrong": "If the car later moves to torque control, re-run FLM on a fresh route.",
"plotSvg": "",
"plotData": {},
}]
path_decision = {
"primaryPathKey": "cleanup_pass",
"alternatePathKey": "baseline_fix",
"reason": "Angle-control diagnostic mode does not participate in the torque trial workflow.",
"baselineScore": 0,
}
paths_payload = [{
"key": "cleanup_pass",
"title": "Diagnostic Only",
"description": "This route is using an angle-control path, so torque-controller trial profiles do not apply.",
"whenToUse": "Use this report only for diagnostic review.",
"alternateHint": "",
"isPrimary": True,
"whySelected": path_decision["reason"],
"suggestions": suggestions,
"profiles": [],
}]
profiles = []
report = {
"reportId": report_id,
"createdAt": time.time(),
"routeNames": route_names,
"warnings": warnings,
"feedback": feedback,
"car": {
"carFingerprint": str(car_params.carFingerprint),
"brand": str(getattr(car_params, "brand", "") or ""),
"controlPath": "torque" if torque_control else "angle",
"gitBranch": init_data.get("gitBranch", ""),
"gitCommit": init_data.get("gitCommit", ""),
"steerControlType": str(getattr(car_params, "steerControlType", car.CarParams.SteerControlType.torque)),
},
"capabilities": capabilities,
"stockParams": stock_params,
"currentParams": current_params,
"summary": {
**summary_stats,
"processedSegments": processed_segments,
"usedQlogFallback": used_qlog,
},
"primaryPathKey": path_decision["primaryPathKey"],
"selectedPathKey": path_decision["primaryPathKey"],
"pathSelectionSource": "auto",
"pathDecision": path_decision,
"paths": paths_payload,
"findings": summaries,
"rawFindings": raw_summaries if torque_control else summaries,
"suggestions": suggestions,
"profiles": profiles,
"addTheseParametersAndStartHere": _add_parameters_start_here(capabilities, suggestions, path_decision["primaryPathKey"]),
}
paths = ensure_flm_workspace()
html = _render_report_html(report)
report["htmlPath"] = str(paths["reports"] / f"{report_id}.html")
report["jsonPath"] = str(paths["reports"] / f"{report_id}.json")
(paths["reports"] / f"{report_id}.html").write_text(html, encoding="utf-8")
_write_json(paths["reports"] / f"{report_id}.json", report)
_write_json(paths["profiles"] / f"{report_id}.json", profiles)
_write_flm_status({
"pid": os.getpid(),
"startedAt": time.time(),
"running": False,
"state": "complete",
"routes": route_names,
"progress": processed_segments,
"total": processed_segments,
"reportId": report_id,
})
return report
def load_report(report_id: str) -> dict[str, Any]:
paths = ensure_flm_workspace()
report_path = paths["reports"] / f"{report_id}.json"
report = _read_json(report_path, {})
if not isinstance(report, dict) or not report:
raise FileNotFoundError(report_id)
html_path = paths["reports"] / f"{report_id}.html"
report["html"] = html_path.read_text(encoding="utf-8") if html_path.exists() else ""
return report
def select_report_path(report_id: str, path_key: str) -> dict[str, Any]:
paths = ensure_flm_workspace()
report = load_report(report_id)
report_paths = [path for path in report.get("paths", []) if isinstance(path, dict)]
selected_path = next((path for path in report_paths if path.get("key") == path_key), None)
if selected_path is None:
raise ValueError(f"Unknown FLM path: {path_key}")
report["selectedPathKey"] = path_key
report["pathSelectionSource"] = "manual"
report["suggestions"] = list(selected_path.get("suggestions", []))
report["addTheseParametersAndStartHere"] = _add_parameters_start_here(
report.get("capabilities", {}),
report["suggestions"],
path_key,
)
report.pop("html", None)
(paths["reports"] / f"{report_id}.html").write_text(_render_report_html(report), encoding="utf-8")
_write_json(paths["reports"] / f"{report_id}.json", report)
return {
"message": f"Using {selected_path.get('title', path_key)} for this report.",
"report": load_report(report_id),
}
def _active_trial_display_state(paths: dict[str, Path], snapshot: Any) -> dict[str, Any] | None:
if not isinstance(snapshot, dict) or not snapshot:
return None
if "appliedGenericParams" in snapshot:
return snapshot
report_id = str(snapshot.get("reportId", "") or "")
profile_id = str(snapshot.get("profileId", "") or "")
profiles = _read_json(paths["profiles"] / f"{report_id}.json", []) if report_id else []
profile = next((
item for item in profiles
if isinstance(item, dict) and item.get("id") == profile_id
), None) if isinstance(profiles, list) else None
if profile is None:
return snapshot
generic_params = dict(profile.get("genericParams", {}))
flm_overrides = normalize_flm_overrides(profile.get("flmOverrides", {}))
return {
**snapshot,
"profileLabel": str(profile.get("label", "FLM") or "FLM"),
"pathKey": str(profile.get("pathKey", "") or ""),
"pathLabel": str(profile.get("pathLabel", "") or ""),
"appliedGenericParams": {
key: value for key, value in generic_params.items()
if key in FLM_ADVANCED_LATERAL_PARAM_KEYS
},
"appliedFrictionThresholds": flm_overrides.get("baseFrictionThresholds", {}),
"appliedVehicleKnobs": flm_overrides.get("vehicleKnobs", {}),
}
def list_workspace() -> dict[str, Any]:
paths = ensure_flm_workspace()
reports = []
for path in sorted(paths["reports"].glob("*.json"), reverse=True):
payload = _read_json(path, {})
if not isinstance(payload, dict) or not payload:
continue
reports.append({
"reportId": payload.get("reportId", path.stem),
"createdAt": payload.get("createdAt", path.stat().st_mtime),
"carFingerprint": payload.get("car", {}).get("carFingerprint", ""),
"routeNames": payload.get("routeNames", []),
"controlPath": payload.get("car", {}).get("controlPath", ""),
})
feedback_files = sorted(paths["feedback"].glob("*.json"), reverse=True)
params = Params(return_defaults=True)
current_profile_id = params.get("FLMActiveProfileId", encoding="utf-8") or ""
raw_active_snapshot = _read_json(paths["snapshots"] / "active.json", {})
if params.get_bool("FLMTrialApplied"):
active_payload = raw_active_snapshot if isinstance(raw_active_snapshot, dict) else {}
baseline_snapshot = _find_revert_snapshot(paths, raw_active_snapshot, current_profile_id, params)
if baseline_snapshot is not None:
raw_active_snapshot = {
**active_payload,
"params": baseline_snapshot["params"],
"profileId": current_profile_id or active_payload.get("profileId", ""),
"recoveryNeeded": baseline_snapshot is not raw_active_snapshot,
"rollbackAvailable": True,
}
else:
raw_active_snapshot = {
**active_payload,
"profileId": current_profile_id or active_payload.get("profileId", ""),
"recoveryNeeded": True,
"rollbackAvailable": False,
}
active_snapshot = _active_trial_display_state(paths, raw_active_snapshot)
return {
"reports": reports[:20],
"feedbackCount": len(feedback_files),
"activeTrial": active_snapshot,
"status": read_flm_status(),
}
def delete_report(report_id: str) -> dict[str, Any]:
paths = ensure_flm_workspace()
params = Params(return_defaults=True)
if params.get_bool("FLMTrialApplied"):
raise RuntimeError("Revert or keep the active FLM trial before deleting tuning reports.")
active_snapshot = _read_json(paths["snapshots"] / "active.json", {})
if isinstance(active_snapshot, dict) and active_snapshot.get("reportId") == report_id:
raise RuntimeError("Revert the active FLM trial before deleting its source report.")
removed = []
direct_paths = [
paths["reports"] / f"{report_id}.json",
paths["reports"] / f"{report_id}.html",
paths["profiles"] / f"{report_id}.json",
paths["feedback"] / f"{report_id}.json",
]
for path in direct_paths:
if path.exists():
path.unlink()
removed.append(str(path))
for path in paths["snapshots"].glob(f"{report_id}-*.json"):
path.unlink()
removed.append(str(path))
if not removed:
raise FileNotFoundError(report_id)
status = read_flm_status()
if not status.get("running") and status.get("reportId") == report_id:
_clear_flm_status()
return {
"message": f"Deleted tuning report {report_id}.",
"removed": removed,
"workspace": list_workspace(),
}
def clear_workspace() -> dict[str, Any]:
paths = ensure_flm_workspace()
status = read_flm_status()
if status.get("running"):
raise RuntimeError("Stop the active FLM analysis before clearing the workspace.")
params = Params(return_defaults=True)
active_snapshot = _read_json(paths["snapshots"] / "active.json", {})
if params.get_bool("FLMTrialApplied") or (isinstance(active_snapshot, dict) and active_snapshot.get("params")):
raise RuntimeError("Revert or keep the active FLM trial before clearing the workspace.")
removed = []
for key in ("reports", "profiles", "feedback", "snapshots"):
for path in paths[key].glob("*"):
if path.is_file():
path.unlink()
removed.append(str(path))
_clear_persistent_trial_baseline(params)
_clear_flm_status()
return {
"message": "Cleared saved tuning reports, feedback, profiles, and snapshots.",
"removedCount": len(removed),
"workspace": list_workspace(),
}
def _snapshot_current_trial_state(params: Params) -> dict[str, Any]:
snapshot = {}
for key, kind in TRIAL_PARAM_SPECS.items():
if kind == "bool":
snapshot[key] = params.get_bool(key)
elif kind == "float":
snapshot[key] = params.get_float(key, return_default=True)
elif kind == "json":
snapshot[key] = normalize_flm_overrides(params.get(key, encoding="utf-8") or "{}")
else:
snapshot[key] = params.get(key, encoding="utf-8") or ""
return snapshot
def _read_persistent_trial_baseline(params: Params) -> dict[str, Any] | None:
raw = params.get(FLM_TRIAL_BASELINE_PARAM, encoding="utf-8") or {}
if isinstance(raw, str):
try:
raw = json.loads(raw)
except (TypeError, ValueError):
return None
if not isinstance(raw, dict) or not isinstance(raw.get("params"), dict):
return None
if raw["params"].get("FLMTrialApplied", False):
return None
return raw
def _persist_trial_baseline(params: Params, snapshot: dict[str, Any]) -> None:
if isinstance(snapshot.get("params"), dict) and not snapshot["params"].get("FLMTrialApplied", False):
params.put(FLM_TRIAL_BASELINE_PARAM, snapshot)
def _clear_persistent_trial_baseline(params: Params) -> None:
params.remove(FLM_TRIAL_BASELINE_PARAM)
def _profile_report_id(profile_id: str) -> str:
return str(profile_id or "").split(":", 1)[0]
def _recover_report_baseline(paths: dict[str, Path], profile_id: str,
visited_profiles: set[str] | None = None) -> dict[str, Any] | None:
profile_id = str(profile_id or "")
if not profile_id:
return None
visited_profiles = set(visited_profiles or set())
if profile_id in visited_profiles:
return None
visited_profiles.add(profile_id)
report_id = _profile_report_id(profile_id)
report = _read_json(paths["reports"] / f"{report_id}.json", {})
report_params = report.get("currentParams") if isinstance(report, dict) else None
if not isinstance(report_params, dict) or not report_params:
return None
baseline_params = {
key: value for key, value in report_params.items()
if key in TRIAL_PARAM_SPECS
}
if not baseline_params:
return None
if not baseline_params.get("FLMTrialApplied", False):
baseline_params["FLMTrialApplied"] = False
baseline_params.setdefault("FLMActiveProfileId", "")
return {
"reportId": report_id,
"profileId": profile_id,
"capturedAt": float(report.get("createdAt", 0.0) or 0.0),
"params": baseline_params,
"recoverySource": "report",
}
previous_profile_id = str(baseline_params.get("FLMActiveProfileId", "") or "")
if previous_profile_id and previous_profile_id != profile_id:
return _recover_report_baseline(paths, previous_profile_id, visited_profiles)
return None
def _apply_param_bundle(params: Params, bundle: dict[str, Any]) -> None:
for key, value in bundle.items():
kind = TRIAL_PARAM_SPECS.get(key)
if kind == "bool":
params.put_bool(key, bool(value))
elif kind == "float":
params.put_float(key, float(value))
elif kind == "json":
params.put(key, normalize_flm_overrides(value))
elif kind == "string":
params.put(key, str(value or ""))
def _merge_flm_override_state(base: dict[str, Any], delta: dict[str, Any]) -> dict[str, Any]:
base = normalize_flm_overrides(base)
delta = normalize_flm_overrides(delta)
merged = {
"schemaVersion": 1,
"baseFrictionThresholds": {
**base.get("baseFrictionThresholds", {}),
**delta.get("baseFrictionThresholds", {}),
},
"vehicleKnobs": {
**base.get("vehicleKnobs", {}),
**delta.get("vehicleKnobs", {}),
},
}
return normalize_flm_overrides(merged)
def _find_revert_snapshot(paths: dict[str, Path], active_snapshot: dict[str, Any],
current_profile_id: str = "", params: Params | None = None) -> dict[str, Any] | None:
if isinstance(active_snapshot, dict) and isinstance(active_snapshot.get("params"), dict):
if not active_snapshot["params"].get("FLMTrialApplied", False):
return active_snapshot
if params is not None:
persistent_baseline = _read_persistent_trial_baseline(params)
if persistent_baseline is not None:
return persistent_baseline
cutoff = float(active_snapshot.get("capturedAt", math.inf) or math.inf) if isinstance(active_snapshot, dict) else math.inf
candidates = []
for path in paths["snapshots"].glob("*.json"):
if path.name == "active.json":
continue
candidate = _read_json(path, {})
candidate_params = candidate.get("params", {}) if isinstance(candidate, dict) else {}
if not isinstance(candidate_params, dict) or candidate_params.get("FLMTrialApplied", False):
continue
captured_at = float(candidate.get("capturedAt", 0.0) or 0.0)
if captured_at > cutoff:
continue
candidates.append(candidate)
if candidates:
matching = [candidate for candidate in candidates if current_profile_id and candidate.get("profileId") == current_profile_id]
pool = matching or candidates
return max(pool, key=lambda candidate: float(candidate.get("capturedAt", 0.0) or 0.0))
return _recover_report_baseline(paths, current_profile_id)
def apply_trial_profile(report_id: str, profile_id: str) -> dict[str, Any]:
paths = ensure_flm_workspace()
params = Params(return_defaults=True)
profiles = _read_json(paths["profiles"] / f"{report_id}.json", [])
if not isinstance(profiles, list):
raise FileNotFoundError(profile_id)
profile = next((item for item in profiles if isinstance(item, dict) and item.get("id") == profile_id), None)
if profile is None:
raise FileNotFoundError(profile_id)
generic_params = dict(profile.get("genericParams", {}))
flm_overrides = normalize_flm_overrides(profile.get("flmOverrides", {}))
current_state = _snapshot_current_trial_state(params)
raw_active_snapshot = _read_json(paths["snapshots"] / "active.json", {})
previous_display_state = _active_trial_display_state(paths, raw_active_snapshot) or {}
trial_already_active = bool(current_state.get("FLMTrialApplied", False))
if trial_already_active:
baseline_snapshot = _find_revert_snapshot(
paths,
raw_active_snapshot,
str(current_state.get("FLMActiveProfileId", "") or ""),
params,
)
if baseline_snapshot is None:
raise RuntimeError("The active FLM trial has no recoverable rollback baseline. Keep the current tune as the new baseline before applying another profile.")
baseline_params = baseline_snapshot["params"]
session_started_at = float(baseline_snapshot.get("sessionStartedAt", baseline_snapshot.get("capturedAt", time.time())) or time.time())
else:
baseline_params = current_state
session_started_at = time.time()
previous_generic_params = dict(previous_display_state.get("appliedGenericParams", {}))
for key in FLM_ADVANCED_LATERAL_PARAM_KEYS:
if key in current_state and current_state.get(key) != baseline_params.get(key):
previous_generic_params[key] = current_state[key]
baseline_overrides = normalize_flm_overrides(baseline_params.get("FLMActiveOverrides", {}))
current_overrides = normalize_flm_overrides(current_state.get("FLMActiveOverrides", {}))
previous_friction_thresholds = dict(previous_display_state.get("appliedFrictionThresholds", {}))
for family, payload in current_overrides.get("baseFrictionThresholds", {}).items():
if payload != baseline_overrides.get("baseFrictionThresholds", {}).get(family):
previous_friction_thresholds[family] = payload
previous_vehicle_knobs = dict(previous_display_state.get("appliedVehicleKnobs", {}))
for symbol, value in current_overrides.get("vehicleKnobs", {}).items():
if value != baseline_overrides.get("vehicleKnobs", {}).get(symbol):
previous_vehicle_knobs[symbol] = value
applied_generic_params = {
**previous_generic_params,
**{
key: value for key, value in generic_params.items()
if key in FLM_ADVANCED_LATERAL_PARAM_KEYS
},
}
applied_friction_thresholds = {
**previous_friction_thresholds,
**flm_overrides.get("baseFrictionThresholds", {}),
}
applied_vehicle_knobs = {
**previous_vehicle_knobs,
**flm_overrides.get("vehicleKnobs", {}),
}
now = time.time()
snapshot = {
"reportId": report_id,
"profileId": profile_id,
"profileLabel": str(profile.get("label", "FLM") or "FLM"),
"pathKey": str(profile.get("pathKey", "") or ""),
"pathLabel": str(profile.get("pathLabel", "") or ""),
"capturedAt": session_started_at,
"updatedAt": now,
"sessionStartedAt": session_started_at,
"revisionCount": int(previous_display_state.get("revisionCount", 0) or 0) + 1,
"params": baseline_params,
"appliedGenericParams": applied_generic_params,
"appliedFrictionThresholds": applied_friction_thresholds,
"appliedVehicleKnobs": applied_vehicle_knobs,
}
_write_json(paths["snapshots"] / "active.json", snapshot)
_write_json(paths["snapshots"] / f"{report_id}-{profile_id.replace(':', '_')}.json", snapshot)
_persist_trial_baseline(params, snapshot)
bundle = generic_params
bundle["FLMActiveProfileId"] = profile_id
bundle["FLMActiveOverrides"] = _merge_flm_override_state(
current_state.get("FLMActiveOverrides", {}),
flm_overrides,
)
bundle["FLMTrialApplied"] = True
_apply_param_bundle(params, bundle)
return {
"message": f"Applied {profile.get('label', 'FLM')} profile.",
"profile": profile,
}
def revert_trial_profile() -> dict[str, Any]:
paths = ensure_flm_workspace()
snapshot_path = paths["snapshots"] / "active.json"
snapshot = _read_json(snapshot_path, {})
params = Params(return_defaults=True)
current_profile_id = params.get("FLMActiveProfileId", encoding="utf-8") or ""
revert_snapshot = _find_revert_snapshot(paths, snapshot if isinstance(snapshot, dict) else {}, current_profile_id, params)
if revert_snapshot is None:
raise FileNotFoundError("active trial snapshot")
_apply_param_bundle(params, revert_snapshot["params"])
_clear_persistent_trial_baseline(params)
try:
snapshot_path.unlink()
except FileNotFoundError:
pass
return {
"message": "Reverted the complete FLM trial session to its original baseline.",
"snapshot": {
**(snapshot if isinstance(snapshot, dict) else {}),
"params": revert_snapshot["params"],
"recoveredBaseline": revert_snapshot is not snapshot,
},
}
def accept_trial_as_baseline() -> dict[str, Any]:
paths = ensure_flm_workspace()
params = Params(return_defaults=True)
active_snapshot = _read_json(paths["snapshots"] / "active.json", {})
if not params.get_bool("FLMTrialApplied") and not (isinstance(active_snapshot, dict) and active_snapshot):
raise FileNotFoundError("active trial")
params.put_bool("FLMTrialApplied", False)
params.put("FLMActiveProfileId", "")
_clear_persistent_trial_baseline(params)
for path in paths["snapshots"].glob("*.json"):
path.unlink()
return {
"message": "Kept the current tuning values and made them the new FLM baseline.",
"workspace": list_workspace(),
}
def record_feedback(report_id: str, feedback: dict[str, Any]) -> dict[str, Any]:
paths = ensure_flm_workspace()
normalized = {
"acceptedDimensions": [str(item) for item in feedback.get("acceptedDimensions", [])],
"ignoredDimensions": [str(item) for item in feedback.get("ignoredDimensions", [])],
"notes": str(feedback.get("notes", "") or "").strip(),
"updatedAt": time.time(),
}
_write_json(paths["feedback"] / f"{report_id}.json", normalized)
report = load_report(report_id)
report["feedback"] = normalized
if isinstance(report.get("paths"), list) and report.get("paths"):
selected_path_key = str(report.get("selectedPathKey") or report.get("primaryPathKey") or "")
flattened_profiles = []
for path in report["paths"]:
if not isinstance(path, dict):
continue
profiles = build_trial_profiles(
report_id,
path.get("suggestions", []),
normalized,
report.get("capabilities", {}),
path_key=str(path.get("key", "cleanup_pass")),
path_label=str(path.get("title", "Cleanup Pass")),
)
path["profiles"] = profiles
flattened_profiles.extend(profiles)
if path.get("key") == selected_path_key:
report["suggestions"] = list(path.get("suggestions", []))
report["profiles"] = flattened_profiles
else:
report["profiles"] = build_trial_profiles(report_id, report.get("suggestions", []), normalized, report.get("capabilities", {}))
(paths["reports"] / f"{report_id}.html").write_text(_render_report_html(report), encoding="utf-8")
report.pop("html", None)
_write_json(paths["reports"] / f"{report_id}.json", report)
_write_json(paths["profiles"] / f"{report_id}.json", report["profiles"])
return {
"message": "Saved FLM feedback.",
"feedback": normalized,
"profiles": report["profiles"],
"report": load_report(report_id),
}
def run_worker(payload_json: str) -> None:
payload = json.loads(payload_json)
routes = [str(route) for route in payload.get("routes", [])]
footage_paths = [str(path) for path in payload.get("footagePaths", [])]
ensure_flm_workspace()
_write_flm_status({
"pid": os.getpid(),
"startedAt": time.time(),
"running": True,
"state": "starting",
"routes": routes,
"progress": 0,
"total": len(routes),
})
try:
report = analyze_routes(routes, footage_paths)
_write_flm_status({
"pid": os.getpid(),
"startedAt": time.time(),
"running": False,
"state": "complete",
"routes": routes,
"progress": len(routes),
"total": len(routes),
"reportId": report["reportId"],
})
except Exception as error:
_write_flm_status({
"pid": os.getpid(),
"startedAt": time.time(),
"running": False,
"state": "failed",
"routes": routes,
"progress": 0,
"total": len(routes),
"error": str(error),
})
raise
def main() -> None:
if len(sys.argv) >= 3 and sys.argv[1] == "worker":
run_worker(sys.argv[2])
return
if len(sys.argv) >= 2 and sys.argv[1] == "analyze":
routes = sys.argv[2:]
footage_paths = [str(Paths.log_root(HD=True, raw=True)), str(Paths.log_root(konik=True, raw=True)), str(Paths.log_root(raw=True))]
report = analyze_routes(routes, footage_paths)
print(json.dumps({"reportId": report["reportId"], "htmlPath": report["htmlPath"], "jsonPath": report["jsonPath"]}, indent=2))
return
print("Usage: flm_workspace.py analyze <route> [<route>...]")
if __name__ == "__main__":
main()