#!/usr/bin/env python import os import zmq import math import json os.environ["OMP_NUM_THREADS"] = "1" import numpy as np from bisect import bisect_right from cereal import car from common.params import Params from common.numpy_fast import clip import selfdrive.messaging as messaging from selfdrive.swaglog import cloudlog from selfdrive.controls.lib.vehicle_model import VehicleModel from selfdrive.services import service_list from selfdrive.locationd.kalman.loc_local_kf import LocLocalKalman from selfdrive.locationd.kalman.kalman_helpers import ObservationKind DEBUG = False kf = LocLocalKalman() # Make sure that model is generated on import time MAX_ANGLE_OFFSET = math.radians(10.) MAX_ANGLE_OFFSET_TH = math.radians(9.) MIN_STIFFNESS = 0.5 MAX_STIFFNESS = 2.0 MIN_SR = 0.5 MAX_SR = 2.0 MIN_SR_TH = 0.55 MAX_SR_TH = 1.9 LEARNING_RATE = 3 class Localizer(object): def __init__(self, disabled_logs=None, dog=None): self.kf = LocLocalKalman() self.reset_kalman() self.max_age = .2 # seconds self.calibration_valid = False if disabled_logs is None: self.disabled_logs = list() else: self.disabled_logs = disabled_logs def reset_kalman(self): self.filter_time = None self.observation_buffer = [] self.converter = None self.speed_counter = 0 self.sensor_counter = 0 def liveLocationMsg(self, time): fix = messaging.log.KalmanOdometry.new_message() predicted_state = self.kf.x fix.trans = [float(predicted_state[0]), float(predicted_state[1]), float(predicted_state[2])] fix.rot = [float(predicted_state[3]), float(predicted_state[4]), float(predicted_state[5])] return fix def update_kalman(self, time, kind, meas): idx = bisect_right([x[0] for x in self.observation_buffer], time) self.observation_buffer.insert(idx, (time, kind, meas)) while self.observation_buffer[-1][0] - self.observation_buffer[0][0] > self.max_age: self.kf.predict_and_observe(*self.observation_buffer.pop(0)) def handle_cam_odo(self, log, current_time): self.update_kalman(current_time, ObservationKind.CAMERA_ODO_ROTATION, np.concatenate([log.cameraOdometry.rot, log.cameraOdometry.rotStd])) self.update_kalman(current_time, ObservationKind.CAMERA_ODO_TRANSLATION, np.concatenate([log.cameraOdometry.trans, log.cameraOdometry.transStd])) def handle_live100(self, log, current_time): self.speed_counter += 1 if self.speed_counter % 5 == 0: self.update_kalman(current_time, ObservationKind.ODOMETRIC_SPEED, np.array([log.live100.vEgo])) def handle_sensors(self, log, current_time): for sensor_reading in log.sensorEvents: # TODO does not yet account for double sensor readings in the log if sensor_reading.type == 4: self.sensor_counter += 1 if self.sensor_counter % LEARNING_RATE == 0: self.update_kalman(current_time, ObservationKind.PHONE_GYRO, [-sensor_reading.gyro.v[2], -sensor_reading.gyro.v[1], -sensor_reading.gyro.v[0]]) def handle_log(self, log): current_time = 1e-9 * log.logMonoTime typ = log.which if typ in self.disabled_logs: return if typ == "sensorEvents": self.handle_sensors(log, current_time) elif typ == "live100": self.handle_live100(log, current_time) elif typ == "cameraOdometry": self.handle_cam_odo(log, current_time) class ParamsLearner(object): def __init__(self, VM, angle_offset=0., stiffness_factor=1.0, steer_ratio=None, learning_rate=1.0): self.VM = VM self.ao = math.radians(angle_offset) self.slow_ao = math.radians(angle_offset) self.x = stiffness_factor self.sR = VM.sR if steer_ratio is None else steer_ratio self.MIN_SR = MIN_SR * self.VM.sR self.MAX_SR = MAX_SR * self.VM.sR self.MIN_SR_TH = MIN_SR_TH * self.VM.sR self.MAX_SR_TH = MAX_SR_TH * self.VM.sR self.alpha1 = 0.01 * learning_rate self.alpha2 = 0.0005 * learning_rate self.alpha3 = 0.1 * learning_rate self.alpha4 = 1.0 * learning_rate def get_values(self): return { 'angleOffsetAverage': math.degrees(self.slow_ao), 'stiffnessFactor': self.x, 'steerRatio': self.sR, } def update(self, psi, u, sa): cF0 = self.VM.cF cR0 = self.VM.cR aR = self.VM.aR aF = self.VM.aF l = self.VM.l m = self.VM.m x = self.x ao = self.ao sR = self.sR # Gradient descent: learn angle offset, tire stiffness and steer ratio. if u > 10.0 and abs(math.degrees(sa)) < 15.: self.ao -= self.alpha1 * 2.0*cF0*cR0*l*u*x*(1.0*cF0*cR0*l*u*x*(ao - sa) + psi*sR*(cF0*cR0*l**2*x - m*u**2*(aF*cF0 - aR*cR0)))/(sR**2*(cF0*cR0*l**2*x - m*u**2*(aF*cF0 - aR*cR0))**2) ao = self.slow_ao self.slow_ao -= self.alpha2 * 2.0*cF0*cR0*l*u*x*(1.0*cF0*cR0*l*u*x*(ao - sa) + psi*sR*(cF0*cR0*l**2*x - m*u**2*(aF*cF0 - aR*cR0)))/(sR**2*(cF0*cR0*l**2*x - m*u**2*(aF*cF0 - aR*cR0))**2) self.x -= self.alpha3 * -2.0*cF0*cR0*l*m*u**3*(ao - sa)*(aF*cF0 - aR*cR0)*(1.0*cF0*cR0*l*u*x*(ao - sa) + psi*sR*(cF0*cR0*l**2*x - m*u**2*(aF*cF0 - aR*cR0)))/(sR**2*(cF0*cR0*l**2*x - m*u**2*(aF*cF0 - aR*cR0))**3) self.sR -= self.alpha4 * -2.0*cF0*cR0*l*u*x*(ao - sa)*(1.0*cF0*cR0*l*u*x*(ao - sa) + psi*sR*(cF0*cR0*l**2*x - m*u**2*(aF*cF0 - aR*cR0)))/(sR**3*(cF0*cR0*l**2*x - m*u**2*(aF*cF0 - aR*cR0))**2) if DEBUG: # s1 = "Measured yaw rate % .6f" % psi # ao = 0. # s2 = "Uncompensated yaw % .6f" % (1.0*u*(-ao + sa)/(l*sR*(1 - m*u**2*(aF*cF0*x - aR*cR0*x)/(cF0*cR0*l**2*x**2)))) # instant_ao = aF*m*psi*sR*u/(cR0*l*x) - aR*m*psi*sR*u/(cF0*l*x) - l*psi*sR/u + sa s4 = "Instant AO: % .2f Avg. AO % .2f" % (math.degrees(self.ao), math.degrees(self.slow_ao)) s5 = "Stiffnes: % .3f x" % self.x print("{0} {1}".format(s4, s5)) self.ao = clip(self.ao, -MAX_ANGLE_OFFSET, MAX_ANGLE_OFFSET) self.slow_ao = clip(self.slow_ao, -MAX_ANGLE_OFFSET, MAX_ANGLE_OFFSET) self.x = clip(self.x, MIN_STIFFNESS, MAX_STIFFNESS) self.sR = clip(self.sR, self.MIN_SR, self.MAX_SR) # don't check stiffness for validity, as it can change quickly if sR is off valid = abs(self.slow_ao) < MAX_ANGLE_OFFSET_TH and \ self.sR > self.MIN_SR_TH and self.sR < self.MAX_SR_TH return valid def locationd_thread(gctx, addr, disabled_logs): ctx = zmq.Context() poller = zmq.Poller() live100_socket = messaging.sub_sock(ctx, service_list['live100'].port, poller, addr=addr, conflate=True) sensor_events_socket = messaging.sub_sock(ctx, service_list['sensorEvents'].port, poller, addr=addr, conflate=True) camera_odometry_socket = messaging.sub_sock(ctx, service_list['cameraOdometry'].port, poller, addr=addr, conflate=True) kalman_odometry_socket = messaging.pub_sock(ctx, service_list['kalmanOdometry'].port) live_parameters_socket = messaging.pub_sock(ctx, service_list['liveParameters'].port) params_reader = Params() cloudlog.info("Parameter learner is waiting for CarParams") CP = car.CarParams.from_bytes(params_reader.get("CarParams", block=True)) VM = VehicleModel(CP) cloudlog.info("Parameter learner got CarParams: %s" % CP.carFingerprint) params = params_reader.get("LiveParameters") # Check if car model matches if params is not None: params = json.loads(params) if params.get('carFingerprint', None) != CP.carFingerprint: cloudlog.info("Parameter learner found parameters for wrong car.") params = None if params is None: params = { 'carFingerprint': CP.carFingerprint, 'angleOffsetAverage': 0.0, 'stiffnessFactor': 1.0, 'steerRatio': VM.sR, } cloudlog.info("Parameter learner resetting to default values") cloudlog.info("Parameter starting with: %s" % str(params)) localizer = Localizer(disabled_logs=disabled_logs) learner = ParamsLearner(VM, angle_offset=params['angleOffsetAverage'], stiffness_factor=params['stiffnessFactor'], steer_ratio=params['steerRatio'], learning_rate=LEARNING_RATE) i = 0 while True: for socket, event in poller.poll(timeout=1000): log = messaging.recv_one(socket) localizer.handle_log(log) if socket is live100_socket: if not localizer.kf.t: continue if i % LEARNING_RATE == 0: # live100 is not updating the Kalman Filter, so update KF manually localizer.kf.predict(1e-9 * log.logMonoTime) predicted_state = localizer.kf.x yaw_rate = -float(predicted_state[5]) steering_angle = math.radians(log.live100.angleSteers) params_valid = learner.update(yaw_rate, log.live100.vEgo, steering_angle) params = messaging.new_message() params.init('liveParameters') params.liveParameters.valid = bool(params_valid) params.liveParameters.angleOffset = float(math.degrees(learner.ao)) params.liveParameters.angleOffsetAverage = float(math.degrees(learner.slow_ao)) params.liveParameters.stiffnessFactor = float(learner.x) params.liveParameters.steerRatio = float(learner.sR) live_parameters_socket.send(params.to_bytes()) if i % 6000 == 0: # once a minute params = learner.get_values() params['carFingerprint'] = CP.carFingerprint params_reader.put("LiveParameters", json.dumps(params)) params_reader.put("ControlsParams", json.dumps({'angle_model_bias': log.live100.angleModelBias})) i += 1 elif socket is camera_odometry_socket: msg = messaging.new_message() msg.init('kalmanOdometry') msg.logMonoTime = log.logMonoTime msg.kalmanOdometry = localizer.liveLocationMsg(log.logMonoTime * 1e-9) kalman_odometry_socket.send(msg.to_bytes()) elif socket is sensor_events_socket: pass def main(gctx=None, addr="127.0.0.1"): IN_CAR = os.getenv("IN_CAR", False) disabled_logs = os.getenv("DISABLED_LOGS", "").split(",") # No speed for now disabled_logs.append('live100') if IN_CAR: addr = "192.168.5.11" locationd_thread(gctx, addr, disabled_logs) if __name__ == "__main__": main()