parent
aa1b61eb8e
commit
e90c41c576
56 changed files with 884 additions and 1094 deletions
Binary file not shown.
@ -1,253 +0,0 @@ |
||||
# pylint: skip-file |
||||
from __future__ import print_function |
||||
import abc |
||||
import numpy as np |
||||
# The EKF class contains the framework for an Extended Kalman Filter, but must be subclassed to use. |
||||
# A subclass must implement: |
||||
# 1) calc_transfer_fun(); see bottom of file for more info. |
||||
# 2) __init__() to initialize self.state, self.covar, and self.process_noise appropriately |
||||
|
||||
# Alternatively, the existing implementations of EKF can be used (e.g. EKF2D) |
||||
|
||||
# Sensor classes are optionally used to pass measurement information into the EKF, to keep |
||||
# sensor parameters and processing methods for a each sensor together. |
||||
# Sensor classes have a read() method which takes raw sensor data and returns |
||||
# a SensorReading object, which can be passed to the EKF update() method. |
||||
|
||||
# For usage, see run_ekf1d.py in selfdrive/new for a simple example. |
||||
# ekf.predict(dt) should be called between update cycles with the time since it was last called. |
||||
# Ideally, predict(dt) should be called at a relatively constant rate. |
||||
# update() should be called once per sensor, and can be called multiple times between predict steps. |
||||
# Access and set the state of the filter directly with ekf.state and ekf.covar. |
||||
|
||||
|
||||
class SensorReading: |
||||
# Given a perfect model and no noise, data = obs_model * state |
||||
def __init__(self, data, covar, obs_model): |
||||
self.data = data |
||||
self.obs_model = obs_model |
||||
self.covar = covar |
||||
|
||||
def __repr__(self): |
||||
return "SensorReading(data={}, covar={}, obs_model={})".format( |
||||
repr(self.data), repr(self.covar), repr(self.obs_model)) |
||||
|
||||
|
||||
# A generic sensor class that does no pre-processing of data |
||||
class SimpleSensor: |
||||
# obs_model can be |
||||
# a full observation model matrix, or |
||||
# an integer or tuple of indices into ekf.state, indicating which variables are being directly observed |
||||
# covar can be |
||||
# a full covariance matrix |
||||
# a float or tuple of individual covars for each component of the sensor reading |
||||
# dims is the number of states in the EKF |
||||
def __init__(self, obs_model, covar, dims): |
||||
# Allow for integer covar/obs_model |
||||
if not hasattr(obs_model, "__len__"): |
||||
obs_model = (obs_model, ) |
||||
if not hasattr(covar, "__len__"): |
||||
covar = (covar, ) |
||||
|
||||
# Full observation model passed |
||||
if dims in np.array(obs_model).shape: |
||||
self.obs_model = np.asmatrix(obs_model) |
||||
self.covar = np.asmatrix(covar) |
||||
# Indices of unit observations passed |
||||
else: |
||||
self.obs_model = np.matlib.zeros((len(obs_model), dims)) |
||||
self.obs_model[:, list(obs_model)] = np.identity(len(obs_model)) |
||||
if np.asarray(covar).ndim == 2: |
||||
self.covar = np.asmatrix(covar) |
||||
elif len(covar) == len(obs_model): |
||||
self.covar = np.matlib.diag(covar) |
||||
else: |
||||
self.covar = np.matlib.identity(len(obs_model)) * covar |
||||
|
||||
def read(self, data, covar=None): |
||||
if covar: |
||||
self.covar = covar |
||||
return SensorReading(data, self.covar, self.obs_model) |
||||
|
||||
|
||||
class EKF: |
||||
__metaclass__ = abc.ABCMeta |
||||
|
||||
def __init__(self, debug=False): |
||||
self.DEBUG = debug |
||||
|
||||
|
||||
def __str__(self): |
||||
return "EKF(state={}, covar={})".format(self.state, self.covar) |
||||
|
||||
# Measurement update |
||||
# Reading should be a SensorReading object with data, covar, and obs_model attributes |
||||
def update(self, reading): |
||||
# Potential improvements: |
||||
# deal with negative covars |
||||
# add noise to really low covars to ensure stability |
||||
# use mahalanobis distance to reject outliers |
||||
# wrap angles after state updates and innovation |
||||
|
||||
# y = z - H*x |
||||
innovation = reading.data - reading.obs_model * self.state |
||||
|
||||
if self.DEBUG: |
||||
print("reading:\n",reading.data) |
||||
print("innovation:\n",innovation) |
||||
|
||||
# S = H*P*H' + R |
||||
innovation_covar = reading.obs_model * self.covar * reading.obs_model.T + reading.covar |
||||
|
||||
# K = P*H'*S^-1 |
||||
kalman_gain = self.covar * reading.obs_model.T * np.linalg.inv( |
||||
innovation_covar) |
||||
|
||||
if self.DEBUG: |
||||
print("gain:\n", kalman_gain) |
||||
print("innovation_covar:\n", innovation_covar) |
||||
print("innovation: ", innovation) |
||||
print("test: ", self.covar * reading.obs_model.T * ( |
||||
reading.obs_model * self.covar * reading.obs_model.T + reading.covar * |
||||
0).I) |
||||
|
||||
# x = x + K*y |
||||
self.state += kalman_gain*innovation |
||||
|
||||
# print "covar", np.diag(self.covar) |
||||
#self.state[(roll_vel, yaw_vel, pitch_vel),:] = reading.data |
||||
|
||||
# Standard form: P = (I - K*H)*P |
||||
# self.covar = (self.identity - kalman_gain*reading.obs_model) * self.covar |
||||
|
||||
# Use the Joseph form for numerical stability: P = (I-K*H)*P*(I - K*H)' + K*R*K' |
||||
aux_mtrx = (self.identity - kalman_gain * reading.obs_model) |
||||
self.covar = aux_mtrx * self.covar * aux_mtrx.T + kalman_gain * reading.covar * kalman_gain.T |
||||
|
||||
if self.DEBUG: |
||||
print("After update") |
||||
print("state\n", self.state) |
||||
print("covar:\n",self.covar) |
||||
|
||||
def update_scalar(self, reading): |
||||
# like update but knowing that measurement is a scalar |
||||
# this avoids matrix inversions and speeds up (surprisingly) drived.py a lot |
||||
|
||||
# innovation = reading.data - np.matmul(reading.obs_model, self.state) |
||||
# innovation_covar = np.matmul(np.matmul(reading.obs_model, self.covar), reading.obs_model.T) + reading.covar |
||||
# kalman_gain = np.matmul(self.covar, reading.obs_model.T)/innovation_covar |
||||
# self.state += np.matmul(kalman_gain, innovation) |
||||
# aux_mtrx = self.identity - np.matmul(kalman_gain, reading.obs_model) |
||||
# self.covar = np.matmul(aux_mtrx, np.matmul(self.covar, aux_mtrx.T)) + np.matmul(kalman_gain, np.matmul(reading.covar, kalman_gain.T)) |
||||
|
||||
# written without np.matmul |
||||
es = np.einsum |
||||
ABC_T = "ij,jk,lk->il" |
||||
AB_T = "ij,kj->ik" |
||||
AB = "ij,jk->ik" |
||||
innovation = reading.data - es(AB, reading.obs_model, self.state) |
||||
innovation_covar = es(ABC_T, reading.obs_model, self.covar, |
||||
reading.obs_model) + reading.covar |
||||
kalman_gain = es(AB_T, self.covar, reading.obs_model) / innovation_covar |
||||
|
||||
self.state += es(AB, kalman_gain, innovation) |
||||
aux_mtrx = self.identity - es(AB, kalman_gain, reading.obs_model) |
||||
self.covar = es(ABC_T, aux_mtrx, self.covar, aux_mtrx) + \ |
||||
es(ABC_T, kalman_gain, reading.covar, kalman_gain) |
||||
|
||||
# Prediction update |
||||
def predict(self, dt): |
||||
es = np.einsum |
||||
ABC_T = "ij,jk,lk->il" |
||||
AB = "ij,jk->ik" |
||||
|
||||
# State update |
||||
transfer_fun, transfer_fun_jacobian = self.calc_transfer_fun(dt) |
||||
|
||||
# self.state = np.matmul(transfer_fun, self.state) |
||||
# self.covar = np.matmul(np.matmul(transfer_fun_jacobian, self.covar), transfer_fun_jacobian.T) + self.process_noise * dt |
||||
|
||||
# x = f(x, u), written in the form x = A(x, u)*x |
||||
self.state = es(AB, transfer_fun, self.state) |
||||
|
||||
# P = J*P*J' + Q |
||||
self.covar = es(ABC_T, transfer_fun_jacobian, self.covar, |
||||
transfer_fun_jacobian) + self.process_noise * dt #!dt |
||||
|
||||
#! Clip covariance to avoid explosions |
||||
self.covar = np.clip(self.covar,-1e10,1e10) |
||||
|
||||
@abc.abstractmethod |
||||
def calc_transfer_fun(self, dt): |
||||
"""Return a tuple with the transfer function and transfer function jacobian |
||||
The transfer function and jacobian should both be a numpy matrix of size DIMSxDIMS |
||||
|
||||
The transfer function matrix A should satisfy the state-update equation |
||||
x_(k+1) = A * x_k |
||||
|
||||
The jacobian J is the direct jacobian A*x_k. For linear systems J=A. |
||||
|
||||
Current implementations calculate A and J as functions of state. Control input |
||||
can be added trivially by adding a control parameter to predict() and calc_tranfer_update(), |
||||
and using it during calculation of A and J |
||||
""" |
||||
|
||||
|
||||
class FastEKF1D(EKF): |
||||
"""Fast version of EKF for 1D problems with scalar readings.""" |
||||
|
||||
def __init__(self, dt, var_init, Q): |
||||
super(FastEKF1D, self).__init__(False) |
||||
self.state = [0, 0] |
||||
self.covar = [var_init, var_init, 0] |
||||
|
||||
# Process Noise |
||||
self.dtQ0 = dt * Q[0] |
||||
self.dtQ1 = dt * Q[1] |
||||
|
||||
def update(self, reading): |
||||
raise NotImplementedError |
||||
|
||||
def update_scalar(self, reading): |
||||
# TODO(mgraczyk): Delete this for speed. |
||||
# assert np.all(reading.obs_model == [1, 0]) |
||||
|
||||
rcov = reading.covar[0, 0] |
||||
|
||||
x = self.state |
||||
S = self.covar |
||||
|
||||
innovation = reading.data - x[0] |
||||
innovation_covar = S[0] + rcov |
||||
|
||||
k0 = S[0] / innovation_covar |
||||
k1 = S[2] / innovation_covar |
||||
|
||||
x[0] += k0 * innovation |
||||
x[1] += k1 * innovation |
||||
|
||||
mk = 1 - k0 |
||||
S[1] += k1 * (k1 * (S[0] + rcov) - 2 * S[2]) |
||||
S[2] = mk * (S[2] - k1 * S[0]) + rcov * k0 * k1 |
||||
S[0] = mk * mk * S[0] + rcov * k0 * k0 |
||||
|
||||
def predict(self, dt): |
||||
# State update |
||||
x = self.state |
||||
|
||||
x[0] += dt * x[1] |
||||
|
||||
# P = J*P*J' + Q |
||||
S = self.covar |
||||
S[0] += dt * (2 * S[2] + dt * S[1]) + self.dtQ0 |
||||
S[2] += dt * S[1] |
||||
S[1] += self.dtQ1 |
||||
|
||||
# Clip covariance to avoid explosions |
||||
S = max(-1e10, min(S, 1e10)) |
||||
|
||||
def calc_transfer_fun(self, dt): |
||||
tf = np.identity(2) |
||||
tf[0, 1] = dt |
||||
tfj = tf |
||||
return tf, tfj |
@ -1,116 +0,0 @@ |
||||
import numpy as np |
||||
import numpy.matlib |
||||
import unittest |
||||
import timeit |
||||
|
||||
from common.kalman.ekf import EKF, SimpleSensor, FastEKF1D |
||||
|
||||
class TestEKF(EKF): |
||||
def __init__(self, var_init, Q): |
||||
super(TestEKF, self).__init__(False) |
||||
self.identity = numpy.matlib.identity(2) |
||||
self.state = numpy.matlib.zeros((2, 1)) |
||||
self.covar = self.identity * var_init |
||||
|
||||
self.process_noise = numpy.matlib.diag(Q) |
||||
|
||||
def calc_transfer_fun(self, dt): |
||||
tf = numpy.matlib.identity(2) |
||||
tf[0, 1] = dt |
||||
return tf, tf |
||||
|
||||
|
||||
class EKFTest(unittest.TestCase): |
||||
def test_update_scalar(self): |
||||
ekf = TestEKF(1e3, [0.1, 1]) |
||||
dt = 1. / 100 |
||||
|
||||
sensor = SimpleSensor(0, 1, 2) |
||||
readings = map(sensor.read, np.arange(100, 300)) |
||||
|
||||
for reading in readings: |
||||
ekf.update_scalar(reading) |
||||
ekf.predict(dt) |
||||
|
||||
np.testing.assert_allclose(ekf.state, [[300], [100]], 1e-4) |
||||
np.testing.assert_allclose( |
||||
ekf.covar, |
||||
np.asarray([[0.0563, 0.10278], [0.10278, 0.55779]]), |
||||
atol=1e-4) |
||||
|
||||
def test_unbiased(self): |
||||
ekf = TestEKF(1e3, [0., 0.]) |
||||
dt = np.float64(1. / 100) |
||||
|
||||
sensor = SimpleSensor(0, 1, 2) |
||||
readings = map(sensor.read, np.arange(1000)) |
||||
|
||||
for reading in readings: |
||||
ekf.update_scalar(reading) |
||||
ekf.predict(dt) |
||||
|
||||
np.testing.assert_allclose(ekf.state, [[1000.], [100.]], 1e-4) |
||||
|
||||
|
||||
class FastEKF1DTest(unittest.TestCase): |
||||
def test_correctness(self): |
||||
dt = 1. / 100 |
||||
reading = SimpleSensor(0, 1, 2).read(100) |
||||
|
||||
ekf = TestEKF(1e3, [0.1, 1]) |
||||
fast_ekf = FastEKF1D(dt, 1e3, [0.1, 1]) |
||||
|
||||
ekf.update_scalar(reading) |
||||
fast_ekf.update_scalar(reading) |
||||
self.assertAlmostEqual(ekf.state[0] , fast_ekf.state[0]) |
||||
self.assertAlmostEqual(ekf.state[1] , fast_ekf.state[1]) |
||||
self.assertAlmostEqual(ekf.covar[0, 0], fast_ekf.covar[0]) |
||||
self.assertAlmostEqual(ekf.covar[0, 1], fast_ekf.covar[2]) |
||||
self.assertAlmostEqual(ekf.covar[1, 1], fast_ekf.covar[1]) |
||||
|
||||
ekf.predict(dt) |
||||
fast_ekf.predict(dt) |
||||
self.assertAlmostEqual(ekf.state[0] , fast_ekf.state[0]) |
||||
self.assertAlmostEqual(ekf.state[1] , fast_ekf.state[1]) |
||||
self.assertAlmostEqual(ekf.covar[0, 0], fast_ekf.covar[0]) |
||||
self.assertAlmostEqual(ekf.covar[0, 1], fast_ekf.covar[2]) |
||||
self.assertAlmostEqual(ekf.covar[1, 1], fast_ekf.covar[1]) |
||||
|
||||
def test_speed(self): |
||||
setup = """ |
||||
import numpy as np |
||||
from common.kalman.tests.test_ekf import TestEKF |
||||
from common.kalman.ekf import SimpleSensor, FastEKF1D |
||||
|
||||
dt = 1. / 100 |
||||
reading = SimpleSensor(0, 1, 2).read(100) |
||||
|
||||
var_init, Q = 1e3, [0.1, 1] |
||||
ekf = TestEKF(var_init, Q) |
||||
fast_ekf = FastEKF1D(dt, var_init, Q) |
||||
""" |
||||
|
||||
timeit.timeit(""" |
||||
ekf.update_scalar(reading) |
||||
ekf.predict(dt) |
||||
""", setup=setup, number=1000) |
||||
|
||||
ekf_speed = timeit.timeit(""" |
||||
ekf.update_scalar(reading) |
||||
ekf.predict(dt) |
||||
""", setup=setup, number=20000) |
||||
|
||||
timeit.timeit(""" |
||||
fast_ekf.update_scalar(reading) |
||||
fast_ekf.predict(dt) |
||||
""", setup=setup, number=1000) |
||||
|
||||
fast_ekf_speed = timeit.timeit(""" |
||||
fast_ekf.update_scalar(reading) |
||||
fast_ekf.predict(dt) |
||||
""", setup=setup, number=20000) |
||||
|
||||
assert fast_ekf_speed < ekf_speed / 4 |
||||
|
||||
if __name__ == "__main__": |
||||
unittest.main() |
Binary file not shown.
Binary file not shown.
@ -1 +1 @@ |
||||
#define COMMA_VERSION "0.6.1-release" |
||||
#define COMMA_VERSION "0.6.2-release" |
||||
|
Loading…
Reference in new issue