SR-UKF 1.0
Square-Root Unscented Kalman Filter Library
Loading...
Searching...
No Matches
srukf.h
Go to the documentation of this file.
1#ifndef _SRUKF_H_
2#define _SRUKF_H_
3
4/**
5 * @file srukf.h
6 * @brief Square-Root Unscented Kalman Filter (SR-UKF) Library
7 *
8 * @mainpage SR-UKF: A Teaching Implementation
9 *
10 * @section intro_sec Introduction
11 *
12 * This library implements the **Square-Root Unscented Kalman Filter (SR-UKF)**,
13 * a numerically robust algorithm for state estimation in nonlinear dynamic
14 * systems. It's designed both for practical use and as a learning resource.
15 *
16 * @section motivation_sec Why Kalman Filtering?
17 *
18 * Imagine you're tracking a robot's position. You have:
19 * - A **motion model** predicting where the robot should be
20 * - **Sensor readings** telling you where sensors think it is
21 *
22 * Neither source is perfect. The motion model accumulates drift; sensors are
23 * noisy. The Kalman filter **optimally fuses** these imperfect sources,
24 * weighting each by its uncertainty. The result: an estimate better than
25 * either source alone.
26 *
27 * @section ukf_intuition_sec The Unscented Transform: Intuition
28 *
29 * The classic Kalman filter only works for **linear** systems. For nonlinear
30 * systems, we need tricks. The **Extended Kalman Filter (EKF)** linearizes
31 * via Jacobians, but this can be inaccurate and requires you to derive
32 * analytical derivatives.
33 *
34 * The **Unscented Kalman Filter (UKF)** takes a different approach:
35 *
36 * > "It's easier to approximate a probability distribution than to
37 * > approximate an arbitrary nonlinear function."
38 * > — Julier & Uhlmann
39 *
40 * Instead of linearizing the function, the UKF:
41 * 1. Picks a small set of carefully chosen **sigma points** that capture the
42 * mean and covariance of the current state estimate
43 * 2. Propagates **each sigma point** through the (possibly nonlinear) function
44 * 3. Reconstructs the mean and covariance from the transformed points
45 *
46 * This captures nonlinear effects up to 2nd order (vs 1st for EKF) without
47 * needing any Jacobians.
48 *
49 * @section sigma_intuition_sec Sigma Points: The Core Idea
50 *
51 * For an N-dimensional state, we generate \f$2N+1\f$ sigma points:
52 *
53 * \f[
54 * \begin{aligned}
55 * \chi_0 &= \bar{x} & \text{(the mean)} \\
56 * \chi_i &= \bar{x} + \gamma \cdot S_{:,i} & i = 1, \ldots, N \\
57 * \chi_{i+N} &= \bar{x} - \gamma \cdot S_{:,i} & i = 1, \ldots, N
58 * \end{aligned}
59 * \f]
60 *
61 * Where:
62 * - \f$\bar{x}\f$ is the current state estimate
63 * - \f$S\f$ is the **square-root of the covariance** (so \f$P = SS^T\f$)
64 * - \f$\gamma = \sqrt{N + \lambda}\f$ is a scaling factor
65 * - \f$\lambda = \alpha^2(N + \kappa) - N\f$ controls the spread
66 *
67 * These points form a "cloud" around the mean that captures the uncertainty
68 * ellipse. When propagated through a nonlinear function, the cloud deforms,
69 * and we can recover the new mean and covariance from the deformed cloud.
70 *
71 * @section srukf_motivation_sec Why Square-Root?
72 *
73 * The standard UKF works with the full covariance matrix \f$P\f$, but this
74 * has problems:
75 *
76 * 1. **Numerical instability**: Small numerical errors can make \f$P\f$
77 * non-positive-definite, causing the filter to fail
78 * 2. **Efficiency**: We often need \f$\sqrt{P}\f$ anyway (for sigma points)
79 *
80 * The **Square-Root UKF** works directly with \f$S\f$ where \f$P = SS^T\f$:
81 *
82 * - \f$S\f$ is always well-defined (no need to ensure positive-definiteness)
83 * - Numerical precision is effectively **doubled** (errors in \f$S\f$ become
84 * squared errors in \f$P\f$)
85 * - The Cholesky decomposition is computed only once at initialization
86 *
87 * The key operations become:
88 * - **QR decomposition** to compute square-root of sums of outer products
89 * - **Cholesky rank-1 updates/downdates** to efficiently modify \f$S\f$
90 *
91 * @section algorithm_overview_sec Algorithm Overview
92 *
93 * @subsection predict_overview Predict Step
94 *
95 * Given state \f$\hat{x}\f$ with sqrt-covariance \f$S\f$:
96 *
97 * 1. Generate sigma points from \f$(\hat{x}, S)\f$
98 * 2. Propagate each through the process model: \f$\mathcal{Y}_i = f(\chi_i)\f$
99 * 3. Compute predicted mean: \f$\bar{x}^- = \sum w^m_i \mathcal{Y}_i\f$
100 * 4. Compute deviations: \f$\mathcal{D}_i = \sqrt{|w^c_i|}(\mathcal{Y}_i -
101 * \bar{x}^-)\f$
102 * 5. Update sqrt-covariance via QR:
103 * \f$S^- = \text{qr}\left([\mathcal{D}_{1:2N}^T \;|\;
104 * Q_{sqrt}^T]\right)^T\f$
105 * 6. If \f$w^c_0 < 0\f$, apply Cholesky downdate with \f$\mathcal{D}_0\f$
106 *
107 * @subsection correct_overview Correct Step
108 *
109 * Given predicted state \f$(\bar{x}^-, S^-)\f$ and measurement \f$z\f$:
110 *
111 * 1. Generate sigma points from \f$(\bar{x}^-, S^-)\f$
112 * 2. Propagate through measurement model: \f$\mathcal{Z}_i = h(\chi_i)\f$
113 * 3. Compute predicted measurement: \f$\bar{z} = \sum w^m_i \mathcal{Z}_i\f$
114 * 4. Compute measurement sqrt-covariance \f$S_{yy}\f$ via QR (like step 4-6
115 * above)
116 * 5. Compute cross-covariance: \f$P_{xz} = \sum w^c_i (\chi_i -
117 * \bar{x}^-)(\mathcal{Z}_i - \bar{z})^T\f$
118 * 6. Compute Kalman gain: \f$K = P_{xz} S_{yy}^{-T} S_{yy}^{-1}\f$
119 * 7. Update state: \f$\hat{x} = \bar{x}^- + K(z - \bar{z})\f$
120 * 8. Update sqrt-covariance via M Cholesky downdates:
121 * \f$(S^-)^2 \leftarrow (S^-)^2 - (KS_{yy})(KS_{yy})^T\f$
122 *
123 * @section tuning_sec Tuning Parameters
124 *
125 * The UKF has three tuning parameters (\f$\alpha, \beta, \kappa\f$):
126 *
127 * | Parameter | Typical Value | Effect |
128 * |-----------|---------------|--------|
129 * | \f$\alpha\f$ | 0.001 - 1 | Controls sigma point spread. Smaller = tighter
130 * around mean | | \f$\beta\f$ | 2.0 | Prior knowledge of distribution. 2.0 is
131 * optimal for Gaussian | | \f$\kappa\f$ | 0 or 3-N | Secondary scaling. Often
132 * set to 0 or to ensure \f$N + \kappa = 3\f$ |
133 *
134 * The derived parameter \f$\lambda = \alpha^2(N + \kappa) - N\f$ determines
135 * the actual spread. For small \f$\alpha\f$, \f$\lambda \approx -N\f$, which
136 * can make \f$w^c_0\f$ negative. This library handles negative \f$w^c_0\f$
137 * correctly via Cholesky downdates.
138 *
139 * @section usage_sec Basic Usage
140 *
141 * @code{.c}
142 * // 1. Create filter (N=3 states, M=2 measurements)
143 * srukf *ukf = srukf_create(3, 2);
144 *
145 * // 2. Set noise covariances (square-roots)
146 * srukf_mat *Qsqrt = SRUKF_MAT_ALLOC(3, 3); // process noise
147 * srukf_mat *Rsqrt = SRUKF_MAT_ALLOC(2, 2); // measurement noise
148 * // ... fill in Qsqrt, Rsqrt as lower-triangular Cholesky factors
149 * srukf_set_noise(ukf, Qsqrt, Rsqrt);
150 *
151 * // 3. Initialize state
152 * srukf_reset(ukf, 1.0); // zero state, identity covariance scaled by 1.0
153 *
154 * // 4. Run filter loop
155 * while (have_data) {
156 * srukf_predict(ukf, process_model, NULL);
157 * srukf_correct(ukf, measurement, measurement_model, NULL);
158 *
159 * // Read out current estimate
160 * srukf_get_state(ukf, state_out);
161 * }
162 *
163 * // 5. Cleanup
164 * srukf_free(ukf);
165 * @endcode
166 *
167 * @section thread_safety_sec Thread Safety
168 *
169 * This library is **NOT thread-safe**:
170 * - The diagnostic callback (srukf_set_diag_callback()) is global
171 * - Each srukf instance should only be accessed from one thread
172 *
173 * @section dependencies_sec Dependencies
174 *
175 * Requires CBLAS and LAPACKE (e.g., OpenBLAS).
176 *
177 * @section attribution_sec Attribution
178 *
179 * Matrix utilities derived from LAH (Linear Algebra Helpers) by maj0e,
180 * MIT License. See https://github.com/maj0e/linear-algebra-helpers
181 *
182 * @section references_sec References
183 *
184 * - Julier, S.J. & Uhlmann, J.K. (1997). "A New Extension of the Kalman Filter
185 * to Nonlinear Systems"
186 * - Van der Merwe, R. & Wan, E.A. (2001). "The Square-Root Unscented Kalman
187 * Filter for State and Parameter-Estimation"
188 * - Wan, E.A. & Van der Merwe, R. (2000). "The Unscented Kalman Filter for
189 * Nonlinear Estimation"
190 */
191
192#include <assert.h>
193#include <stdbool.h>
194#include <stddef.h>
195#include <stdlib.h>
196#include <string.h>
197
198#include <cblas.h>
199#include <lapacke.h>
200
201/*============================================================================
202 * @defgroup matrix Matrix Types and Utilities
203 * @brief Basic matrix infrastructure for the SR-UKF
204 *
205 * The library uses a lightweight matrix type optimized for the specific
206 * needs of the Kalman filter: column-major storage (for BLAS compatibility),
207 * support for matrix views (zero-copy column extraction), and simple
208 * allocation semantics.
209 * @{
210 *============================================================================*/
211
212/**
213 * @brief Scalar index type
214 *
215 * Used for matrix dimensions and loop indices. Using size_t ensures we can
216 * handle matrices up to the platform's memory limits.
217 */
218typedef size_t srukf_index;
219
220/**
221 * @brief Scalar value type
222 *
223 * Double precision by default. Define SRUKF_SINGLE before including this
224 * header for single precision (float), which may be faster on some hardware
225 * but reduces numerical precision.
226 */
227#ifdef SRUKF_SINGLE
228typedef float srukf_value;
229#else
230typedef double srukf_value;
231#endif
232
233/**
234 * @brief Function return codes
235 *
236 * All functions that can fail return one of these codes. Check the return
237 * value and handle errors appropriately.
238 */
239typedef enum {
240 SRUKF_RETURN_OK = 0, /**< Success */
241 SRUKF_RETURN_PARAMETER_ERROR, /**< Invalid parameter (NULL, wrong dims, etc.)
242 */
243 SRUKF_RETURN_MATH_ERROR /**< Numerical failure (non-SPD matrix, etc.) */
245
246/**
247 * @brief Matrix type flags
248 *
249 * Bit flags describing matrix properties. Used internally for validation
250 * and optimization.
251 */
252typedef enum {
253 SRUKF_TYPE_COL_MAJOR = 0x01, /**< Column-major storage (always set) */
254 SRUKF_TYPE_NO_DATA = 0x02, /**< Matrix struct exists but data not allocated */
255 SRUKF_TYPE_VECTOR = 0x04, /**< Single column (n_cols == 1) */
256 SRUKF_TYPE_SQUARE = 0x08 /**< Square matrix (n_rows == n_cols) */
258
259/**
260 * @brief Matrix structure
261 *
262 * Represents a 2D matrix in column-major storage. The inc_row and inc_col
263 * fields allow representing submatrices and column views without copying data.
264 *
265 * **Memory layout (column-major):**
266 * For a 3x2 matrix, elements are stored as:
267 * @code
268 * data[0] = A(0,0) data[3] = A(0,1)
269 * data[1] = A(1,0) data[4] = A(1,1)
270 * data[2] = A(2,0) data[5] = A(2,1)
271 * @endcode
272 *
273 * Access element (i,j) as: `data[i * inc_row + j * inc_col]`
274 */
275typedef struct {
276 srukf_index n_cols; /**< Number of columns */
277 srukf_index n_rows; /**< Number of rows */
278 srukf_index inc_row; /**< Row stride (typically 1 for column-major) */
279 srukf_index inc_col; /**< Column stride (typically n_rows for column-major) */
280 srukf_value *data; /**< Pointer to data buffer */
281 srukf_mat_type type; /**< Type flags (see srukf_mat_type) */
282} srukf_mat;
283
284/**
285 * @brief Element access macro (column-major)
286 *
287 * @param A Pointer to srukf_mat
288 * @param i Row index (0-based)
289 * @param j Column index (0-based)
290 * @return Reference to element A(i,j)
291 *
292 * Example:
293 * @code
294 * SRUKF_ENTRY(mat, 0, 0) = 1.0; // Set top-left element
295 * double val = SRUKF_ENTRY(mat, 1, 2); // Read element at row 1, col 2
296 * @endcode
297 */
298#define SRUKF_ENTRY(A, i, j) \
299 ((A)->data[(i) * (A)->inc_row + (j) * (A)->inc_col])
300
301/** @brief Set a type flag on a matrix */
302#define SRUKF_SET_TYPE(A, t) ((A)->type |= (t))
303/** @brief Clear a type flag on a matrix */
304#define SRUKF_UNSET_TYPE(A, t) ((A)->type &= ~(t))
305/** @brief Test if a type flag is set */
306#define SRUKF_IS_TYPE(A, t) ((A)->type & (t))
307
308/** @brief Get leading dimension for BLAS/LAPACK (always n_rows for col-major)
309 */
310#define SRUKF_LEADING_DIM(A) ((A)->n_rows)
311
312/** @brief BLAS layout constant */
313#define SRUKF_CBLAS_LAYOUT CblasColMajor
314/** @brief LAPACK layout constant */
315#define SRUKF_LAPACK_LAYOUT LAPACK_COL_MAJOR
316
317/* BLAS/LAPACK routine selection based on precision */
318#ifdef SRUKF_SINGLE
319#define SRUKF_GEMM cblas_sgemm /**< Matrix multiply (single) */
320#define SRUKF_TRSM cblas_strsm /**< Triangular solve (single) */
321#define SRUKF_TRSV cblas_strsv /**< Triangular vector solve (single) */
322#define SRUKF_GEQRF LAPACKE_sgeqrf /**< QR factorization (single) */
323#define SRUKF_POTRF LAPACKE_spotrf /**< Cholesky factorization (single) */
324#else
325#define SRUKF_GEMM cblas_dgemm /**< Matrix multiply (double) */
326#define SRUKF_TRSM cblas_dtrsm /**< Triangular solve (double) */
327#define SRUKF_TRSV cblas_dtrsv /**< Triangular vector solve (double) */
328#define SRUKF_GEQRF LAPACKE_dgeqrf /**< QR factorization (double) */
329#define SRUKF_POTRF LAPACKE_dpotrf /**< Cholesky factorization (double) */
330#endif
331
332/**
333 * @brief Allocate a matrix
334 *
335 * Creates a new matrix with the specified dimensions. Memory is
336 * zero-initialized.
337 *
338 * @param rows Number of rows
339 * @param cols Number of columns
340 * @param alloc_data If non-zero, allocate data buffer; otherwise data is NULL
341 * @return Allocated matrix, or NULL on failure
342 *
343 * @note Use srukf_mat_free() to release memory when done.
344 */
345srukf_mat *srukf_mat_alloc(srukf_index rows, srukf_index cols, int alloc_data);
346
347/**
348 * @brief Free a matrix and its data
349 *
350 * Releases all memory associated with the matrix. Safe to call with NULL.
351 *
352 * @param mat Matrix to free (may be NULL)
353 */
354void srukf_mat_free(srukf_mat *mat);
355
356/** @brief Allocate a matrix with data buffer */
357#define SRUKF_MAT_ALLOC(rows, cols) srukf_mat_alloc((rows), (cols), 1)
358/** @brief Allocate a matrix struct only (data pointer will be NULL) */
359#define SRUKF_MAT_ALLOC_NO_DATA(rows, cols) srukf_mat_alloc((rows), (cols), 0)
360
361/** @} */ /* end of matrix group */
362
363/*============================================================================
364 * @defgroup filter SR-UKF Filter
365 * @brief The main Square-Root Unscented Kalman Filter interface
366 * @{
367 *============================================================================*/
368
369/**
370 * @brief Opaque workspace structure
371 *
372 * Pre-allocated temporaries to avoid malloc during predict/correct.
373 * Allocated on demand, freed with filter or explicitly.
374 */
376
377/**
378 * @brief Square-Root Unscented Kalman Filter
379 *
380 * This structure holds the complete state of an SR-UKF instance:
381 * - Current state estimate and its sqrt-covariance
382 * - Noise covariances (as square-roots)
383 * - UKF tuning parameters and derived weights
384 * - Pre-allocated workspace for efficiency
385 *
386 * **Key insight:** We store \f$S\f$ where \f$P = SS^T\f$, not \f$P\f$ itself.
387 * This is the "square-root" in SR-UKF, providing numerical stability.
388 *
389 * @see srukf_create, srukf_create_from_noise
390 */
391typedef struct {
392 /** @name State Estimate
393 * The current best estimate of the system state
394 * @{
395 */
396 srukf_mat *x; /**< State estimate vector (N x 1) */
397 srukf_mat *S; /**< Sqrt of state covariance (N x N, lower triangular).
398 Satisfies \f$P = SS^T\f$ where P is the covariance. */
399 /** @} */
400
401 /** @name Noise Covariances
402 * Process and measurement noise, stored as square-roots
403 * @{
404 */
405 srukf_mat *Qsqrt; /**< Sqrt of process noise covariance (N x N).
406 Models uncertainty in the state transition.
407 Larger values = less trust in the model. */
408 srukf_mat *Rsqrt; /**< Sqrt of measurement noise covariance (M x M).
409 Models sensor noise.
410 Larger values = less trust in measurements. */
411 /** @} */
412
413 /** @name UKF Parameters
414 * Tuning parameters controlling sigma point distribution
415 * @{
416 */
417 srukf_value alpha; /**< Spread of sigma points (typically 1e-3 to 1).
418 Smaller values keep points closer to the mean. */
419 srukf_value beta; /**< Prior distribution knowledge (2.0 for Gaussian).
420 Affects the zeroth covariance weight. */
421 srukf_value kappa; /**< Secondary scaling (typically 0 or 3-N).
422 Can be used to ensure semi-positive definiteness. */
423 srukf_value lambda; /**< Computed: \f$\alpha^2 (N + \kappa) - N\f$.
424 Determines actual sigma point spread. */
425 /** @} */
426
427 /** @name Sigma Point Weights
428 * Weights for reconstructing mean and covariance from sigma points
429 * @{
430 */
431 srukf_value *wm; /**< Mean weights (2N+1 elements).
432 \f$w^m_0 = \frac{\lambda}{N+\lambda}\f$,
433 \f$w^m_i = \frac{1}{2(N+\lambda)}\f$ for i>0 */
434 srukf_value *wc; /**< Covariance weights (2N+1 elements).
435 \f$w^c_0 = w^m_0 + (1 - \alpha^2 + \beta)\f$,
436 \f$w^c_i = w^m_i\f$ for i>0.
437 Note: \f$w^c_0\f$ can be negative for small alpha! */
438 /** @} */
439
440 srukf_workspace *ws; /**< Pre-allocated workspace (allocated on demand) */
441} srukf;
442
443/*----------------------------------------------------------------------------
444 * @defgroup diagnostics Diagnostics
445 * @brief Diagnostic and debugging support
446 * @{
447 *----------------------------------------------------------------------------*/
448
449/**
450 * @brief Diagnostic callback function type
451 *
452 * User-provided function to receive diagnostic messages from the library.
453 * Useful for debugging filter issues.
454 *
455 * @param msg Null-terminated diagnostic message
456 */
457typedef void (*srukf_diag_fn)(const char *msg);
458
459/**
460 * @brief Set the global diagnostic callback
461 *
462 * When set, the library will call this function with diagnostic messages
463 * about errors and warnings (e.g., "QR factorization failed").
464 *
465 * @param fn Callback function, or NULL to disable diagnostics
466 *
467 * @note This is a global setting affecting all filter instances.
468 *
469 * Example:
470 * @code
471 * void my_diag(const char *msg) {
472 * fprintf(stderr, "SR-UKF: %s\n", msg);
473 * }
474 * srukf_set_diag_callback(my_diag);
475 * @endcode
476 */
478
479/** @} */ /* end of diagnostics group */
480
481/*----------------------------------------------------------------------------
482 * @defgroup lifecycle Creation and Destruction
483 * @brief Filter lifecycle management
484 * @{
485 *----------------------------------------------------------------------------*/
486
487/**
488 * @brief Create a filter from noise covariance matrices
489 *
490 * Creates and initializes a filter with the given noise characteristics.
491 * The state dimension N is inferred from Qsqrt, measurement dimension M
492 * from Rsqrt.
493 *
494 * @param Qsqrt Process noise sqrt-covariance (N x N, lower triangular)
495 * @param Rsqrt Measurement noise sqrt-covariance (M x M, lower triangular)
496 * @return Allocated filter, or NULL on failure
497 *
498 * @note Both matrices must be square. They are copied, so the originals
499 * can be freed after this call.
500 *
501 * @see srukf_create, srukf_free
502 */
503srukf *srukf_create_from_noise(const srukf_mat *Qsqrt, const srukf_mat *Rsqrt);
504
505/**
506 * @brief Create a filter with uninitialized noise matrices
507 *
508 * Creates a filter with the specified dimensions but without setting
509 * noise covariances. You must call srukf_set_noise() before using
510 * predict/correct.
511 *
512 * @param N State dimension (must be > 0)
513 * @param M Measurement dimension (must be > 0)
514 * @return Allocated filter, or NULL on failure
515 *
516 * This two-step initialization is useful when noise parameters are
517 * computed or loaded separately from filter creation.
518 *
519 * @see srukf_set_noise, srukf_free
520 */
521srukf *srukf_create(int N, int M);
522
523/**
524 * @brief Free all memory allocated for the filter
525 *
526 * Releases the filter struct, all matrices, weight vectors, and workspace.
527 * Safe to call with NULL.
528 *
529 * @param ukf Filter to free (may be NULL)
530 */
531void srukf_free(srukf *ukf);
532
533/** @} */ /* end of lifecycle group */
534
535/*----------------------------------------------------------------------------
536 * @defgroup initialization Initialization
537 * @brief Setting filter parameters and initial conditions
538 * @{
539 *----------------------------------------------------------------------------*/
540
541/**
542 * @brief Set the noise sqrt-covariance matrices
543 *
544 * Updates the process and measurement noise covariances. This affects
545 * how the filter trades off between trusting the model vs measurements.
546 *
547 * @param ukf Filter instance
548 * @param Qsqrt Process noise sqrt-covariance (N x N)
549 * @param Rsqrt Measurement noise sqrt-covariance (M x M)
550 * @return SRUKF_RETURN_OK on success
551 *
552 * **Intuition:**
553 * - Larger Qsqrt = "my model is unreliable" = trust measurements more
554 * - Larger Rsqrt = "my sensors are noisy" = trust model more
555 *
556 * @note The matrices are copied. Originals can be freed after this call.
557 * @note On failure, the existing noise matrices are preserved.
558 */
560 const srukf_mat *Rsqrt);
561
562/**
563 * @brief Set the UKF scaling parameters
564 *
565 * Configures the sigma point distribution parameters. These affect how
566 * the filter captures nonlinear effects.
567 *
568 * @param ukf Filter instance
569 * @param alpha Spread of sigma points around mean (must be > 0, typically 1e-3
570 * to 1)
571 * @param beta Prior knowledge of distribution (2.0 is optimal for Gaussian)
572 * @param kappa Secondary scaling parameter (typically 0 or 3-N)
573 * @return SRUKF_RETURN_OK on success, SRUKF_RETURN_PARAMETER_ERROR if alpha <=
574 * 0
575 *
576 * **Derived parameters:**
577 * - \f$\lambda = \alpha^2 (N + \kappa) - N\f$
578 * - \f$\gamma = \sqrt{N + \lambda}\f$ (sigma point spread factor)
579 *
580 * **Weight formulas:**
581 * - \f$w^m_0 = \lambda / (N + \lambda)\f$
582 * - \f$w^m_i = 1 / (2(N + \lambda))\f$ for i > 0
583 * - \f$w^c_0 = w^m_0 + (1 - \alpha^2 + \beta)\f$
584 * - \f$w^c_i = w^m_i\f$ for i > 0
585 *
586 * @note For very small alpha, \f$w^c_0\f$ can become negative. This library
587 * handles this correctly via Cholesky downdates.
588 */
590 srukf_value kappa);
591
592/** @} */ /* end of initialization group */
593
594/*----------------------------------------------------------------------------
595 * @defgroup accessors State Accessors
596 * @brief Reading and writing filter state
597 * @{
598 *----------------------------------------------------------------------------*/
599
600/**
601 * @brief Get the state dimension N
602 * @param ukf Filter instance
603 * @return State dimension, or 0 if ukf is NULL/invalid
604 */
606
607/**
608 * @brief Get the measurement dimension M
609 * @param ukf Filter instance
610 * @return Measurement dimension, or 0 if ukf is NULL/invalid
611 */
613
614/**
615 * @brief Get the current state estimate
616 *
617 * Copies the state vector to a user-provided buffer.
618 *
619 * @param ukf Filter instance
620 * @param x_out Output buffer (N x 1), must be pre-allocated
621 * @return SRUKF_RETURN_OK on success
622 */
623srukf_return srukf_get_state(const srukf *ukf, srukf_mat *x_out);
624
625/**
626 * @brief Set the state estimate
627 *
628 * Overwrites the current state with user-provided values.
629 *
630 * @param ukf Filter instance
631 * @param x_in New state vector (N x 1)
632 * @return SRUKF_RETURN_OK on success
633 *
634 * @note This does not update the covariance. Use srukf_set_sqrt_cov()
635 * or srukf_reset() to also set uncertainty.
636 */
638
639/**
640 * @brief Get the sqrt-covariance matrix
641 *
642 * Copies S where \f$P = SS^T\f$ is the state covariance.
643 *
644 * @param ukf Filter instance
645 * @param S_out Output buffer (N x N), must be pre-allocated
646 * @return SRUKF_RETURN_OK on success
647 *
648 * To get the full covariance P, compute \f$P = S \cdot S^T\f$.
649 */
651
652/**
653 * @brief Set the sqrt-covariance matrix
654 *
655 * @param ukf Filter instance
656 * @param S_in New sqrt-covariance (N x N, should be lower triangular)
657 * @return SRUKF_RETURN_OK on success
658 *
659 * @warning S_in should be a valid Cholesky factor (lower triangular,
660 * positive diagonal). Invalid values may cause filter divergence.
661 */
663
664/**
665 * @brief Reset filter to initial conditions
666 *
667 * Sets state to zero and sqrt-covariance to a scaled identity matrix.
668 * Useful for reinitializing a filter without reallocating.
669 *
670 * @param ukf Filter instance
671 * @param init_std Initial standard deviation (> 0). The sqrt-covariance
672 * is set to `init_std * I`.
673 * @return SRUKF_RETURN_OK on success
674 *
675 * **Interpretation:** After reset, each state variable has zero mean and
676 * variance `init_std^2`, with no correlation between variables.
677 */
679
680/** @} */ /* end of accessors group */
681
682/*----------------------------------------------------------------------------
683 * @defgroup operations Core Operations
684 * @brief The main predict and correct steps
685 * @{
686 *----------------------------------------------------------------------------*/
687
688/**
689 * @brief Process model callback type
690 *
691 * User-provided function implementing the state transition model:
692 * \f$x_{k+1} = f(x_k)\f$
693 *
694 * @param x_in Current state (N x 1), read-only
695 * @param x_out Next state (N x 1), write output here
696 * @param user User data pointer (passed through from predict call)
697 *
698 * Example (constant velocity model):
699 * @code
700 * void process_model(const srukf_mat *x_in, srukf_mat *x_out, void *user) {
701 * double dt = *(double*)user;
702 * // State: [position, velocity]
703 * SRUKF_ENTRY(x_out, 0, 0) = SRUKF_ENTRY(x_in, 0, 0)
704 * + dt * SRUKF_ENTRY(x_in, 1, 0);
705 * SRUKF_ENTRY(x_out, 1, 0) = SRUKF_ENTRY(x_in, 1, 0); // velocity
706 * unchanged
707 * }
708 * @endcode
709 */
710
711/**
712 * @brief Measurement model callback type
713 *
714 * User-provided function implementing the measurement model:
715 * \f$z = h(x)\f$
716 *
717 * @param x_in Current state (N x 1), read-only
718 * @param z_out Predicted measurement (M x 1), write output here
719 * @param user User data pointer (passed through from correct call)
720 *
721 * Example (observe position only):
722 * @code
723 * void meas_model(const srukf_mat *x_in, srukf_mat *z_out, void *user) {
724 * (void)user;
725 * // We only measure position (first state variable)
726 * SRUKF_ENTRY(z_out, 0, 0) = SRUKF_ENTRY(x_in, 0, 0);
727 * }
728 * @endcode
729 */
730
731/**
732 * @brief Predict step: propagate state through process model
733 *
734 * Advances the state estimate forward in time using the process model.
735 * This increases uncertainty (covariance grows due to process noise).
736 *
737 * @param ukf Filter instance
738 * @param f Process model function \f$x_{k+1} = f(x_k)\f$
739 * @param user User data passed to f
740 * @return SRUKF_RETURN_OK on success. On error, filter state is unchanged.
741 *
742 * **Algorithm:**
743 * 1. Generate 2N+1 sigma points from current (x, S)
744 * 2. Propagate each sigma point through f
745 * 3. Compute new mean as weighted average of propagated points
746 * 4. Compute new S via QR decomposition of weighted deviations + Qsqrt
747 *
748 * @see srukf_correct
749 */
751 void (*f)(const srukf_mat *, srukf_mat *, void *),
752 void *user);
753
754/**
755 * @brief Correct step: incorporate measurement
756 *
757 * Updates the state estimate using a new measurement. This decreases
758 * uncertainty (measurement reduces our ignorance about the state).
759 *
760 * @param ukf Filter instance
761 * @param z Measurement vector (M x 1)
762 * @param h Measurement model function \f$z = h(x)\f$
763 * @param user User data passed to h
764 * @return SRUKF_RETURN_OK on success. On error, filter state is unchanged.
765 *
766 * **Algorithm:**
767 * 1. Generate sigma points from predicted state
768 * 2. Propagate through measurement model h
769 * 3. Compute predicted measurement mean and sqrt-covariance Syy
770 * 4. Compute cross-covariance Pxz between state and measurement
771 * 5. Compute Kalman gain K = Pxz * inv(Syy' * Syy)
772 * 6. Update state: x = x + K * (z - z_predicted)
773 * 7. Update S via Cholesky downdates: S^2 -= (K*Syy)(K*Syy)'
774 *
775 * **Intuition:** The Kalman gain K determines how much to trust the
776 * measurement vs the prediction. If Rsqrt is large (noisy sensor), K is
777 * small and we mostly keep our prediction. If Syy is large (uncertain
778 * prediction), K is large and we trust the measurement more.
779 *
780 * @see srukf_predict
781 */
783 void (*h)(const srukf_mat *, srukf_mat *, void *),
784 void *user);
785
786/** @} */ /* end of operations group */
787
788/*----------------------------------------------------------------------------
789 * @defgroup transactional Transactional Operations
790 * @brief Advanced: operate on external state buffers
791 * @{
792 *----------------------------------------------------------------------------*/
793
794/**
795 * @brief Transactional predict: operate on external buffers
796 *
797 * Like srukf_predict(), but reads/writes state from user-provided buffers
798 * instead of the filter's internal state. Useful for:
799 * - Implementing particle filters (multiple hypotheses)
800 * - What-if analysis without modifying the filter
801 * - Custom rollback/checkpoint schemes
802 *
803 * @param ukf Filter instance (provides parameters and workspace)
804 * @param x State vector (N x 1), modified in-place
805 * @param S Sqrt-covariance (N x N), modified in-place
806 * @param f Process model function
807 * @param user User data passed to f
808 * @return SRUKF_RETURN_OK on success
809 */
811 void (*f)(const srukf_mat *, srukf_mat *, void *),
812 void *user);
813
814/**
815 * @brief Transactional correct: operate on external buffers
816 *
817 * Like srukf_correct(), but reads/writes state from user-provided buffers.
818 *
819 * @param ukf Filter instance (provides parameters and workspace)
820 * @param x State vector (N x 1), modified in-place
821 * @param S Sqrt-covariance (N x N), modified in-place
822 * @param z Measurement vector (M x 1)
823 * @param h Measurement model function
824 * @param user User data passed to h
825 * @return SRUKF_RETURN_OK on success
826 */
828 srukf_mat *z,
829 void (*h)(const srukf_mat *, srukf_mat *, void *),
830 void *user);
831
832/** @} */ /* end of transactional group */
833
834/*----------------------------------------------------------------------------
835 * @defgroup workspace Workspace Management
836 * @brief Control over internal memory allocation
837 * @{
838 *----------------------------------------------------------------------------*/
839
840/**
841 * @brief Pre-allocate workspace
842 *
843 * The workspace holds temporary matrices used during predict/correct.
844 * By default, it's allocated on first use. Call this to allocate it
845 * explicitly (e.g., during initialization to avoid allocation during
846 * real-time operation).
847 *
848 * @param ukf Filter instance
849 * @return SRUKF_RETURN_OK on success
850 *
851 * @note Workspace size depends on N and M. If these change (via
852 * srukf_set_noise with different dimensions), workspace is
853 * reallocated automatically.
854 */
856
857/**
858 * @brief Free workspace to reclaim memory
859 *
860 * Releases the workspace memory. The next predict/correct call will
861 * reallocate it. Useful for long-running applications where the filter
862 * is dormant for extended periods.
863 *
864 * @param ukf Filter instance (may be NULL)
865 *
866 * @note Called automatically by srukf_free().
867 */
868void srukf_free_workspace(srukf *ukf);
869
870/** @} */ /* end of workspace group */
871
872#endif /* _SRUKF_H_ */
srukf_index srukf_state_dim(const srukf *ukf)
Get the state dimension N.
Definition srukf.c:562
srukf_index srukf_meas_dim(const srukf *ukf)
Get the measurement dimension M.
Definition srukf.c:568
srukf_return srukf_set_noise(srukf *ukf, const srukf_mat *Qsqrt, const srukf_mat *Rsqrt)
Set the noise sqrt-covariance matrices.
Definition srukf.c:756
struct srukf_workspace srukf_workspace
Opaque workspace structure.
Definition srukf.h:375
srukf * srukf_create(int N, int M)
Create a filter with uninitialized noise matrices.
Definition srukf.c:714
srukf_return srukf_predict_to(srukf *ukf, srukf_mat *x, srukf_mat *S, void(*f)(const srukf_mat *, srukf_mat *, void *), void *user)
Transactional predict: operate on external buffers.
Definition srukf.c:1492
srukf * srukf_create_from_noise(const srukf_mat *Qsqrt, const srukf_mat *Rsqrt)
Create a filter from noise covariance matrices.
Definition srukf.c:734
double srukf_value
Scalar value type.
Definition srukf.h:230
void srukf_free(srukf *ukf)
Free all memory allocated for the filter.
Definition srukf.c:530
void(* srukf_diag_fn)(const char *msg)
Diagnostic callback function type.
Definition srukf.h:457
srukf_return srukf_correct_to(srukf *ukf, srukf_mat *x, srukf_mat *S, srukf_mat *z, void(*h)(const srukf_mat *, srukf_mat *, void *), void *user)
Transactional correct: operate on external buffers.
Definition srukf.c:1790
srukf_mat_type
Matrix type flags.
Definition srukf.h:252
@ SRUKF_TYPE_COL_MAJOR
Definition srukf.h:253
@ SRUKF_TYPE_NO_DATA
Definition srukf.h:254
@ SRUKF_TYPE_SQUARE
Definition srukf.h:256
@ SRUKF_TYPE_VECTOR
Definition srukf.h:255
srukf_return srukf_predict(srukf *ukf, void(*f)(const srukf_mat *, srukf_mat *, void *), void *user)
Process model callback type.
Definition srukf.c:1515
srukf_return srukf_alloc_workspace(srukf *ukf)
Pre-allocate workspace.
Definition srukf.c:278
srukf_return srukf_reset(srukf *ukf, srukf_value init_std)
Reset filter to initial conditions.
Definition srukf.c:624
srukf_return srukf_set_sqrt_cov(srukf *ukf, const srukf_mat *S_in)
Set the sqrt-covariance matrix.
Definition srukf.c:612
srukf_return srukf_get_sqrt_cov(const srukf *ukf, srukf_mat *S_out)
Get the sqrt-covariance matrix.
Definition srukf.c:600
srukf_return srukf_get_state(const srukf *ukf, srukf_mat *x_out)
Get the current state estimate.
Definition srukf.c:576
void srukf_set_diag_callback(srukf_diag_fn fn)
Set the global diagnostic callback.
Definition srukf.c:125
void srukf_mat_free(srukf_mat *mat)
Free a matrix and its data.
Definition srukf.c:105
srukf_return srukf_set_state(srukf *ukf, const srukf_mat *x_in)
Set the state estimate.
Definition srukf.c:588
void srukf_free_workspace(srukf *ukf)
Free workspace to reclaim memory.
Definition srukf.c:240
size_t srukf_index
Scalar index type.
Definition srukf.h:218
srukf_return
Function return codes.
Definition srukf.h:239
@ SRUKF_RETURN_OK
Definition srukf.h:240
@ SRUKF_RETURN_MATH_ERROR
Definition srukf.h:243
@ SRUKF_RETURN_PARAMETER_ERROR
Definition srukf.h:241
srukf_mat * srukf_mat_alloc(srukf_index rows, srukf_index cols, int alloc_data)
Allocate a matrix.
Definition srukf.c:75
srukf_return srukf_set_scale(srukf *ukf, srukf_value alpha, srukf_value beta, srukf_value kappa)
Set the UKF scaling parameters.
Definition srukf.c:494
srukf_return srukf_correct(srukf *ukf, srukf_mat *z, void(*h)(const srukf_mat *, srukf_mat *, void *), void *user)
Correct step: incorporate measurement.
Definition srukf.c:1817
Matrix structure.
Definition srukf.h:275
srukf_index n_rows
Definition srukf.h:277
srukf_value * data
Definition srukf.h:280
srukf_index inc_col
Definition srukf.h:279
srukf_index inc_row
Definition srukf.h:278
srukf_mat_type type
Definition srukf.h:281
srukf_index n_cols
Definition srukf.h:276
Square-Root Unscented Kalman Filter.
Definition srukf.h:391
srukf_value beta
Definition srukf.h:419
srukf_workspace * ws
Definition srukf.h:440
srukf_value kappa
Definition srukf.h:421
srukf_mat * Qsqrt
Definition srukf.h:405
srukf_mat * x
Definition srukf.h:396
srukf_value alpha
Definition srukf.h:417
srukf_mat * Rsqrt
Definition srukf.h:408
srukf_value * wc
Definition srukf.h:434
srukf_value lambda
Definition srukf.h:423
srukf_mat * S
Definition srukf.h:397
srukf_value * wm
Definition srukf.h:431