Skip to content

Commit

Permalink
Merge pull request #1 from commaai/master
Browse files Browse the repository at this point in the history
EKF_sym class rewritten to c++ (commaai#9)
  • Loading branch information
rav4kumar committed Apr 9, 2021
2 parents 946a034 + a0eb4d2 commit 3552c27
Show file tree
Hide file tree
Showing 23 changed files with 1,008 additions and 120 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Expand Up @@ -13,4 +13,4 @@ jobs:
run: |
docker run rednose bash -c "git init && git add -A && pre-commit run --all"
- name: Unit Tests
run: docker run rednose bash -c "cd /project/rednose/examples; python -m unittest discover"
run: docker run rednose bash -c "cd /project/examples; python -m unittest discover"
7 changes: 7 additions & 0 deletions .gitignore
Expand Up @@ -2,12 +2,19 @@ generated/
.sconsign.dblite
*.swp

# Cython intermediates
*_pyx.cpp
*_pyx.h
*_pyx_api.h
*.os

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.o
*.so

# Distribution / packaging
Expand Down
8 changes: 4 additions & 4 deletions Dockerfile
Expand Up @@ -5,12 +5,12 @@ RUN apt-get update && apt-get install -y libzmq3-dev capnproto libcapnp-dev clan

RUN curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash
ENV PATH="/root/.pyenv/bin:/root/.pyenv/shims:${PATH}"
RUN pyenv install 3.7.3
RUN pyenv global 3.7.3
RUN pyenv install 3.8.5
RUN pyenv global 3.8.5
RUN pyenv rehash
RUN pip3 install scons==3.1.1 pre-commit==2.4.0 pylint==2.5.2
RUN pip3 install scons==4.1.0.post1 pre-commit==2.10.1 pylint==2.7.1 Cython==0.29.22

WORKDIR /project/rednose
WORKDIR /project

ENV PYTHONPATH=/project

Expand Down
32 changes: 0 additions & 32 deletions SConscript

This file was deleted.

43 changes: 41 additions & 2 deletions SConstruct
@@ -1,8 +1,20 @@
import os
import subprocess
import sysconfig
import numpy as np

arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip()

python_path = sysconfig.get_paths()['include']
cpppath = [
'#',
'#rednose',
'#rednose/examples/generated',
'/usr/lib/include',
python_path,
np.get_include(),
]

env = Environment(
ENV=os.environ,
CC='clang',
Expand All @@ -17,10 +29,37 @@ env = Environment(
"-Werror=return-type",
"-Werror=format-extra-args",
],
LIBPATH=["#rednose/examples/generated"],
CFLAGS="-std=gnu11",
CXXFLAGS="-std=c++1z",
CPPPATH=cpppath,
tools=["default", "cython"],
)

# Cython build enviroment
envCython = env.Clone()
envCython["CCFLAGS"] += ["-Wno-#warnings", "-Wno-deprecated-declarations"]

envCython["LIBS"] = []
if arch == "Darwin":
envCython["LINKFLAGS"] = ["-bundle", "-undefined", "dynamic_lookup"]
elif arch == "aarch64":
envCython["LINKFLAGS"] = ["-shared"]
envCython["LIBS"] = [os.path.basename(python_path)]
else:
envCython["LINKFLAGS"] = ["-pthread", "-shared"]

rednose_config = {
'generated_folder': '#examples/generated',
'to_build': {
'live': ('#examples/live_kf.py', True),
'kinematic': ('#examples/kinematic_kf.py', True),
'compare': ('#examples/test_compare.py', True),
'pos_computer_4': ('#rednose/helpers/lst_sq_computer.py', False),
'pos_computer_5': ('#rednose/helpers/lst_sq_computer.py', False),
'feature_handler_5': ('#rednose/helpers/feature_handler.py', False),
},
}

Export('env', 'arch')
SConscript(['SConscript'])
Export('env', 'envCython', 'arch', 'rednose_config')
SConscript(['#rednose/SConscript'])
16 changes: 14 additions & 2 deletions examples/kinematic_kf.py
Expand Up @@ -4,8 +4,12 @@
import numpy as np
import sympy as sp

from rednose import KalmanFilter
from rednose.helpers.ekf_sym import gen_code
from rednose.helpers.kalmanfilter import KalmanFilter

if __name__ == '__main__': # generating sympy code
from rednose.helpers.ekf_sym import gen_code
else:
from rednose.helpers.ekf_sym_pyx import EKF_sym


class ObservationKind():
Expand Down Expand Up @@ -64,6 +68,14 @@ def generate_code(generated_dir):

gen_code(generated_dir, name, f_sym, dt, state_sym, obs_eqs, dim_state, dim_state)

def __init__(self, generated_dir):
dim_state = self.initial_x.shape[0]
dim_state_err = self.initial_P_diag.shape[0]

# init filter
self.filter = EKF_sym(generated_dir, self.name, self.Q, self.initial_x, np.diag(self.initial_P_diag), dim_state, dim_state_err)


if __name__ == "__main__":
generated_dir = sys.argv[2]
KinematicKalman.generate_code(generated_dir)
10 changes: 7 additions & 3 deletions examples/live_kf.py
@@ -1,11 +1,15 @@
#!/usr/bin/env python3
import sys
import numpy as np
import sympy as sp

from rednose.helpers import KalmanError
from rednose.helpers.ekf_sym import EKF_sym, gen_code
from rednose.helpers.sympy_helpers import (euler_rotate, quat_matrix_r, quat_rotate)

if __name__ == '__main__': # Generating sympy
import sympy as sp
from rednose.helpers.sympy_helpers import euler_rotate, quat_matrix_r, quat_rotate
from rednose.helpers.ekf_sym import gen_code
else:
from rednose.helpers.ekf_sym_pyx import EKF_sym # pylint: disable=no-name-in-module

EARTH_GM = 3.986005e14 # m^3/s^2 (gravitational constant * mass of earth)

Expand Down
125 changes: 125 additions & 0 deletions examples/test_compare.py
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
import os
import sys
import sympy as sp
import numpy as np
import unittest

if __name__ == '__main__': # generating sympy code
from rednose.helpers.ekf_sym import gen_code
else:
from rednose.helpers.ekf_sym_pyx import EKF_sym
from rednose.helpers.ekf_sym import EKF_sym as EKF_sym2


GENERATED_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), 'generated'))


class ObservationKind:
UNKNOWN = 0
NO_OBSERVATION = 1
POSITION = 1

names = [
'Unknown',
'No observation',
'Position'
]

@classmethod
def to_string(cls, kind):
return cls.names[kind]


class States:
POSITION = slice(0, 1)
VELOCITY = slice(1, 2)


class CompareFilter:
name = "compare"

initial_x = np.array([0.5, 0.0])
initial_P_diag = np.array([1.0**2, 1.0**2])
Q = np.diag([0.1**2, 2.0**2])
obs_noise = {ObservationKind.POSITION: np.atleast_2d(0.1**2)}

@staticmethod
def generate_code(generated_dir):
name = CompareFilter.name
dim_state = CompareFilter.initial_x.shape[0]

state_sym = sp.MatrixSymbol('state', dim_state, 1)
state = sp.Matrix(state_sym)

position = state[States.POSITION, :][0,:]
velocity = state[States.VELOCITY, :][0,:]

dt = sp.Symbol('dt')
state_dot = sp.Matrix(np.zeros((dim_state, 1)))
state_dot[States.POSITION.start, 0] = velocity
f_sym = state + dt * state_dot

obs_eqs = [
[sp.Matrix([position]), ObservationKind.POSITION, None],
]

gen_code(generated_dir, name, f_sym, dt, state_sym, obs_eqs, dim_state, dim_state)

def __init__(self, generated_dir):
dim_state = self.initial_x.shape[0]
dim_state_err = self.initial_P_diag.shape[0]

# init filter
self.filter_py = EKF_sym(generated_dir, self.name, self.Q, self.initial_x, np.diag(self.initial_P_diag), dim_state, dim_state_err)
self.filter_pyx = EKF_sym2(generated_dir, self.name, self.Q, self.initial_x, np.diag(self.initial_P_diag), dim_state, dim_state_err)

def get_R(self, kind, n):
obs_noise = self.obs_noise[kind]
dim = obs_noise.shape[0]
R = np.zeros((n, dim, dim))
for i in range(n):
R[i, :, :] = obs_noise
return R


class TestCompare(unittest.TestCase):
def test_compare(self):
np.random.seed(0)

kf = CompareFilter(GENERATED_DIR)

# Simple simulation
dt = 0.01
ts = np.arange(0, 5, step=dt)
xs = np.empty(ts.shape)

# Simulate
x = 0.0
for i, v in enumerate(np.sin(ts * 5)):
xs[i] = x
x += v * dt

# insert late observation
switch = (20, 40)
ts[switch[0]], ts[switch[1]] = ts[switch[1]], ts[switch[0]]
xs[switch[0]], xs[switch[1]] = xs[switch[1]], xs[switch[0]]

for t, x in zip(ts, xs):
# get measurement
meas = np.random.normal(x, 0.1)
z = np.array([[meas]])
R = kf.get_R(ObservationKind.POSITION, 1)

# Update kf
kf.filter_py.predict_and_update_batch(t, ObservationKind.POSITION, z, R)
kf.filter_pyx.predict_and_update_batch(t, ObservationKind.POSITION, z, R)

self.assertAlmostEqual(kf.filter_py.get_filter_time(), kf.filter_pyx.get_filter_time())
self.assertTrue(np.allclose(kf.filter_py.state(), kf.filter_pyx.state()))
self.assertTrue(np.allclose(kf.filter_py.covs(), kf.filter_pyx.covs()))


if __name__ == "__main__":
generated_dir = sys.argv[2]
CompareFilter.generate_code(generated_dir)
37 changes: 37 additions & 0 deletions rednose/SConscript
@@ -0,0 +1,37 @@
Import('env', 'envCython', 'arch', 'rednose_config')

generated_folder = rednose_config['generated_folder']

templates = Glob('#rednose/templates/*')

sympy_helpers = "#rednose/helpers/sympy_helpers.py"
ekf_sym = "#rednose/helpers/ekf_sym.py"
ekf_sym_pyx = "#rednose/helpers/ekf_sym_pyx.pyx"
ekf_sym_cc = "#rednose/helpers/ekf_sym.cc"
common_ekf = "#rednose/helpers/common_ekf.cc"

found = {}
for target, (command, combined_lib) in rednose_config['to_build'].items():
if File(command).exists():
found[target] = (command, combined_lib)

lib_target = [common_ekf]
for target, (command, combined_lib) in found.items():
target_files = File([f'{generated_folder}/{target}.cpp', f'{generated_folder}/{target}.h'])
command_file = File(command)

env.Command(target_files,
[templates, command_file, sympy_helpers, ekf_sym],
command_file.get_abspath() + " " + target + " " + Dir(generated_folder).get_abspath())

if combined_lib:
lib_target.append(target_files[0])
else:
env.SharedLibrary(f'{generated_folder}/' + target, [target_files[0], common_ekf])

libkf = env.SharedLibrary(f'{generated_folder}/libkf', lib_target)

lenv = envCython.Clone()
lenv["LINKFLAGS"] += [libkf[0].get_labspath()]
ekf_sym_so = lenv.Program('#rednose/helpers/ekf_sym_pyx.so', [ekf_sym_pyx, ekf_sym_cc, common_ekf])
lenv.Depends(ekf_sym_so, libkf)
9 changes: 7 additions & 2 deletions rednose/helpers/__init__.py
Expand Up @@ -13,14 +13,19 @@ def write_code(folder, name, code, header):
open(os.path.join(folder, f"{name}.h"), 'w').write(header)


def load_code(folder, name):
def load_code(folder, name, lib_name=None):
if lib_name is None:
lib_name = name
shared_ext = "dylib" if platform.system() == "Darwin" else "so"
shared_fn = os.path.join(folder, f"lib{name}.{shared_ext}")
shared_fn = os.path.join(folder, f"lib{lib_name}.{shared_ext}")
header_fn = os.path.join(folder, f"{name}.h")

with open(header_fn) as f:
header = f.read()

# is the only thing that can be parsed by cffi
header = "\n".join([line for line in header.split("\n") if line.startswith("void ")])

ffi = FFI()
ffi.cdef(header)
return (ffi, ffi.dlopen(shared_fn))
Expand Down
19 changes: 19 additions & 0 deletions rednose/helpers/common_ekf.cc
@@ -0,0 +1,19 @@
#include "common_ekf.h"

std::vector<const EKF*>& get_ekfs() {
static std::vector<const EKF*> vec;
return vec;
}

void ekf_register(const EKF* ekf) {
get_ekfs().push_back(ekf);
}

const EKF* ekf_lookup(const std::string& ekf_name) {
for (const auto& ekfi : get_ekfs()) {
if (ekf_name == ekfi->name) {
return ekfi;
}
}
return NULL;
}

0 comments on commit 3552c27

Please sign in to comment.