Live torque (#25456)
* wip torqued
* add basic logic
* setup in manager
* check sanity and publish msg
* add first order filter to outputs
* wire up controlsd, and update gains
* rename intercept to offset
* add cloudlog, live values are not updated
* fix bugs, do not reset points for now
* fix crashes
* rename to main
* fix bugs, works offline
* fix float in cereal bug
* add latacc filter
* randomly choose points, approx for iid
* add variable decay
* local param to capnp instead of dict
* verify works in replay
* use torqued output in controlsd
* use in controlsd; use points from past routes
* controlsd bugfix
* filter before updating gains, needs to be replaced
* save all points to ensure smooth transition across routes, revert friction factor to 1.5
* add filters to prevent noisy low-speed data points; improve fit sanity
* add engaged buffer
* revert lat_acc thresh
* use paramsd realtime process config
* make latacc-to-torque generic, and overrideable
* move freq to 4Hz, avoid storing in np.array, don't publish points in the message
* float instead of np
* remove constant while storing pts
* rename slope, offset to lat_accet_factor, offset
* resolve issues
* use camelcase in all capnp params
* use camelcase everywhere
* reduce latacc threshold or sanity, add car_sane todo, save points properly
* add and check tag
* write param to disk at end of route
* remove args
* rebase op, cereal
* save on exit
* restore default handler
* cpu usage check
* add to process replay
* handle reset better, reduce unnecessary computation
* always publish raw values - useful for debug
* regen routes
* update refs
* checks on cache restore
* check tuning vals too
* clean that up
* reduce cpu usage
* reduce cpu usage by 75%
* cleanup
* optimize further
* handle reset condition better, don't put points in init, use only in corolla
* bump cereal after rebasing
* update refs
* Update common/params.cc
Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
* remove unnecessary checks
* Update RELEASES.md
Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
old-commit-hash: 4fa62f1464
taco
parent
233e82dee7
commit
51d25b2011
15 changed files with 350 additions and 18 deletions
@ -0,0 +1,290 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
import os |
||||||
|
import sys |
||||||
|
import signal |
||||||
|
import numpy as np |
||||||
|
from collections import deque, defaultdict |
||||||
|
|
||||||
|
import cereal.messaging as messaging |
||||||
|
from cereal import car, log |
||||||
|
from common.params import Params |
||||||
|
from common.realtime import config_realtime_process, DT_MDL |
||||||
|
from common.filter_simple import FirstOrderFilter |
||||||
|
from system.swaglog import cloudlog |
||||||
|
from selfdrive.controls.lib.vehicle_model import ACCELERATION_DUE_TO_GRAVITY |
||||||
|
from selfdrive.car.toyota.values import CAR as TOYOTA |
||||||
|
|
||||||
|
HISTORY = 5 # secs |
||||||
|
POINTS_PER_BUCKET = 1500 |
||||||
|
MIN_POINTS_TOTAL = 4000 |
||||||
|
FIT_POINTS_TOTAL = 2000 |
||||||
|
MIN_VEL = 15 # m/s |
||||||
|
FRICTION_FACTOR = 1.5 # ~85% of data coverage |
||||||
|
FACTOR_SANITY = 0.3 |
||||||
|
FRICTION_SANITY = 0.5 |
||||||
|
STEER_MIN_THRESHOLD = 0.02 |
||||||
|
MIN_FILTER_DECAY = 50 |
||||||
|
MAX_FILTER_DECAY = 250 |
||||||
|
LAT_ACC_THRESHOLD = 1 |
||||||
|
STEER_BUCKET_BOUNDS = [(-0.5, -0.3), (-0.3, -0.2), (-0.2, -0.1), (-0.1, 0), (0, 0.1), (0.1, 0.2), (0.2, 0.3), (0.3, 0.5)] |
||||||
|
MIN_BUCKET_POINTS = [100, 300, 500, 500, 500, 500, 300, 100] |
||||||
|
MAX_RESETS = 5.0 |
||||||
|
MAX_INVALID_THRESHOLD = 10 |
||||||
|
MIN_ENGAGE_BUFFER = 2 # secs |
||||||
|
|
||||||
|
VERSION = 1 # bump this to invalidate old parameter caches |
||||||
|
ALLOWED_FINGERPRINTS = [TOYOTA.COROLLA_TSS2, TOYOTA.COROLLA, TOYOTA.COROLLAH_TSS2] |
||||||
|
|
||||||
|
|
||||||
|
def slope2rot(slope): |
||||||
|
sin = np.sqrt(slope**2 / (slope**2 + 1)) |
||||||
|
cos = np.sqrt(1 / (slope**2 + 1)) |
||||||
|
return np.array([[cos, -sin], [sin, cos]]) |
||||||
|
|
||||||
|
|
||||||
|
class npqueue: |
||||||
|
def __init__(self, maxlen, rowsize): |
||||||
|
self.maxlen = maxlen |
||||||
|
self.arr = np.empty((0, rowsize)) |
||||||
|
|
||||||
|
def __len__(self): |
||||||
|
return len(self.arr) |
||||||
|
|
||||||
|
def append(self, pt): |
||||||
|
if len(self.arr) < self.maxlen: |
||||||
|
self.arr = np.append(self.arr, [pt], axis=0) |
||||||
|
else: |
||||||
|
self.arr[:-1] = self.arr[1:] |
||||||
|
self.arr[-1] = pt |
||||||
|
|
||||||
|
|
||||||
|
class PointBuckets: |
||||||
|
def __init__(self, x_bounds, min_points): |
||||||
|
self.x_bounds = x_bounds |
||||||
|
self.buckets = {bounds: npqueue(maxlen=POINTS_PER_BUCKET, rowsize=3) for bounds in x_bounds} |
||||||
|
self.buckets_min_points = {bounds: min_point for bounds, min_point in zip(x_bounds, min_points)} |
||||||
|
|
||||||
|
def bucket_lengths(self): |
||||||
|
return [len(v) for v in self.buckets.values()] |
||||||
|
|
||||||
|
def __len__(self): |
||||||
|
return sum(self.bucket_lengths()) |
||||||
|
|
||||||
|
def is_valid(self): |
||||||
|
return all(len(v) >= min_pts for v, min_pts in zip(self.buckets.values(), self.buckets_min_points.values())) and (self.__len__() >= MIN_POINTS_TOTAL) |
||||||
|
|
||||||
|
def add_point(self, x, y): |
||||||
|
for bound_min, bound_max in self.x_bounds: |
||||||
|
if (x >= bound_min) and (x < bound_max): |
||||||
|
self.buckets[(bound_min, bound_max)].append([x, 1.0, y]) |
||||||
|
break |
||||||
|
|
||||||
|
def get_points(self, num_points=None): |
||||||
|
points = np.concatenate([x.arr for x in self.buckets.values() if len(x) > 0]) |
||||||
|
if num_points is None: |
||||||
|
return points |
||||||
|
return points[np.random.choice(np.arange(len(points)), min(len(points), num_points), replace=False)] |
||||||
|
|
||||||
|
def load_points(self, points): |
||||||
|
for x, y in points: |
||||||
|
self.add_point(x, y) |
||||||
|
|
||||||
|
|
||||||
|
class TorqueEstimator: |
||||||
|
def __init__(self, CP): |
||||||
|
self.hist_len = int(HISTORY / DT_MDL) |
||||||
|
self.lag = CP.steerActuatorDelay + .2 # from controlsd |
||||||
|
|
||||||
|
self.offline_friction = 0.0 |
||||||
|
self.offline_latAccelFactor = 0.0 |
||||||
|
self.resets = 0.0 |
||||||
|
self.use_params = CP.carFingerprint in ALLOWED_FINGERPRINTS |
||||||
|
|
||||||
|
if CP.lateralTuning.which() == 'torque': |
||||||
|
self.offline_friction = CP.lateralTuning.torque.friction |
||||||
|
self.offline_latAccelFactor = CP.lateralTuning.torque.latAccelFactor |
||||||
|
|
||||||
|
self.reset() |
||||||
|
|
||||||
|
initial_params = { |
||||||
|
'latAccelFactor': self.offline_latAccelFactor, |
||||||
|
'latAccelOffset': 0.0, |
||||||
|
'frictionCoefficient': self.offline_friction, |
||||||
|
'points': [] |
||||||
|
} |
||||||
|
self.decay = MIN_FILTER_DECAY |
||||||
|
self.min_lataccel_factor = (1.0 - FACTOR_SANITY) * self.offline_latAccelFactor |
||||||
|
self.max_lataccel_factor = (1.0 + FACTOR_SANITY) * self.offline_latAccelFactor |
||||||
|
self.min_friction = (1.0 - FRICTION_SANITY) * self.offline_friction |
||||||
|
self.max_friction = (1.0 + FRICTION_SANITY) * self.offline_friction |
||||||
|
|
||||||
|
# try to restore cached params |
||||||
|
params = Params() |
||||||
|
params_cache = params.get("LiveTorqueCarParams") |
||||||
|
torque_cache = params.get("LiveTorqueParameters") |
||||||
|
if params_cache is not None and torque_cache is not None: |
||||||
|
try: |
||||||
|
cache_ltp = log.Event.from_bytes(torque_cache).liveTorqueParameters |
||||||
|
cache_CP = car.CarParams.from_bytes(params_cache) |
||||||
|
if self.get_restore_key(cache_CP, cache_ltp.version) == self.get_restore_key(CP, VERSION): |
||||||
|
initial_params = { |
||||||
|
'latAccelFactor': cache_ltp.latAccelFactorFiltered, |
||||||
|
'latAccelOffset': cache_ltp.latAccelOffsetFiltered, |
||||||
|
'frictionCoefficient': cache_ltp.frictionCoefficientFiltered, |
||||||
|
'points': cache_ltp.points |
||||||
|
} |
||||||
|
self.decay = cache_ltp.decay |
||||||
|
self.filtered_points.load_points(initial_params['points']) |
||||||
|
cloudlog.info("restored torque params from cache") |
||||||
|
except Exception: |
||||||
|
cloudlog.exception("failed to restore cached torque params") |
||||||
|
params.remove("LiveTorqueCarParams") |
||||||
|
params.remove("LiveTorqueParameters") |
||||||
|
|
||||||
|
self.filtered_params = {} |
||||||
|
for param in initial_params: |
||||||
|
self.filtered_params[param] = FirstOrderFilter(initial_params[param], self.decay, DT_MDL) |
||||||
|
|
||||||
|
def get_restore_key(self, CP, version): |
||||||
|
a, b = None, None |
||||||
|
if CP.lateralTuning.which() == 'torque': |
||||||
|
a = CP.lateralTuning.torque.friction |
||||||
|
b = CP.lateralTuning.torque.latAccelFactor |
||||||
|
return (CP.carFingerprint, CP.lateralTuning.which(), a, b, version) |
||||||
|
|
||||||
|
def reset(self): |
||||||
|
self.resets += 1.0 |
||||||
|
self.invalid_values_tracker = 0.0 |
||||||
|
self.decay = MIN_FILTER_DECAY |
||||||
|
self.raw_points = defaultdict(lambda: deque(maxlen=self.hist_len)) |
||||||
|
self.filtered_points = PointBuckets(x_bounds=STEER_BUCKET_BOUNDS, min_points=MIN_BUCKET_POINTS) |
||||||
|
|
||||||
|
def estimate_params(self): |
||||||
|
points = self.filtered_points.get_points(FIT_POINTS_TOTAL) |
||||||
|
# total least square solution as both x and y are noisy observations |
||||||
|
# this is empirically the slope of the hysteresis parallelogram as opposed to the line through the diagonals |
||||||
|
try: |
||||||
|
_, _, v = np.linalg.svd(points, full_matrices=False) |
||||||
|
slope, offset = -v.T[0:2, 2] / v.T[2, 2] |
||||||
|
_, spread = np.matmul(points[:, [0, 2]], slope2rot(slope)).T |
||||||
|
friction_coeff = np.std(spread) * FRICTION_FACTOR |
||||||
|
except np.linalg.LinAlgError as e: |
||||||
|
cloudlog.exception(f"Error computing live torque params: {e}") |
||||||
|
slope = offset = friction_coeff = np.nan |
||||||
|
return slope, offset, friction_coeff |
||||||
|
|
||||||
|
def update_params(self, params): |
||||||
|
self.decay = min(self.decay + DT_MDL, MAX_FILTER_DECAY) |
||||||
|
for param, value in params.items(): |
||||||
|
self.filtered_params[param].update(value) |
||||||
|
self.filtered_params[param].update_alpha(self.decay) |
||||||
|
|
||||||
|
def is_sane(self, latAccelFactor, latAccelOffset, friction): |
||||||
|
if any([val is None or np.isnan(val) for val in [latAccelFactor, latAccelOffset, friction]]): |
||||||
|
return False |
||||||
|
return (self.max_friction >= friction >= self.min_friction) and\ |
||||||
|
(self.max_lataccel_factor >= latAccelFactor >= self.min_lataccel_factor) |
||||||
|
|
||||||
|
def handle_log(self, t, which, msg): |
||||||
|
if which == "carControl": |
||||||
|
self.raw_points["carControl_t"].append(t + self.lag) |
||||||
|
self.raw_points["steer_torque"].append(-msg.actuatorsOutput.steer) |
||||||
|
self.raw_points["active"].append(msg.latActive) |
||||||
|
elif which == "carState": |
||||||
|
self.raw_points["carState_t"].append(t + self.lag) |
||||||
|
self.raw_points["vego"].append(msg.vEgo) |
||||||
|
self.raw_points["steer_override"].append(msg.steeringPressed) |
||||||
|
elif which == "liveLocationKalman": |
||||||
|
if len(self.raw_points['steer_torque']) == self.hist_len: |
||||||
|
yaw_rate = msg.angularVelocityCalibrated.value[2] |
||||||
|
roll = msg.orientationNED.value[0] |
||||||
|
active = np.interp(np.arange(t - MIN_ENGAGE_BUFFER, t, DT_MDL), self.raw_points['carControl_t'], self.raw_points['active']).astype(bool) |
||||||
|
steer_override = np.interp(np.arange(t - MIN_ENGAGE_BUFFER, t, DT_MDL), self.raw_points['carState_t'], self.raw_points['steer_override']).astype(bool) |
||||||
|
vego = np.interp(t, self.raw_points['carState_t'], self.raw_points['vego']) |
||||||
|
steer = np.interp(t, self.raw_points['carControl_t'], self.raw_points['steer_torque']) |
||||||
|
lateral_acc = (vego * yaw_rate) - (np.sin(roll) * ACCELERATION_DUE_TO_GRAVITY) |
||||||
|
if all(active) and (not any(steer_override)) and (vego > MIN_VEL) and (abs(steer) > STEER_MIN_THRESHOLD) and (abs(lateral_acc) <= LAT_ACC_THRESHOLD): |
||||||
|
self.filtered_points.add_point(float(steer), float(lateral_acc)) |
||||||
|
|
||||||
|
def get_msg(self, valid=True, with_points=False): |
||||||
|
msg = messaging.new_message('liveTorqueParameters') |
||||||
|
msg.valid = valid |
||||||
|
liveTorqueParameters = msg.liveTorqueParameters |
||||||
|
liveTorqueParameters.version = VERSION |
||||||
|
liveTorqueParameters.useParams = self.use_params |
||||||
|
|
||||||
|
if self.filtered_points.is_valid(): |
||||||
|
latAccelFactor, latAccelOffset, friction_coeff = self.estimate_params() |
||||||
|
liveTorqueParameters.latAccelFactorRaw = float(latAccelFactor) |
||||||
|
liveTorqueParameters.latAccelOffsetRaw = float(latAccelOffset) |
||||||
|
liveTorqueParameters.frictionCoefficientRaw = float(friction_coeff) |
||||||
|
|
||||||
|
if self.is_sane(latAccelFactor, latAccelOffset, friction_coeff): |
||||||
|
liveTorqueParameters.liveValid = True |
||||||
|
self.update_params({'latAccelFactor': latAccelFactor, 'latAccelOffset': latAccelOffset, 'frictionCoefficient': friction_coeff}) |
||||||
|
self.invalid_values_tracker = max(0.0, self.invalid_values_tracker - 0.5) |
||||||
|
else: |
||||||
|
cloudlog.exception("live torque params are numerically unstable") |
||||||
|
liveTorqueParameters.liveValid = False |
||||||
|
self.invalid_values_tracker += 1.0 |
||||||
|
# Reset when ~10 invalid over 5 secs |
||||||
|
if self.invalid_values_tracker > MAX_INVALID_THRESHOLD: |
||||||
|
# Do not reset the filter as it may cause a drastic jump, just reset points |
||||||
|
self.reset() |
||||||
|
else: |
||||||
|
liveTorqueParameters.liveValid = False |
||||||
|
|
||||||
|
if with_points: |
||||||
|
liveTorqueParameters.points = self.filtered_points.get_points()[:, [0, 2]].tolist() |
||||||
|
|
||||||
|
liveTorqueParameters.latAccelFactorFiltered = float(self.filtered_params['latAccelFactor'].x) |
||||||
|
liveTorqueParameters.latAccelOffsetFiltered = float(self.filtered_params['latAccelOffset'].x) |
||||||
|
liveTorqueParameters.frictionCoefficientFiltered = float(self.filtered_params['frictionCoefficient'].x) |
||||||
|
liveTorqueParameters.totalBucketPoints = len(self.filtered_points) |
||||||
|
liveTorqueParameters.decay = self.decay |
||||||
|
liveTorqueParameters.maxResets = self.resets |
||||||
|
return msg |
||||||
|
|
||||||
|
|
||||||
|
def main(sm=None, pm=None): |
||||||
|
config_realtime_process([0, 1, 2, 3], 5) |
||||||
|
|
||||||
|
if sm is None: |
||||||
|
sm = messaging.SubMaster(['carControl', 'carState', 'liveLocationKalman'], poll=['liveLocationKalman']) |
||||||
|
|
||||||
|
if pm is None: |
||||||
|
pm = messaging.PubMaster(['liveTorqueParameters']) |
||||||
|
|
||||||
|
params = Params() |
||||||
|
CP = car.CarParams.from_bytes(params.get("CarParams", block=True)) |
||||||
|
estimator = TorqueEstimator(CP) |
||||||
|
|
||||||
|
def cache_params(sig, frame): |
||||||
|
signal.signal(sig, signal.SIG_DFL) |
||||||
|
cloudlog.warning("caching torque params") |
||||||
|
|
||||||
|
params = Params() |
||||||
|
params.put("LiveTorqueCarParams", CP.as_builder().to_bytes()) |
||||||
|
|
||||||
|
msg = estimator.get_msg(with_points=True) |
||||||
|
params.put("LiveTorqueParameters", msg.to_bytes()) |
||||||
|
|
||||||
|
sys.exit(0) |
||||||
|
if "REPLAY" not in os.environ: |
||||||
|
signal.signal(signal.SIGINT, cache_params) |
||||||
|
|
||||||
|
while True: |
||||||
|
sm.update() |
||||||
|
if sm.all_checks(): |
||||||
|
for which in sm.updated.keys(): |
||||||
|
if sm.updated[which]: |
||||||
|
t = sm.logMonoTime[which] * 1e-9 |
||||||
|
estimator.handle_log(t, which, sm[which]) |
||||||
|
|
||||||
|
# 4Hz driven by liveLocationKalman |
||||||
|
if sm.frame % 5 == 0: |
||||||
|
pm.send('liveTorqueParameters', estimator.get_msg(valid=sm.all_checks())) |
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
main() |
@ -1 +1 @@ |
|||||||
d14f1a61a4bfde810128a6bb703aa543268fa4a9 |
deb07ca8c5dc021e57e81307764a84aa3d7aef32 |
Loading…
Reference in new issue