diff --git a/Makefile b/Makefile index 9d482efec0516a41f36d9edd8acdfe69b2bfc794..58fa33dadce988c17325a20f4b84b252a09c764c 100644 --- a/Makefile +++ b/Makefile @@ -128,6 +128,7 @@ CSRC = $(ALLCSRC) \ $(SRCDIR)/benchmarks/blind_abstraction.c \ $(SRCDIR)/benchmarks/observer_abstraction.c \ $(SRCDIR)/benchmarks/luenberger_observer.c \ + $(SRCDIR)/benchmarks/kalman_filter.c \ $(SRCDIR)/main.c \ $(SRCDIR)/random.c \ $(SRCDIR)/serial.c \ diff --git a/doc/references.bib b/doc/references.bib index 62b5c0d4f21eb9849be06931c3f8d3983308a129..0fd73af8600983d0bde23ba4ff94a72e88be6e80 100644 --- a/doc/references.bib +++ b/doc/references.bib @@ -18,3 +18,12 @@ volume = {5}, doi = {10/gq5wdm}, } + +@inproceedings{MFI15-Noack, + author = {Noack, Benjamin and Baum, Marcus and Hanebeck, Uwe D.}, + booktitle = {Proceedings of the IEEE International Conference on Multisensor Fusion and Integration for Intelligent Systems (MFI)}, + title = {{State estimation for ellipsoidally constrained dynamic systems with set-membership pseudo measurements}}, + year = {2015}, + pages = {297--302}, + doi = {10/gq5wfg}, +} diff --git a/scripts/benchmarks/kalman_filter.py b/scripts/benchmarks/kalman_filter.py new file mode 100644 index 0000000000000000000000000000000000000000..6f0d05bba5b1d5d32bb62ca0795425eb56b533ec --- /dev/null +++ b/scripts/benchmarks/kalman_filter.py @@ -0,0 +1,123 @@ +## This file is part of the execution-time evaluation for the qronos observer abstractions. +## Copyright (C) 2022-2023 Tim Rheinfels <tim.rheinfels@fau.de> +## See https://gitlab.cs.fau.de/qronos-state-abstractions/execution-time +## +## This program is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program. If not, see <https://www.gnu.org/licenses/>. + +### +### @file benchmarks/kalman_filter.py +### +### @brief Provides @p kalman_filter.KalmanFilter, the @p benchmark.Benchmark +### subclass for the Kalman filter benchmark +### +### @author Tim Rheinfels <tim.rheinfels@fau.de> +### + +import json +import numpy as np +import scipy as sp +import scipy.linalg + +from benchmark import Benchmark +from data import decode + +### +### @brief Encapsulates the blind abstraction execution time benchmark +### +class KalmanFilter(Benchmark): + + ### + ### @brief Validates the computations performed on the STM32 by running them + ### again in numpy and comparing the results. + ### + ### @see benchmark.Benchmark._validate for parameters + ### + def _validate(self, benchmark, key): + n_y = int(key) + + for run in benchmark['tests']: + A = decode(run['setup']['A']) + C = decode(run['setup']['C']) + W = decode(run['setup']['W']) + V = decode(run['setup']['V']) + x = decode(run['setup']['x']) + X = decode(run['setup']['X']) + y = decode(run['setup']['y']) + + if n_y > 0: + assert(A.shape == (n_y, n_y)) + assert(C.shape == (n_y, n_y)) + assert(W.shape == (n_y, n_y)) + assert(np.allclose(W, W.T) and np.all(np.linalg.eigvalsh(W) > 0.0)) + assert(X.shape == (n_y, n_y)) + assert(np.allclose(X, X.T) and np.all(np.linalg.eigvalsh(X) > 0.0)) + else: + assert(A.size == 0) + assert(C.size == 0) + assert(W.size == 0) + assert(X.size == 0) + A = np.zeros((0, 0)) + C = np.zeros((0, 0)) + W = np.zeros((0, 0)) + X = np.zeros((0, 0)) + + assert(x.shape == (n_y,)) + assert(y.shape == (n_y,)) + + for i in range(benchmark['repetition_count']): + # Predict + x = A @ x + X = A @ X @ A.T + delta = np.sqrt(np.trace(X) / np.trace(W)) + X = (1.0 + 1.0 / delta) * X + (1.0 + delta) * W + + # Compute gain + omega = 0.5 + S = (1.0 / omega) * C @ X @ C.T + (1.0 / (1.0 - omega)) * V + K = (1.0 / omega) * X @ C.T + K = sp.linalg.solve(S.T, K.T, assume_a='pos').T + IKC = np.eye(K.shape[0]) - K @ C + + # Update + x = IKC @ x + K @ y + X = (1.0 / omega) * IKC @ X @ IKC.T + (1.0 / (1.0 - omega)) * K @ V @ K.T + + x_p = decode(run['teardown']['x_p']) + X_p = decode(run['teardown']['X_p']) + + # + # Up until solving the linear system the values identical. The small + # floating point error introduced then amplifies over the iterations + # as inverses are numerically unstable. + # The relatively high value for atol practically disables the check + # for very small absolute values. We argue that since the absolute + # values of X are ranging roughly from 1e-4 to 3e2, there are plenty + # of values validated due to the small rtol of 1e-6. + # + # See warning and notes on + # https://numpy.org/doc/1.24/reference/generated/numpy.isclose.html#numpy.isclose + # + # TL;DR: The inversion introduces floating point inaccuracies which is + # why the comparision with standard values fails here + # + assert(np.allclose(x, x_p, atol=3e-4, rtol=1e-6)) + assert(np.allclose(X, X_p, atol=3e-4, rtol=1e-6)) + + ### + ### @brief Constructor + ### + ### @see benchmark.Benchmark.__init__ for parameters + ### + def __init__(self, data): + Benchmark.__init__(self, data, 'Kalman Filter', r'kalman_filter_([0-9]+)', lambda m: m.group(1)) diff --git a/src/benchmarks.c b/src/benchmarks.c index 47f43ef94484d7a9c5c58a603b6e64572370b6a9..8a065bd85a61a32bbaf0e6cbf686fdeba8c3510d 100644 --- a/src/benchmarks.c +++ b/src/benchmarks.c @@ -30,6 +30,7 @@ #include <benchmarks/blind_abstraction.h> #include <benchmarks/observer_abstraction.h> #include <benchmarks/luenberger_observer.h> +#include <benchmarks/kalman_filter.h> void benchmarks_run(void) { @@ -45,5 +46,6 @@ void benchmarks_run(void) benchmarks_run_blind_abstraction(); benchmarks_run_observer_abstraction(); benchmarks_run_luenberger_observer(); + benchmarks_run_kalman_filter(); } diff --git a/src/benchmarks/kalman_filter.c b/src/benchmarks/kalman_filter.c new file mode 100644 index 0000000000000000000000000000000000000000..4395362724dbea55deff9d5fcd74072ffe9d18bc --- /dev/null +++ b/src/benchmarks/kalman_filter.c @@ -0,0 +1,253 @@ +// This file is part of the execution-time evaluation for the qronos observer abstractions. +// Copyright (C) 2022-2023 Tim Rheinfels <tim.rheinfels@fau.de> +// See https://gitlab.cs.fau.de/qronos-state-abstractions/execution-time +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +/// +/// @file benchmarks/kalman_filter.c +/// +/// @brief Provides the implementation of the Kalman filter execution time benchmark +/// +/// @author Tim Rheinfels <tim.rheinfels@fau.de> +/// + +#include "kalman_filter.h" + +#include <stdlib.h> +#include <string.h> + +#include <lapack.h> +#include <linalg.h> +#include <random.h> +#include <serial.h> +#include <benchmarks/campaign.h> + +static float *A; ///< System dynamics matrix +static float *C; ///< System measurement matrix +static float *W; ///< Process disturbance ellipsoid's shape matrix +static float *V; ///< Measurement disturbance ellipsoid's shape matrix +static float *S; ///< Intermediate matrix +static float *K; ///< Intermediate matrix (Kalman gains) + +static float *x; ///< Ellipsoid's center vector +static float *x_p; ///< Memory for transforming the ellipsoid's center vector +static float *X; ///< Ellipsoid's shape matrix +static float *X_p; ///< Memory for transforming the ellipsoid's shape matrix +static float *y; ///< Measurement vector + +static float trace_W; ///< Trace of the process disturbance ellipsoid's shape matrix + +/// +/// @brief Configures the memory layout and randomly initializes the data +/// +static void setup_matrices(size_t n_y) +{ + A = benchmarks_memory; + C = A + n_y*n_y; + W = C + n_y*n_y; + V = W + n_y*n_y; + S = V + n_y*n_y; + K = S + n_y*n_y; + x = K + n_y*n_y; + x_p = x + n_y; + X = x_p + n_y; + X_p = X + n_y*n_y; + y = X_p + n_y*n_y; + + random_matrix(A, n_y, n_y); + random_matrix(C, n_y, n_y); + random_matrix_spd(W, n_y); + random_matrix_spd(V, n_y); + random_matrix(x, n_y, 1u); + random_matrix_spd(X, n_y); + random_matrix(y, n_y, 1u); + memset(S, 0x00u, n_y * n_y * sizeof(float)); + memset(K, 0x00u, n_y * n_y * sizeof(float)); + memset(x_p, 0x00u, n_y * sizeof(float)); + memset(X_p, 0x00u, n_y * n_y * sizeof(float)); + + LINALG_TRACE(W, trace_W, n_y); + + serial_print_matrix("A", A, n_y, n_y); + serial_print_matrix("C", C, n_y, n_y); + serial_print_matrix("W", W, n_y, n_y); + serial_print_matrix("V", V, n_y, n_y); + serial_print_vector("x", x, n_y); + serial_print_matrix("X", X, n_y, n_y); + serial_print_vector("y", y, n_y); +} + +void benchmarks_run_kalman_filter(void) +{ + + #define SETUP(N_Y) { \ + setup_matrices(N_Y); \ + } + + #define TEARDOWN(N_Y) { \ + serial_print_vector("x_p", x, N_Y); \ + serial_print_matrix("X_p", X, N_Y, N_Y); \ + } + + #define CAMPAIGN(N_Y) { \ + BENCHMARKS_CAMPAIGN("kalman_filter_" #N_Y, \ + { \ + /* === Prediction step === */ \ + \ + /* x_p <- A x */ \ + LINALG_MV(A, x, x_p, 1.0f, 0.0f, N_Y, N_Y); \ + \ + /* X_p <- A X */ \ + LINALG_MM(A, X, X_p, 1.0f, 0.0f, N_Y, N_Y, N_Y); \ + \ + /* X <- X_p A' */ \ + LINALG_MM_T(X_p, A, X, 1.0f, 0.0f, N_Y, N_Y, N_Y); \ + \ + /* Compute delta optimally */ \ + float trace_X; \ + LINALG_TRACE(X, trace_X, N_Y); \ + float delta = sqrtf(trace_X / trace_W); \ + \ + /* X <- (1 + 1/delta) X + (1 + delta) W */ \ + LINALG_MA(X, W, X, 1.0f + 1.0f/delta, 1.0f + delta, 0.0f, N_Y, N_Y); \ + \ + /* === Compute Kalman Gain === */ \ + \ + /* Select omega arbitrarily but fixed (should actually be optimised for) */ \ + float omega = 0.5f; \ + \ + /* Copy V to S */ \ + memcpy(S, V, N_Y*N_Y*sizeof(float)); \ + \ + /* X_p <- (1/omega) * C X */ \ + LINALG_MM(C, X, X_p, 1.0f / omega, 0.0f, N_Y, N_Y, N_Y); \ + \ + /* K <- X_p' (transpose since BLAS and LAPACK use column major order) */ \ + for(size_t i = 0u; i < N_Y; ++i) { \ + for(size_t j = 0u; j < N_Y; ++j) { \ + K[i * N_Y + j] = X_p[j * N_Y + i]; \ + } \ + } \ + \ + /* S <- X_p C' + (1 / (1 - omega)) S */ \ + LINALG_MM_T(X_p, C, S, 1.0f, 1.0f / (1.0f - omega), N_Y, N_Y, N_Y); \ + \ + /* Solve K <- S\K */ \ + sposv('U', N_Y, N_Y, S, N_Y, K, N_Y); \ + \ + /* === Update step === */ \ + \ + /* x <- K y */ \ + LINALG_MV(K, y, x, 1.0f, 0.0f, N_Y, N_Y); \ + \ + /* S <- V K */ \ + LINALG_MM_T(V, K, S, 1.0f, 0.0f, N_Y, N_Y, N_Y); \ + \ + /* X_p <- K' S */ \ + LINALG_MM(K, S, X_p, 1.0f, 0.0f, N_Y, N_Y, N_Y); \ + \ + /* Initialise S with identity */ \ + for(size_t i = 0u; i < N_Y; ++i) { \ + for(size_t j = 0; j < N_Y; ++j) { \ + S[i * N_Y + j] = (i == j) ? 1.0f : 0.0f; \ + } \ + } \ + \ + /* S <- S - K C */ \ + LINALG_MM(K, C, S, -1.0f, 1.0f, N_Y, N_Y, N_Y); \ + \ + /* K <- S X */ \ + LINALG_MM(S, X, K, 1.0f, 0.0f, N_Y, N_Y, N_Y); \ + \ + /* X_p <- (1 / omega) K S' + (1 / (1-omega)) X_p */ \ + LINALG_MM_T(K, S, X_p, 1.0f / omega, 1.0f / (1.0f - omega), N_Y, N_Y, N_Y); \ + \ + /* Swap X and X_p */ \ + float *tmp = X; \ + X = X_p; \ + X_p = tmp; \ + \ + /* x <- S x_p + x */ \ + LINALG_MV(S, x_p, x, 1.0f, 1.0f, N_Y, N_Y); \ + }, \ + SETUP(N_Y), \ + TEARDOWN(N_Y), \ + 10u, \ + 10u \ + ) \ + } + + CAMPAIGN(1); + CAMPAIGN(2); + CAMPAIGN(3); + CAMPAIGN(4); + CAMPAIGN(5); + CAMPAIGN(6); + CAMPAIGN(7); + CAMPAIGN(8); + CAMPAIGN(9); + CAMPAIGN(10); + CAMPAIGN(11); + CAMPAIGN(12); + CAMPAIGN(13); + CAMPAIGN(14); + CAMPAIGN(15); + CAMPAIGN(16); + CAMPAIGN(17); + CAMPAIGN(18); + CAMPAIGN(19); + CAMPAIGN(20); + CAMPAIGN(21); + CAMPAIGN(22); + CAMPAIGN(23); + CAMPAIGN(24); + CAMPAIGN(25); + CAMPAIGN(26); + CAMPAIGN(27); + CAMPAIGN(28); + CAMPAIGN(29); + CAMPAIGN(30); + CAMPAIGN(31); + CAMPAIGN(32); + CAMPAIGN(33); + CAMPAIGN(34); + CAMPAIGN(35); + CAMPAIGN(36); + CAMPAIGN(37); + CAMPAIGN(38); + CAMPAIGN(39); + CAMPAIGN(40); + CAMPAIGN(41); + CAMPAIGN(42); + CAMPAIGN(43); + CAMPAIGN(44); + CAMPAIGN(45); + CAMPAIGN(46); + CAMPAIGN(47); + CAMPAIGN(48); + CAMPAIGN(49); + CAMPAIGN(50); + CAMPAIGN(51); + CAMPAIGN(52); + CAMPAIGN(53); + CAMPAIGN(54); + CAMPAIGN(55); + CAMPAIGN(56); + CAMPAIGN(57); + CAMPAIGN(58); + CAMPAIGN(59); + CAMPAIGN(60); + +} diff --git a/src/benchmarks/kalman_filter.h b/src/benchmarks/kalman_filter.h new file mode 100644 index 0000000000000000000000000000000000000000..4a041e765bf2627395131b756aa1c1d60eda7a43 --- /dev/null +++ b/src/benchmarks/kalman_filter.h @@ -0,0 +1,57 @@ +// This file is part of the execution-time evaluation for the qronos observer abstractions. +// Copyright (C) 2022-2023 Tim Rheinfels <tim.rheinfels@fau.de> +// See https://gitlab.cs.fau.de/qronos-state-abstractions/execution-time +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +/// +/// @file benchmarks/kalman_filter.h +/// +/// @brief Provides the interface for the Kalman filter execution time benchmark +/// +/// @author Tim Rheinfels <tim.rheinfels@fau.de> +/// + +#ifndef BENCHMARKS_KALMANFILTER_H +#define BENCHMARKS_KALMANFILTER_H + +#include <benchmarks/memory.h> + +/// +/// @brief Memory required for running the Kalman filter execution time benchmark +/// +#define BENCHMARKS_KALMAN_FILTER_WORK_SIZE (8u*N_Y_MAX*N_Y_MAX + 3u*N_Y_MAX) + +/// +/// @brief Runs the Kalman filter execution time benchmark +/// +/// This function measures the execution time needed in order to update the +/// interval Kalman filter proposed in @cite MFI15-Noack. We omit all +/// probabilistic terms (the filter can handle both set-valued and probabilistic +/// constraints) and apply some simplifcations: +/// - Use fixed value for omega in equations (12) and (13c). This should greatly +/// reduce the execution time as K needs to be evaluated by equation (12) only +/// once. Further, this allows for a constant-time algorithm. +/// - Don't compute the inverse in equation (12) explicitly but solve the linear +/// system obtained after transposition instead. For the latter, we use sposv +/// from LAPACK which makes use of the fact that the matrix to be inverted is +/// s.p.d. thus allowing for a constant-time algorithm (compute Cholesky +/// factorization and solve triangular system). +/// - Avoid copying in memory data where possible. +/// There are surely some further optimizations but we are certain that our results +/// signify the increased cost as compared to the other benchmarks well. +/// +void benchmarks_run_kalman_filter(void); + +#endif // BENCHMARKS_KALMANFILTER_H diff --git a/src/benchmarks/memory.c b/src/benchmarks/memory.c index caaa789f5523b865c17971fe5d0bdfa3e40227a2..aae06236b0f07d4994da9903112caaf467d746b7 100644 --- a/src/benchmarks/memory.c +++ b/src/benchmarks/memory.c @@ -31,6 +31,7 @@ #include <benchmarks/blind_abstraction.h> #include <benchmarks/observer_abstraction.h> #include <benchmarks/luenberger_observer.h> +#include <benchmarks/kalman_filter.h> /// /// @brief Size of the padding in bytes used for checking memory violations @@ -53,7 +54,8 @@ static float work[2u * PADDING + MAX(BENCHMARKS_BLIND_ABSTRACTION_WORK_SIZE, MAX(BENCHMARKS_OBSERVER_ABSTRACTION_WORK_SIZE, - BENCHMARKS_LUENBERGER_OBSERVER_WORK_SIZE)) + MAX(BENCHMARKS_LUENBERGER_OBSERVER_WORK_SIZE, + BENCHMARKS_KALMAN_FILTER_WORK_SIZE))) ]; float * const benchmarks_memory = work + PADDING;