SR-UKF 1.0
Square-Root Unscented Kalman Filter Library
Loading...
Searching...
No Matches
srukf.c
Go to the documentation of this file.
1/**
2 * @file srukf.c
3 * @brief Square-Root Unscented Kalman Filter Implementation
4 *
5 * This file contains the complete implementation of the SR-UKF algorithm.
6 * The code is organized into several sections:
7 *
8 * 1. **Matrix utilities** - Basic allocation and helper functions
9 * 2. **Workspace management** - Pre-allocated temporaries for efficiency
10 * 3. **Weight computation** - UKF sigma point weights
11 * 4. **Sigma point generation** - Creating the 2N+1 sample points
12 * 5. **SR-UKF core operations** - QR-based covariance updates, Cholesky
13 * downdates
14 * 6. **Predict/Correct** - The main filter operations
15 *
16 * @section impl_numerical Numerical Considerations
17 *
18 * The SR-UKF differs from the standard UKF in how covariance is maintained:
19 *
20 * **Standard UKF:**
21 * @code
22 * P = sum(wc[i] * (X[i] - x_mean) * (X[i] - x_mean)') + Q
23 * @endcode
24 * This requires ensuring P remains positive-definite, which can fail
25 * due to numerical errors.
26 *
27 * **Square-Root UKF:**
28 * @code
29 * S = qr([sqrt(wc) * (X - x_mean)' ; Qsqrt'])'
30 * @endcode
31 * We never form P explicitly. Instead, we maintain S where P = S*S'.
32 * The QR decomposition guarantees S is a valid Cholesky factor.
33 *
34 * @section impl_negative_w Handling Negative Weights
35 *
36 * For small alpha (< 1), the zeroth covariance weight wc[0] can be negative:
37 * @code
38 * wc[0] = lambda/(N+lambda) + (1 - alpha^2 + beta)
39 * @endcode
40 *
41 * If lambda ≈ -N (which happens for small alpha), wc[0] becomes negative.
42 * We handle this by:
43 * 1. Excluding the zeroth deviation from the QR (which computes S² = sum of
44 * squares)
45 * 2. Applying a Cholesky rank-1 downdate: S² → S² - dev0 * dev0'
46 *
47 * The downdate uses Givens rotations for numerical stability.
48 */
49
50#include <assert.h>
51#include <math.h>
52#include <stdbool.h>
53#include <stdio.h>
54#include <stdlib.h>
55#include <string.h>
56
57#include "srukf.h"
58
59/** @brief Default sigma point spread parameter */
60#define DEFAULT_ALPHA 1e-3
61/** @brief Default prior distribution parameter (optimal for Gaussian) */
62#define DEFAULT_BETA 2.0
63/** @brief Default secondary scaling parameter */
64#define DEFAULT_KAPPA 1.0
65/** @brief Numerical tolerance for near-zero checks */
66#define SRUKF_EPS 1e-12
67
68/*============================================================================
69 * @internal
70 * @defgroup impl_matrix Matrix Allocation
71 * @brief Internal matrix utilities (derived from LAH by maj0e, MIT License)
72 * @{
73 *============================================================================*/
74
75srukf_mat *srukf_mat_alloc(srukf_index rows, srukf_index cols, int alloc_data) {
76 srukf_mat *mat = (srukf_mat *)calloc(1, sizeof(srukf_mat));
77 if (!mat)
78 return NULL;
79
80 mat->n_rows = rows;
81 mat->n_cols = cols;
82 mat->inc_row = 1; /* column-major */
83 mat->inc_col = rows;
85
86 if (cols == 1)
88 if (rows == cols)
90
91 if (alloc_data) {
92 mat->data = (srukf_value *)calloc(rows * cols, sizeof(srukf_value));
93 if (!mat->data) {
94 free(mat);
95 return NULL;
96 }
97 } else {
98 mat->data = NULL;
100 }
101
102 return mat;
103}
104
106 if (!mat)
107 return;
109 free(mat->data);
110 free(mat);
111}
112
113/** @} */ /* end impl_matrix */
114
115/*============================================================================
116 * @internal
117 * @defgroup impl_diag Diagnostics
118 * @brief Internal diagnostic support
119 * @{
120 *============================================================================*/
121
122/** Global diagnostic callback (NULL = disabled) */
124
128
129/**
130 * @brief Report a diagnostic message
131 *
132 * If a diagnostic callback is registered, invokes it with the message.
133 * Used throughout the implementation to report errors and warnings.
134 *
135 * @param msg Message to report
136 */
137static void diag_report(const char *msg) {
138 if (g_diag_callback)
139 g_diag_callback(msg);
140}
141
142/** @} */ /* end impl_diag */
143
144/*============================================================================
145 * Forward Declarations
146 *============================================================================*/
147
148/* Core sigma point operations */
149static srukf_return
151 void (*func)(const srukf_mat *, srukf_mat *, void *),
152 void *user);
153static srukf_return
154compute_cross_covariance(const srukf_mat *Xsig, const srukf_mat *Ysig,
155 const srukf_mat *x_mean, const srukf_mat *y_mean,
156 const srukf_value *weights, srukf_mat *Pxz);
157
158/* SR-UKF specific functions */
160 srukf_value *work);
162 const srukf_mat *mean,
163 const srukf_value *wc,
164 srukf_mat *Dev);
165
166/*============================================================================
167 * @internal
168 * @defgroup impl_workspace Workspace Management
169 * @brief Pre-allocated temporaries for zero-allocation filter operation
170 *
171 * The workspace contains all temporary matrices and buffers needed by
172 * predict() and correct(). By pre-allocating these, we avoid malloc/free
173 * during filter operation, which is critical for real-time applications.
174 *
175 * **Memory layout:**
176 * - Matrices for sigma point storage: Xsig, Ysig_N, Ysig_M
177 * - Mean vectors: x_pred, y_mean
178 * - Covariance temporaries: S_tmp, P_pred, Pyy, Pxz, etc.
179 * - SR-UKF specific: Dev_N, Dev_M, qr_work_*, Syy
180 * - Small buffers: tau (QR), downdate_work, dev0 (for negative wc[0])
181 *
182 * @{
183 *============================================================================*/
184/**
185 * @brief Pre-allocated workspace for filter operations
186 *
187 * Contains all temporary storage needed by predict() and correct().
188 * Sized for specific (N, M) dimensions; reallocated if dimensions change.
189 */
190struct srukf_workspace {
191 srukf_index N; /**< State dimension this workspace was allocated for */
192 srukf_index M; /**< Measurement dimension */
193
194 /** @name Predict Temporaries
195 * @{ */
196 srukf_mat *Xsig; /**< N x (2N+1) - sigma points before propagation */
197 srukf_mat *Ysig_N; /**< N x (2N+1) - sigma points after process model */
198 srukf_mat *x_pred; /**< N x 1 - predicted state mean */
199 srukf_mat *S_tmp; /**< N x N - temporary for atomic update */
200 /** @} */
201
202 /** @name Correct Temporaries
203 * @{ */
204 srukf_mat *Ysig_M; /**< M x (2N+1) - sigma points in measurement space */
205 srukf_mat *y_mean; /**< M x 1 - predicted measurement mean */
206 srukf_mat *Pxz; /**< N x M - cross-covariance between state and measurement */
207 srukf_mat *K; /**< N x M - Kalman gain */
208 srukf_mat *innov; /**< M x 1 - innovation (z - z_predicted) */
209 srukf_mat *x_new; /**< N x 1 - updated state (for atomic update) */
210 srukf_mat *S_new; /**< N x N - updated sqrt-covariance (for atomic update) */
211 srukf_mat *dx; /**< N x 1 - state correction K * innovation */
212 srukf_mat *tmp1; /**< N x M - temp for K * Syy product */
213 /** @} */
214
215 /** @name SR-UKF Specific
216 * These are the key matrices for the square-root formulation
217 * @{ */
218 srukf_mat *Dev_N; /**< N x (2N+1) - weighted deviations sqrt(|wc|)*(Y-mean)
219 for predict */
220 srukf_mat *Dev_M; /**< M x (2N+1) - weighted deviations for correct */
221 srukf_mat *qr_work_N; /**< (2N+1+N) x N - compound matrix for QR in predict */
222 srukf_mat *qr_work_M; /**< (2N+1+M) x M - compound matrix for QR in correct */
223 srukf_mat *Syy; /**< M x M - measurement sqrt-covariance (lower triangular) */
224 /** @} */
225
226 /** @name Small Buffers
227 * Avoid malloc in hot path
228 * @{ */
229 srukf_value *tau_N; /**< QR householder scalars for predict (N elements) */
230 srukf_value *tau_M; /**< QR householder scalars for correct (M elements) */
232 *downdate_work; /**< Cholesky downdate scratch (max(N,M) elements) */
233 srukf_value *dev0_N; /**< First deviation column for predict downdate */
234 srukf_value *dev0_M; /**< First deviation column for correct downdate */
235 /** @} */
236};
237
238/** @} */ /* end impl_workspace definition */
239
241 if (!ukf || !ukf->ws)
242 return;
243
244 srukf_workspace *ws = ukf->ws;
245
246 /* Free all matrices */
247 srukf_mat_free(ws->Xsig);
248 srukf_mat_free(ws->Ysig_N);
249 srukf_mat_free(ws->x_pred);
250 srukf_mat_free(ws->S_tmp);
251 srukf_mat_free(ws->Ysig_M);
252 srukf_mat_free(ws->y_mean);
253 srukf_mat_free(ws->Pxz);
254 srukf_mat_free(ws->K);
255 srukf_mat_free(ws->innov);
256 srukf_mat_free(ws->x_new);
257 srukf_mat_free(ws->S_new);
258 srukf_mat_free(ws->dx);
259 srukf_mat_free(ws->tmp1);
260 srukf_mat_free(ws->Dev_N);
261 srukf_mat_free(ws->Dev_M);
262 srukf_mat_free(ws->qr_work_N);
263 srukf_mat_free(ws->qr_work_M);
264 srukf_mat_free(ws->Syy);
265
266 /* Free pre-allocated buffers */
267 free(ws->tau_N);
268 free(ws->tau_M);
269 free(ws->downdate_work);
270 free(ws->dev0_N);
271 free(ws->dev0_M);
272
273 free(ws);
274 ukf->ws = NULL;
275}
276
277/* Allocate workspace for given dimensions */
279 if (!ukf)
281
284 if (N == 0 || M == 0)
286
287 /* If workspace exists and dimensions match, nothing to do */
288 if (ukf->ws && ukf->ws->N == N && ukf->ws->M == M)
289 return SRUKF_RETURN_OK;
290
291 /* Free existing workspace if dimensions changed */
292 if (ukf->ws)
294
295 srukf_index n_sigma = 2 * N + 1;
296
297 /* Allocate workspace struct */
298 srukf_workspace *ws = (srukf_workspace *)calloc(1, sizeof(srukf_workspace));
299 if (!ws)
301
302 ws->N = N;
303 ws->M = M;
304
305 /* Allocate all matrices */
306 ws->Xsig = SRUKF_MAT_ALLOC(N, n_sigma);
307 ws->Ysig_N = SRUKF_MAT_ALLOC(N, n_sigma);
308 ws->x_pred = SRUKF_MAT_ALLOC(N, 1);
309 ws->S_tmp = SRUKF_MAT_ALLOC(N, N);
310 ws->Ysig_M = SRUKF_MAT_ALLOC(M, n_sigma);
311 ws->y_mean = SRUKF_MAT_ALLOC(M, 1);
312 ws->Pxz = SRUKF_MAT_ALLOC(N, M);
313 ws->K = SRUKF_MAT_ALLOC(N, M);
314 ws->innov = SRUKF_MAT_ALLOC(M, 1);
315 ws->x_new = SRUKF_MAT_ALLOC(N, 1);
316 ws->S_new = SRUKF_MAT_ALLOC(N, N);
317 ws->dx = SRUKF_MAT_ALLOC(N, 1);
318 ws->tmp1 = SRUKF_MAT_ALLOC(N, M);
319
320 /* SR-UKF specific matrices */
321 ws->Dev_N = SRUKF_MAT_ALLOC(N, n_sigma);
322 ws->Dev_M = SRUKF_MAT_ALLOC(M, n_sigma);
323 /* QR workspace: (n_sigma + dim) x dim, need extra row for safety */
324 ws->qr_work_N = SRUKF_MAT_ALLOC(n_sigma + N + 1, N);
325 ws->qr_work_M = SRUKF_MAT_ALLOC(n_sigma + M + 1, M);
326 ws->Syy = SRUKF_MAT_ALLOC(M, M);
327
328 /* Pre-allocated buffers for hot path (avoid malloc in predict/correct) */
329 ws->tau_N = (srukf_value *)calloc(N, sizeof(srukf_value));
330 ws->tau_M = (srukf_value *)calloc(M, sizeof(srukf_value));
331 srukf_index max_dim = (N > M) ? N : M;
332 ws->downdate_work = (srukf_value *)calloc(max_dim, sizeof(srukf_value));
333 ws->dev0_N = (srukf_value *)calloc(N, sizeof(srukf_value));
334 ws->dev0_M = (srukf_value *)calloc(M, sizeof(srukf_value));
335
336 /* Check all allocations succeeded */
337 if (!ws->Xsig || !ws->Ysig_N || !ws->x_pred || !ws->S_tmp || !ws->Ysig_M ||
338 !ws->y_mean || !ws->Pxz || !ws->K || !ws->innov || !ws->x_new ||
339 !ws->S_new || !ws->dx || !ws->tmp1 || !ws->Dev_N || !ws->Dev_M ||
340 !ws->qr_work_N || !ws->qr_work_M || !ws->Syy || !ws->tau_N ||
341 !ws->tau_M || !ws->downdate_work || !ws->dev0_N || !ws->dev0_M) {
342 ukf->ws = ws; /* Temporarily assign so free_workspace can clean up */
345 }
346
347 ukf->ws = ws;
348 return SRUKF_RETURN_OK;
349}
350
351/**
352 * @brief Ensure workspace is allocated (lazy allocation)
353 *
354 * Checks if workspace exists and matches current dimensions.
355 * Allocates or reallocates as needed.
356 *
357 * @param ukf Filter instance
358 * @return SRUKF_RETURN_OK if workspace is ready
359 */
361 if (ukf->ws) {
362 /* Check dimensions still match */
365 if (ukf->ws->N == N && ukf->ws->M == M)
366 return SRUKF_RETURN_OK;
367 }
368 return srukf_alloc_workspace(ukf);
369}
370
371/** @} */ /* end impl_workspace */
372
373/*============================================================================
374 * @internal
375 * @defgroup impl_helpers Helper Functions
376 * @brief Various utility functions
377 * @{
378 *============================================================================*/
379
380/**
381 * @brief Check if matrix contains any NaN or Inf values
382 *
383 * Used to validate callback outputs. If a process or measurement model
384 * produces non-finite values, we detect it here and return an error
385 * rather than letting garbage propagate through the filter.
386 *
387 * @param M Matrix to check
388 * @return true if all values are finite, false if any NaN/Inf found
389 */
390static bool is_numeric_valid(const srukf_mat *M) {
391 if (!M || !M->data)
392 return false;
393 for (srukf_index j = 0; j < M->n_cols; ++j)
394 for (srukf_index i = 0; i < M->n_rows; ++i) {
395 srukf_value v = SRUKF_ENTRY(M, i, j);
396 if (isnan(v) || isinf(v))
397 return false;
398 }
399 return true;
400}
401
402/**
403 * @brief Allocate a vector of doubles
404 * @param vec Output pointer
405 * @param len Number of elements
406 * @return SRUKF_RETURN_OK on success
407 */
409 *vec = (srukf_value *)calloc(len, sizeof(srukf_value));
411}
412
413/** @} */ /* end impl_helpers */
414
415/*============================================================================
416 * @internal
417 * @defgroup impl_weights Weight Computation
418 * @brief UKF sigma point weights
419 *
420 * The UKF uses weighted averages to reconstruct mean and covariance from
421 * sigma points. There are two sets of weights:
422 *
423 * - **Mean weights (wm):** Used to compute weighted mean
424 * - **Covariance weights (wc):** Used to compute weighted covariance
425 *
426 * The weights sum to 1 for the mean, but wc[0] can differ from wm[0]
427 * to better capture higher-order moments of the distribution.
428 *
429 * @{
430 *============================================================================*/
431
432/**
433 * @brief Compute sigma point weights
434 *
435 * Given the current scaling parameters (alpha, beta, kappa, lambda),
436 * computes the 2N+1 weights for mean and covariance calculations.
437 *
438 * **Weight formulas:**
439 * @code
440 * wm[0] = lambda / (N + lambda)
441 * wm[i] = 1 / (2 * (N + lambda)) for i = 1..2N
442 *
443 * wc[0] = wm[0] + (1 - alpha^2 + beta)
444 * wc[i] = wm[i] for i = 1..2N
445 * @endcode
446 *
447 * **Note on negative wc[0]:**
448 * For small alpha, lambda approaches -N, making wm[0] large and negative.
449 * Adding (1 - alpha^2 + beta) may not compensate enough, leaving wc[0] < 0.
450 * This is handled correctly in the QR-based covariance computation.
451 *
452 * @param ukf Filter with alpha/beta/kappa/lambda set
453 * @param n State dimension N
454 * @return SRUKF_RETURN_OK on success
455 */
457 srukf_index n_sigma = 2 * n + 1;
458
459 /* Allocate weight vectors if needed */
460 if (!ukf->wm) {
461 if (alloc_vector(&ukf->wm, n_sigma) != SRUKF_RETURN_OK)
463 }
464 if (!ukf->wc) {
465 if (alloc_vector(&ukf->wc, n_sigma) != SRUKF_RETURN_OK) {
466 free(ukf->wm);
467 ukf->wm = NULL;
469 }
470 }
471
472 /* Common denominator for all weights */
473 const srukf_value denom = (srukf_value)n + ukf->lambda;
474 if (fabs(denom) < SRUKF_EPS) { /* safeguard against division by zero */
475 diag_report("compute_weights: denominator too small (n + lambda ≈ 0)");
477 }
478
479 /* Mean weights: wm[0] = λ / (n+λ), wm[i>0] = 1/(2(n+λ)) */
480 for (srukf_index i = 0; i < n_sigma; ++i)
481 ukf->wm[i] = 1.0 / (2.0 * denom);
482 ukf->wm[0] = ukf->lambda / denom;
483
484 /* Covariance weights: wc[0] = wm[0] + (1-α²+β), wc[i>0] = wm[i] */
485 for (srukf_index i = 0; i < n_sigma; ++i)
486 ukf->wc[i] = ukf->wm[i];
487 ukf->wc[0] += (1.0 - ukf->alpha * ukf->alpha + ukf->beta);
488
489 return SRUKF_RETURN_OK;
490}
491
492/** @} */ /* end impl_weights */
493
495 srukf_value kappa) {
496 if (!ukf || !ukf->x)
497 return SRUKF_RETURN_PARAMETER_ERROR; /* filter or state not yet allocated */
498
499 if (alpha <= 0.0)
500 return SRUKF_RETURN_PARAMETER_ERROR; /* α must be positive */
501
502 /* ----------------- compute λ --------------------------------- */
503 srukf_index n = ukf->x->n_rows; /* state dimension */
504 srukf_value lambda =
505 alpha * alpha * ((srukf_value)n + kappa) - (srukf_value)n;
506
507 /* --------- guard against λ ≈ –n ------------------------------ */
508 if (fabs((double)(n + lambda)) < SRUKF_EPS) {
509 /* Cannot recompute α if (n + κ) ≈ 0 (would divide by zero) */
510 if (fabs((double)n + kappa) < SRUKF_EPS)
512 /* clamp λ to –n + ε */
513 lambda = -(srukf_value)n + SRUKF_EPS;
514 /* recompute α from the clamped λ (λ = α²(n+κ) – n) */
515 srukf_value a = sqrt((lambda + (srukf_value)n) / ((srukf_value)n + kappa));
516 return srukf_set_scale(ukf, a, beta, kappa);
517 } else {
518 /* --------- store the (possibly adjusted) parameters ------------- */
519 ukf->alpha = alpha;
520 ukf->beta = beta;
521 ukf->kappa = kappa;
522 ukf->lambda = lambda;
523
524 /* --------- recompute mean & covariance weights -------------- */
525 return srukf_compute_weights(ukf, n);
526 }
527}
528
529/* Free all memory allocated for the filter. */
530void srukf_free(srukf *ukf) {
531 if (!ukf)
532 return;
533
534 /* Free workspace if allocated */
536
537 /* Free all internal matrices (they own their data) */
538 if (ukf->x)
539 srukf_mat_free(ukf->x);
540 if (ukf->S)
541 srukf_mat_free(ukf->S);
542 if (ukf->Qsqrt)
543 srukf_mat_free(ukf->Qsqrt);
544 if (ukf->Rsqrt)
545 srukf_mat_free(ukf->Rsqrt);
546
547 /* Free weight vectors if they were allocated */
548 if (ukf->wm) {
549 free(ukf->wm);
550 ukf->wm = NULL;
551 }
552 if (ukf->wc) {
553 free(ukf->wc);
554 ukf->wc = NULL;
555 }
556
557 /* Finally free the filter struct itself */
558 free(ukf);
559}
560
561/*-------------------- Dimension accessors ----------------------------*/
563 if (!ukf || !ukf->x)
564 return 0;
565 return ukf->x->n_rows;
566}
567
569 if (!ukf || !ukf->Rsqrt)
570 return 0;
571 return ukf->Rsqrt->n_rows;
572}
573
574/*-------------------- State accessors --------------------------------*/
575
577 if (!ukf || !ukf->x || !x_out)
579
580 srukf_index N = ukf->x->n_rows;
581 if (x_out->n_rows != N || x_out->n_cols != 1)
583
584 memcpy(x_out->data, ukf->x->data, N * sizeof(srukf_value));
585 return SRUKF_RETURN_OK;
586}
587
589 if (!ukf || !ukf->x || !x_in)
591
592 srukf_index N = ukf->x->n_rows;
593 if (x_in->n_rows != N || x_in->n_cols != 1)
595
596 memcpy(ukf->x->data, x_in->data, N * sizeof(srukf_value));
597 return SRUKF_RETURN_OK;
598}
599
601 if (!ukf || !ukf->S || !S_out)
603
604 srukf_index N = ukf->S->n_rows;
605 if (S_out->n_rows != N || S_out->n_cols != N)
607
608 memcpy(S_out->data, ukf->S->data, N * N * sizeof(srukf_value));
609 return SRUKF_RETURN_OK;
610}
611
613 if (!ukf || !ukf->S || !S_in)
615
616 srukf_index N = ukf->S->n_rows;
617 if (S_in->n_rows != N || S_in->n_cols != N)
619
620 memcpy(ukf->S->data, S_in->data, N * N * sizeof(srukf_value));
621 return SRUKF_RETURN_OK;
622}
623
625 if (!ukf || !ukf->x || !ukf->S)
627
628 if (init_std <= 0.0)
630
631 srukf_index N = ukf->x->n_rows;
632
633 /* Zero the state vector */
634 memset(ukf->x->data, 0, N * sizeof(srukf_value));
635
636 /* Set S to diagonal matrix with init_std on diagonal */
637 for (srukf_index i = 0; i < N; ++i)
638 for (srukf_index j = 0; j < N; ++j)
639 SRUKF_ENTRY(ukf->S, i, j) = (i == j) ? init_std : 0.0;
640
641 return SRUKF_RETURN_OK;
642}
643
644/* ------------------------------------------------------------------ */
645/* Shared internal initialisation routine – used by both `create()` */
646/* and `create_from_noise()` to avoid code duplication. */
647/* ------------------------------------------------------------------ */
648static srukf_return srukf_init(srukf *ukf, int N /* states */,
649 int M /* measurements */,
650 const srukf_mat *Qsqrt_src,
651 const srukf_mat *Rsqrt_src) {
652 if (!ukf)
654
655 /* ----------------- State vector --------------------------------- */
656 ukf->x = SRUKF_MAT_ALLOC(N, 1);
657 if (!ukf->x)
660
661 /* ----------------- State covariance square‑root ----------------- */
662 ukf->S = SRUKF_MAT_ALLOC(N, N);
663 if (!ukf->S) {
664 srukf_mat_free(ukf->x);
666 }
668 /* initialize only the diagonal so that correction
669 * can occur before prediction */
670 for (srukf_index i = 0; i < (srukf_index)N; ++i)
671 for (srukf_index j = 0; j < (srukf_index)N; ++j)
672 SRUKF_ENTRY(ukf->S, i, j) = (i == j) ? 0.001 : 0.0;
673
674 /* ---------- Process‑noise ---------- */
675 if (Qsqrt_src) {
676 ukf->Qsqrt = SRUKF_MAT_ALLOC(N, N);
677 for (srukf_index j = 0; j < (srukf_index)N; ++j)
678 for (srukf_index i = 0; i < (srukf_index)N; ++i)
679 SRUKF_ENTRY(ukf->Qsqrt, i, j) = SRUKF_ENTRY(Qsqrt_src, i, j);
680 } else {
681 ukf->Qsqrt = SRUKF_MAT_ALLOC_NO_DATA(N, N);
682 }
683 if (!ukf->Qsqrt) {
684 srukf_mat_free(ukf->x);
685 srukf_mat_free(ukf->S);
687 }
689
690 /* ---------- Measurement‑noise ---------- */
691 if (Rsqrt_src) {
692 ukf->Rsqrt = SRUKF_MAT_ALLOC(M, M);
693 for (srukf_index j = 0; j < (srukf_index)M; ++j)
694 for (srukf_index i = 0; i < (srukf_index)M; ++i)
695 SRUKF_ENTRY(ukf->Rsqrt, i, j) = SRUKF_ENTRY(Rsqrt_src, i, j);
696 } else {
697 ukf->Rsqrt = SRUKF_MAT_ALLOC_NO_DATA(M, M);
698 }
699 if (!ukf->Rsqrt) {
700 srukf_mat_free(ukf->x);
701 srukf_mat_free(ukf->S);
702 srukf_mat_free(ukf->Qsqrt);
704 }
706
707 /* ----------------- Default scaling -------------------------------- */
709}
710
711/* ------------------------------------------------------------------ */
712/* srukf_create – create a filter with uninitialised noise matrices */
713/* ------------------------------------------------------------------ */
714srukf *srukf_create(int N /* states */, int M /* measurements */) {
715 /* Validate dimensions */
716 if (N <= 0 || M <= 0)
717 return NULL;
718
719 srukf *ukf = (srukf *)calloc(1, sizeof(srukf));
720 if (!ukf)
721 return NULL; /* out‑of‑memory */
722
723 /* initialise all internal data (noise matrices left empty) */
724 if (srukf_init(ukf, N, M, NULL, NULL) != SRUKF_RETURN_OK) {
725 srukf_free(ukf);
726 return NULL;
727 }
728 return ukf;
729}
730
731/* ------------------------------------------------------------------ */
732/* srukf_create_from_noise – create a filter from supplied noise */
733/* ------------------------------------------------------------------ */
734srukf *srukf_create_from_noise(const srukf_mat *Qsqrt, const srukf_mat *Rsqrt) {
735 if (!Qsqrt || !Rsqrt)
736 return NULL;
737
738 /* Dimensions must agree */
739 if (Qsqrt->n_rows != Qsqrt->n_cols || Rsqrt->n_rows != Rsqrt->n_cols)
740 return NULL;
741
742 srukf *ukf = (srukf *)calloc(1, sizeof(srukf));
743 if (!ukf)
744 return NULL;
745
746 /* initialize all internal data and copy the supplied noise matrices */
747 if (srukf_init(ukf, (int)Qsqrt->n_rows, (int)Rsqrt->n_rows, Qsqrt, Rsqrt) !=
749 srukf_free(ukf);
750 return NULL;
751 }
752 return ukf;
753}
754
755/* Set the filter's noise square‑root covariance matrices. */
757 const srukf_mat *Rsqrt) {
758 /* Basic checks */
759 if (!ukf || !Qsqrt || !Rsqrt)
761
762 /* Ensure that the filter is well-formed. */
763 if (!ukf->x)
765 if (!ukf->Qsqrt || !ukf->Rsqrt)
767
768 /* State dimension N and measurement dimension M */
769 srukf_index N = ukf->x->n_rows; /* x is N×1 */
770 srukf_index M = ukf->Rsqrt->n_rows; /* previously allocated M×M */
771
772 /* Check dimensions of the supplied matrices */
773 if (Qsqrt->n_rows != N || Qsqrt->n_cols != N)
775 if (Rsqrt->n_rows != M || Rsqrt->n_cols != M)
777
778 /* Allocate new matrices BEFORE freeing old ones.
779 * This ensures filter state is preserved if allocation fails. */
780 srukf_mat *new_Qsqrt = SRUKF_MAT_ALLOC(N, N);
781 srukf_mat *new_Rsqrt = SRUKF_MAT_ALLOC(M, M);
782 if (!new_Qsqrt || !new_Rsqrt) {
783 /* On failure, clean up any partial allocation and preserve old matrices */
784 if (new_Qsqrt)
785 srukf_mat_free(new_Qsqrt);
786 if (new_Rsqrt)
787 srukf_mat_free(new_Rsqrt);
789 }
790
791 /* Copy data element‑wise (matrix is stored column‑major) */
792 for (srukf_index j = 0; j < N; ++j)
793 for (srukf_index i = 0; i < N; ++i)
794 SRUKF_ENTRY(new_Qsqrt, i, j) = SRUKF_ENTRY(Qsqrt, i, j);
795
796 for (srukf_index j = 0; j < M; ++j)
797 for (srukf_index i = 0; i < M; ++i)
798 SRUKF_ENTRY(new_Rsqrt, i, j) = SRUKF_ENTRY(Rsqrt, i, j);
799
800 /* Set appropriate type flags */
803
804 /* Now safe to free old matrices and assign new ones */
805 srukf_mat_free(ukf->Qsqrt);
806 srukf_mat_free(ukf->Rsqrt);
807 ukf->Qsqrt = new_Qsqrt;
808 ukf->Rsqrt = new_Rsqrt;
809
810 return SRUKF_RETURN_OK;
811}
812
813/*============================================================================
814 * @internal
815 * @defgroup impl_sigma Sigma Point Generation
816 * @brief Creating the 2N+1 sigma points that capture mean and covariance
817 *
818 * Sigma points are the core insight of the Unscented Transform. Instead of
819 * linearizing a nonlinear function (like EKF does), we:
820 *
821 * 1. Choose 2N+1 carefully placed sample points around the mean
822 * 2. Propagate each through the nonlinear function exactly
823 * 3. Reconstruct statistics from the transformed samples
824 *
825 * The sigma points form a symmetric pattern: the mean, plus N points
826 * offset by +gamma * column_of_S, plus N points offset by -gamma * column_of_S.
827 * This pattern exactly matches the first two moments (mean and covariance)
828 * of the original distribution.
829 *
830 * @{
831 *============================================================================*/
832
833/**
834 * @brief Generate sigma points from state and sqrt-covariance
835 *
836 * Creates 2N+1 sigma points arranged symmetrically around the mean:
837 *
838 * @code
839 * chi[0] = x (the mean)
840 * chi[i] = x + gamma * S(:,i) for i = 1..N
841 * chi[i+N] = x - gamma * S(:,i) for i = 1..N
842 * @endcode
843 *
844 * where gamma = sqrt(N + lambda) is the spread factor.
845 *
846 * **Geometric interpretation:**
847 * If P = S*S' is the covariance, then the sigma points lie on an ellipsoid
848 * centered at x, scaled by gamma. The columns of S define the principal
849 * axes of this ellipsoid.
850 *
851 * **Why use S instead of P?**
852 * We need sqrt(P) to generate sigma points. In standard UKF, we'd compute
853 * chol(P) every time. In SR-UKF, we already have S, saving a Cholesky
854 * decomposition per step.
855 *
856 * @param x State vector (N x 1)
857 * @param S Sqrt-covariance (N x N, lower triangular)
858 * @param lambda Scaling parameter from UKF tuning
859 * @param Xsig Output: sigma points (N x (2N+1))
860 * @return SRUKF_RETURN_OK on success
861 */
863 const srukf_mat *S,
864 srukf_value lambda,
865 srukf_mat *Xsig) {
866 if (!x || !S || !Xsig)
868
869 srukf_index n = x->n_rows; /* state dimension N */
870 srukf_index n_sigma = 2 * n + 1; /* number of sigma points */
871 if (Xsig->n_rows != n || Xsig->n_cols != n_sigma)
873 if (S->n_rows != n || S->n_cols != n)
875
876 /* scaling factor γ = sqrt( N + λ ) */
877 srukf_value gamma = sqrt((srukf_value)n + lambda);
878 if (gamma <= 0.0) {
879 diag_report("generate_sigma_points: gamma <= 0 (N + lambda <= 0)");
881 }
882
883 /* 1st column = mean state */
884 for (srukf_index i = 0; i < n; ++i)
885 SRUKF_ENTRY(Xsig, i, 0) = SRUKF_ENTRY(x, i, 0);
886
887 /* Remaining columns */
888 for (srukf_index k = 0; k < n; ++k) {
889 for (srukf_index i = 0; i < n; ++i) {
890 /* +γ * S(:,k) */
891 SRUKF_ENTRY(Xsig, i, k + 1) =
892 SRUKF_ENTRY(x, i, 0) + gamma * SRUKF_ENTRY(S, i, k);
893 /* -γ * S(:,k) */
894 SRUKF_ENTRY(Xsig, i, k + 1 + n) =
895 SRUKF_ENTRY(x, i, 0) - gamma * SRUKF_ENTRY(S, i, k);
896 }
897 }
898 return SRUKF_RETURN_OK;
899}
900
901/**
902 * @brief Create a column view into a matrix (zero-copy)
903 *
904 * Sets up V to reference column k of M without copying data.
905 * This is used extensively when propagating sigma points, where
906 * we need to pass individual columns to the process/measurement model.
907 *
908 * @param V Output view (caller provides storage for the srukf_mat struct)
909 * @param M Source matrix
910 * @param k Column index
911 */
912static inline void srukf_mat_column_view(srukf_mat *V, const srukf_mat *M,
913 srukf_index k) {
914 V->n_rows = M->n_rows;
915 V->n_cols = 1;
916 V->inc_row = 1; /* column vector */
917 V->inc_col = M->n_rows; /* distance to next column in M */
918 V->data = M->data + k * M->inc_col;
919 V->type = 0;
921}
922
923/**
924 * @brief Propagate sigma points through a function
925 *
926 * Applies a function (process model or measurement model) to each
927 * sigma point individually. This is where the "unscented" magic happens:
928 * we evaluate the nonlinear function exactly, rather than linearizing it.
929 *
930 * @param Xsig Input sigma points (N x (2N+1) for state, M x (2N+1) for meas)
931 * @param Ysig Output propagated points (same size as Xsig or different for h)
932 * @param func Function to apply: func(x_in, x_out, user)
933 * @param user User data passed to func
934 * @return SRUKF_RETURN_OK on success
935 */
936static srukf_return
938 void (*func)(const srukf_mat *, srukf_mat *, void *),
939 void *user) {
940 /* Basic sanity checks */
941 if (!Xsig || !func || !Ysig)
943
944 if (Ysig->data == NULL)
946 if (Xsig->n_cols != Ysig->n_cols)
948
949 srukf_index n_sigma = Xsig->n_cols; /* number of sigma points */
950
951 /* Temporary matrix descriptors for a single column vector */
952 srukf_mat col_in, col_out;
953
954 /* Loop over all sigma points */
955 for (srukf_index k = 0; k < n_sigma; ++k) {
956 /* Point to the k‑th column of the input and output matrices */
957 srukf_mat_column_view(&col_in, Xsig, k);
958 srukf_mat_column_view(&col_out, Ysig, k);
959 /* Call the user supplied model */
960 func(&col_in, &col_out, user);
961 }
962
963 return SRUKF_RETURN_OK;
964}
965
966/** @} */ /* end impl_sigma */
967
968/*============================================================================
969 * @internal
970 * @defgroup impl_srukf_core SR-UKF Core Operations
971 * @brief QR-based covariance updates and Cholesky downdates
972 *
973 * These are the key numerical routines that make SR-UKF work. The central
974 * insight is that we can maintain S (where P = S*S') without ever forming P,
975 * using two key operations:
976 *
977 * 1. **QR-based update:** To compute S where S*S' = sum of outer products +
978 *noise, we stack everything into a tall matrix and take QR. The R factor
979 * (transposed) gives us S.
980 *
981 * 2. **Cholesky downdate:** When wc[0] < 0, we need to subtract a rank-1 term.
982 * The downdate modifies S in-place: S_new * S_new' = S * S' - v * v'
983 *
984 * @{
985 *============================================================================*/
986
987/**
988 * @brief Cholesky rank-1 downdate
989 *
990 * Updates a Cholesky factor to subtract a rank-1 term:
991 * @code
992 * S_new * S_new' = S * S' - v * v'
993 * @endcode
994 *
995 * This is the inverse operation of a Cholesky update. While updates are
996 * always stable (adding positive-definite term), downdates can fail if
997 * the result would be non-positive-definite.
998 *
999 * **Algorithm:** Uses Givens rotations applied column-by-column.
1000 * For each column j:
1001 * 1. Compute rotation to zero out work[j] against S[j,j]
1002 * 2. Apply rotation to update S[j,j] and remaining elements
1003 * 3. Propagate effect to work[j+1..n]
1004 *
1005 * **Numerical stability:** Givens rotations are orthogonal transformations,
1006 * which are maximally stable. We detect failure (non-SPD result) by checking
1007 * if r^2 = S[j,j]^2 - work[j]^2 becomes negative.
1008 *
1009 * @param S Lower triangular Cholesky factor (N x N), modified in place
1010 * @param v Vector to downdate by (length N)
1011 * @param work Scratch buffer (length N), contents destroyed
1012 * @return SRUKF_RETURN_OK on success, SRUKF_RETURN_MATH_ERROR if non-SPD
1013 */
1015 srukf_value *work) {
1016 if (!S || !v || !work)
1018
1019 srukf_index n = S->n_rows;
1020 if (S->n_cols != n)
1022
1023 /* Copy v to work buffer */
1024 memcpy(work, v, n * sizeof(srukf_value));
1025
1026 /* Apply Givens rotations to zero out work while updating S */
1027 for (srukf_index j = 0; j < n; ++j) {
1028 srukf_value Sjj = SRUKF_ENTRY(S, j, j);
1029 srukf_value wj = work[j];
1030
1031 /* Compute r = sqrt(Sjj^2 - wj^2) */
1032 srukf_value r2 = Sjj * Sjj - wj * wj;
1033 if (r2 < 0.0) {
1034 /* Matrix would become non-SPD */
1036 }
1037 srukf_value r = sqrt(r2);
1038
1039 if (fabs(Sjj) < SRUKF_EPS) {
1040 /* Skip if diagonal is essentially zero */
1041 if (fabs(wj) > SRUKF_EPS) {
1043 }
1044 continue;
1045 }
1046
1047 /* Givens rotation parameters: c = r/Sjj, s = wj/Sjj */
1048 srukf_value c = r / Sjj;
1049 srukf_value s = wj / Sjj;
1050
1051 /* Update diagonal */
1052 SRUKF_ENTRY(S, j, j) = r;
1053
1054 /* Update remaining rows and work */
1055 for (srukf_index i = j + 1; i < n; ++i) {
1056 srukf_value Sij = SRUKF_ENTRY(S, i, j);
1057 srukf_value wi = work[i];
1058
1059 /* Update S(i,j) and work(i) */
1060 SRUKF_ENTRY(S, i, j) = (Sij - s * wi) / c;
1061 work[i] = c * wi - s * Sij;
1062 }
1063 }
1064
1065 return SRUKF_RETURN_OK;
1066}
1067
1068/**
1069 * @brief Compute weighted deviations from sigma points
1070 *
1071 * Computes:
1072 * @code
1073 * Dev(:,k) = sqrt(|wc[k]|) * (Ysig(:,k) - mean)
1074 * @endcode
1075 *
1076 * These deviations are the building blocks for the QR-based covariance
1077 * computation. The covariance (if we computed it) would be:
1078 * @code
1079 * P = sum_k wc[k] * (Ysig(:,k) - mean) * (Ysig(:,k) - mean)'
1080 * = sum_k sign(wc[k]) * Dev(:,k) * Dev(:,k)'
1081 * @endcode
1082 *
1083 * For positive weights, this is Dev * Dev'. For negative wc[0], we
1084 * compute QR without column 0, then downdate with Dev(:,0).
1085 *
1086 * **Note on negative weights:**
1087 * wc[0] can be negative for small alpha. We use |wc| for the sqrt and
1088 * track the sign separately. The caller must handle the negative case
1089 * via Cholesky downdate.
1090 *
1091 * @param Ysig Propagated sigma points (dim x (2N+1))
1092 * @param mean Weighted mean (dim x 1)
1093 * @param wc Covariance weights (2N+1 elements)
1094 * @param Dev Output deviations (dim x (2N+1))
1095 * @return SRUKF_RETURN_OK on success
1096 */
1098 const srukf_mat *mean,
1099 const srukf_value *wc,
1100 srukf_mat *Dev) {
1101 if (!Ysig || !mean || !wc || !Dev)
1103
1104 srukf_index M = Ysig->n_rows;
1105 srukf_index n_sigma = Ysig->n_cols;
1106
1107 if (mean->n_rows != M || mean->n_cols != 1)
1109 if (Dev->n_rows != M || Dev->n_cols != n_sigma)
1111
1112 for (srukf_index k = 0; k < n_sigma; ++k) {
1113 srukf_value sw = sqrt(fabs(wc[k]));
1114 for (srukf_index i = 0; i < M; ++i) {
1115 SRUKF_ENTRY(Dev, i, k) =
1116 sw * (SRUKF_ENTRY(Ysig, i, k) - SRUKF_ENTRY(mean, i, 0));
1117 }
1118 }
1119
1120 return SRUKF_RETURN_OK;
1121}
1122
1123/**
1124 * @brief Compute sqrt-covariance from weighted deviations via QR
1125 *
1126 * This is the heart of the SR-UKF algorithm. Instead of computing:
1127 * @code
1128 * P = sum_k wc[k] * dev_k * dev_k' + Q
1129 * S = chol(P)
1130 * @endcode
1131 *
1132 * We directly compute S via QR decomposition of a compound matrix:
1133 * @code
1134 * A = [ Dev(:,1:2N)' ] <-- (2N) rows if wc[0] >= 0, else (2N) rows
1135 * [ Noise_sqrt' ] <-- (dim) rows
1136 *
1137 * [Q, R] = qr(A)
1138 * S = R' <-- lower triangular
1139 * @endcode
1140 *
1141 * **Why this works:**
1142 * If A = [a1; a2; ...] (rows), then A'*A = sum(a_i' * a_i).
1143 * The R from QR satisfies R'*R = A'*A (by orthogonality of Q).
1144 * So R'*R = sum(dev_k * dev_k') + Noise_sqrt * Noise_sqrt' = P.
1145 * Thus R' is the Cholesky factor S.
1146 *
1147 * **Handling negative wc[0]:**
1148 * If wc[0] < 0, we exclude Dev(:,0) from the QR (which computes the sum
1149 * of positive terms), then apply a Cholesky downdate to subtract
1150 * Dev(:,0) * Dev(:,0)'.
1151 *
1152 * **Ensuring positive diagonal:**
1153 * QR can produce negative diagonal in R. We flip signs of entire columns
1154 * to ensure S has positive diagonal, which is the standard Cholesky convention.
1155 *
1156 * @param Dev Weighted deviations (dim x (2N+1))
1157 * @param Noise_sqrt Noise sqrt-covariance (dim x dim)
1158 * @param S Output sqrt-covariance (dim x dim)
1159 * @param work QR workspace matrix ((2N+1+dim) x dim)
1160 * @param tau QR householder scalars (dim elements)
1161 * @param downdate_work Scratch for downdate (dim elements)
1162 * @param wc0_negative True if wc[0] < 0 (requires downdate)
1163 * @param dev0 First deviation column (for downdate, NULL if wc0 >= 0)
1164 * @return SRUKF_RETURN_OK on success
1165 */
1166static srukf_return
1168 srukf_mat *S, srukf_mat *work, srukf_value *tau,
1169 srukf_value *downdate_work, bool wc0_negative,
1170 const srukf_value *dev0) {
1171 if (!Dev || !Noise_sqrt || !S || !work || !tau)
1173
1174 srukf_index dim = Dev->n_rows; /* dimension of output S */
1175 srukf_index n_sigma = Dev->n_cols; /* 2N+1 */
1176
1177 if (Noise_sqrt->n_rows != dim || Noise_sqrt->n_cols != dim)
1179 if (S->n_rows != dim || S->n_cols != dim)
1181
1182 /* Build compound matrix for QR:
1183 * If wc0 >= 0: use all deviations
1184 * If wc0 < 0: exclude first deviation column, downdate after
1185 *
1186 * Compound = [ Dev(:,start:end)' ; Noise_sqrt' ]
1187 * where start = wc0_negative ? 1 : 0
1188 */
1189 srukf_index start_col = wc0_negative ? 1 : 0;
1190 srukf_index n_dev_cols = n_sigma - start_col;
1191 srukf_index n_rows = n_dev_cols + dim; /* rows in compound matrix */
1192
1193 /* Verify workspace size */
1194 if (work->n_rows < n_rows || work->n_cols < dim)
1196
1197 /* Fill compound matrix (column-major):
1198 * First n_dev_cols rows: transpose of Dev(:,start:end)
1199 * Last dim rows: transpose of Noise_sqrt
1200 */
1201 for (srukf_index j = 0; j < dim; ++j) {
1202 /* Dev' part */
1203 for (srukf_index i = 0; i < n_dev_cols; ++i) {
1204 SRUKF_ENTRY(work, i, j) = SRUKF_ENTRY(Dev, j, i + start_col);
1205 }
1206 /* Noise_sqrt' part */
1207 for (srukf_index i = 0; i < dim; ++i) {
1208 SRUKF_ENTRY(work, n_dev_cols + i, j) = SRUKF_ENTRY(Noise_sqrt, j, i);
1209 }
1210 }
1211
1212 /* QR factorization to get R (using pre-allocated tau buffer) */
1213 int info = SRUKF_GEQRF(SRUKF_LAPACK_LAYOUT, (int)n_rows, (int)dim, work->data,
1214 (int)SRUKF_LEADING_DIM(work), tau);
1215 if (info != 0) {
1216 diag_report("QR factorization (SRUKF_GEQRF) failed");
1218 }
1219
1220 /* Extract R' (transpose of upper triangular R) into S as lower triangular */
1221 for (srukf_index i = 0; i < dim; ++i) {
1222 for (srukf_index j = 0; j < dim; ++j) {
1223 if (j <= i) {
1224 /* S(i,j) = R(j,i) where R is upper triangular in work */
1225 SRUKF_ENTRY(S, i, j) = SRUKF_ENTRY(work, j, i);
1226 } else {
1227 SRUKF_ENTRY(S, i, j) = 0.0;
1228 }
1229 }
1230 }
1231
1232 /* Ensure positive diagonal (QR can give negative diagonal elements) */
1233 for (srukf_index i = 0; i < dim; ++i) {
1234 if (SRUKF_ENTRY(S, i, i) < 0.0) {
1235 /* Flip sign of entire column */
1236 for (srukf_index k = i; k < dim; ++k)
1237 SRUKF_ENTRY(S, k, i) = -SRUKF_ENTRY(S, k, i);
1238 }
1239 }
1240
1241 /* If wc[0] was negative, we need to downdate S with dev0 */
1242 if (wc0_negative && dev0) {
1243 if (!downdate_work)
1245 srukf_return ret = chol_downdate_rank1(S, dev0, downdate_work);
1246 if (ret != SRUKF_RETURN_OK)
1247 return ret;
1248 }
1249
1250 return SRUKF_RETURN_OK;
1251}
1252
1253/**
1254 * @brief Compute weighted mean from sigma points
1255 *
1256 * Computes:
1257 * @code
1258 * mean = sum_k wm[k] * Ysig(:,k)
1259 * @endcode
1260 *
1261 * The mean weights sum to 1, so this is a proper weighted average.
1262 * For the standard UKF parameters, the central sigma point (k=0)
1263 * gets the most weight when alpha is small.
1264 *
1265 * @param Ysig Sigma points (dim x (2N+1))
1266 * @param wm Mean weights ((2N+1) elements, sum to 1)
1267 * @param mean Output mean (dim x 1)
1268 * @return SRUKF_RETURN_OK on success
1269 */
1271 const srukf_value *wm,
1272 srukf_mat *mean) {
1273 if (!Ysig || !wm || !mean)
1275
1276 srukf_index M = Ysig->n_rows;
1277 srukf_index n_sigma = Ysig->n_cols;
1278
1279 if (mean->n_rows != M || mean->n_cols != 1)
1281
1282 for (srukf_index i = 0; i < M; ++i)
1283 SRUKF_ENTRY(mean, i, 0) = 0.0;
1284
1285 for (srukf_index k = 0; k < n_sigma; ++k) {
1286 srukf_value wk = wm[k];
1287 for (srukf_index i = 0; i < M; ++i)
1288 SRUKF_ENTRY(mean, i, 0) += wk * SRUKF_ENTRY(Ysig, i, k);
1289 }
1290
1291 return SRUKF_RETURN_OK;
1292}
1293
1294/**
1295 * @brief Compute cross-covariance between state and measurement
1296 *
1297 * Computes:
1298 * @code
1299 * Pxz = sum_k wc[k] * (Xsig(:,k) - x_mean) * (Ysig(:,k) - y_mean)'
1300 * @endcode
1301 *
1302 * This cross-covariance measures how state uncertainty correlates with
1303 * measurement uncertainty. It's a key ingredient in the Kalman gain:
1304 * @code
1305 * K = Pxz * inv(Pyy)
1306 * @endcode
1307 *
1308 * **Intuition:** If a state variable and a measurement are highly
1309 * correlated (large Pxz entry), then observing that measurement tells
1310 * us a lot about that state variable.
1311 *
1312 * **Note:** Unlike auto-covariance (Pxx or Pyy), cross-covariance is not
1313 * symmetric and can have any shape (N x M here).
1314 *
1315 * @param Xsig State sigma points (N x (2N+1))
1316 * @param Ysig Measurement sigma points (M x (2N+1))
1317 * @param x_mean State mean (N x 1)
1318 * @param y_mean Measurement mean (M x 1)
1319 * @param weights Covariance weights wc ((2N+1) elements)
1320 * @param Pxz Output cross-covariance (N x M)
1321 * @return SRUKF_RETURN_OK on success
1322 */
1323static srukf_return
1325 const srukf_mat *x_mean, const srukf_mat *y_mean,
1326 const srukf_value *weights, srukf_mat *Pxz) {
1327 if (!Xsig || !Ysig || !x_mean || !y_mean || !weights || !Pxz)
1329
1330 if (Xsig->n_rows != Pxz->n_rows || Ysig->n_rows != Pxz->n_cols)
1332
1333 if (Xsig->n_cols != Ysig->n_cols)
1335
1336 /* zero‑initialize */
1337 for (srukf_index i = 0; i < Pxz->n_rows; ++i)
1338 for (srukf_index j = 0; j < Pxz->n_cols; ++j)
1339 SRUKF_ENTRY(Pxz, i, j) = 0.0;
1340
1341 /* weighted outer products */
1342 for (srukf_index k = 0; k < Xsig->n_cols; ++k) {
1343 srukf_value wk = weights[k];
1344 for (srukf_index i = 0; i < Xsig->n_rows; ++i) {
1345 srukf_value xi = SRUKF_ENTRY(Xsig, i, k) - SRUKF_ENTRY(x_mean, i, 0);
1346 for (srukf_index j = 0; j < Ysig->n_rows; ++j) {
1347 srukf_value yj = SRUKF_ENTRY(Ysig, j, k) - SRUKF_ENTRY(y_mean, j, 0);
1348 SRUKF_ENTRY(Pxz, i, j) += wk * xi * yj;
1349 }
1350 }
1351 }
1352 return SRUKF_RETURN_OK;
1353}
1354
1355/** @} */ /* end impl_srukf_core */
1356
1357/*============================================================================
1358 * @internal
1359 * @defgroup impl_predict Predict Implementation
1360 * @brief The prediction step advances the state estimate forward in time
1361 *
1362 * The predict step answers: "Given our current estimate and the process model,
1363 * where do we expect the state to be next?"
1364 *
1365 * **Key operations:**
1366 * 1. Generate sigma points from current (x, S)
1367 * 2. Propagate each through the process model f
1368 * 3. Compute weighted mean of propagated points
1369 * 4. Compute new S via QR of deviations + process noise
1370 *
1371 * **Effect on uncertainty:**
1372 * Prediction always increases uncertainty (S grows) because:
1373 * - Process noise (Q) adds uncertainty
1374 * - Nonlinear transformation can spread the distribution
1375 *
1376 * @{
1377 *============================================================================*/
1378
1379/**
1380 * @brief Core predict implementation
1381 *
1382 * Performs the SR-UKF predict step using QR-based covariance update.
1383 * Can operate in-place (x_out == x_in, S_out == S_in) or to separate buffers.
1384 *
1385 * **Algorithm:**
1386 * @code
1387 * 1. Xsig = generate_sigma_points(x_in, S_in) // 2N+1 points
1388 * 2. Ysig = f(Xsig) // propagate each
1389 * 3. x_out = weighted_mean(Ysig, wm) // predicted mean
1390 * 4. Dev = sqrt(|wc|) * (Ysig - x_out) // weighted deviations
1391 * 5. S_out = qr([Dev'; Qsqrt'])' // sqrt-covariance via QR
1392 * 6. if wc[0] < 0: choldowndate(S_out, Dev(:,0)) // handle negative weight
1393 * @endcode
1394 *
1395 * @param ukf Filter (provides parameters and workspace)
1396 * @param x_in Current state (N x 1)
1397 * @param S_in Current sqrt-covariance (N x N)
1398 * @param f Process model function
1399 * @param user User data for f
1400 * @param x_out Output predicted state (N x 1, may alias x_in)
1401 * @param S_out Output predicted sqrt-covariance (N x N, may alias S_in)
1402 * @return SRUKF_RETURN_OK on success
1403 */
1404static srukf_return
1405srukf_predict_core(const srukf *ukf, const srukf_mat *x_in,
1406 const srukf_mat *S_in,
1407 void (*f)(const srukf_mat *, srukf_mat *, void *),
1408 void *user, srukf_mat *x_out, srukf_mat *S_out) {
1410 if (!ukf || !f || !x_in || !S_in || !x_out || !S_out)
1412 if (!ukf->Qsqrt || !ukf->wm || !ukf->wc || !ukf->ws)
1414
1415 /* --- Dimensions --------------------------------------------------- */
1416 srukf_index N = x_in->n_rows; /* state dimension */
1417
1418 /* --- Validate output dimensions ---------------------------------- */
1419 if (x_out->n_rows != N || x_out->n_cols != 1)
1421 if (S_out->n_rows != N || S_out->n_cols != N)
1423
1424 /* --- Use workspace temporaries ----------------------------------- */
1425 srukf_workspace *ws = ukf->ws;
1426 srukf_mat *Xsig = ws->Xsig;
1427 srukf_mat *Ysig = ws->Ysig_N;
1428 srukf_mat *x_mean = ws->x_pred;
1429 srukf_mat *Dev = ws->Dev_N;
1430 srukf_mat *qr_work = ws->qr_work_N;
1431
1432 /* --- Generate and propagate sigma points ------------------------ */
1433 ret = generate_sigma_points_from(x_in, S_in, ukf->lambda, Xsig);
1434 if (ret != SRUKF_RETURN_OK) {
1435 diag_report("predict: sigma point generation failed");
1436 return ret;
1437 }
1438
1439 ret = propagate_sigma_points(Xsig, Ysig, f, user);
1440 if (ret != SRUKF_RETURN_OK) {
1441 diag_report("predict: sigma point propagation failed");
1442 return ret;
1443 }
1444
1445 /* --- Validate callback output ----------------------------------- */
1446 if (!is_numeric_valid(Ysig)) {
1447 diag_report("predict: callback f produced NaN or Inf");
1449 }
1450
1451 /* --- Compute weighted mean --------------------------------------- */
1452 ret = compute_weighted_mean(Ysig, ukf->wm, x_mean);
1453 if (ret != SRUKF_RETURN_OK) {
1454 diag_report("predict: compute_weighted_mean failed");
1455 return ret;
1456 }
1457
1458 /* --- Compute weighted deviations --------------------------------- */
1459 ret = compute_weighted_deviations(Ysig, x_mean, ukf->wc, Dev);
1460 if (ret != SRUKF_RETURN_OK) {
1461 diag_report("predict: compute_weighted_deviations failed");
1462 return ret;
1463 }
1464
1465 /* --- Compute S via QR of [Dev'; Qsqrt'] -------------------------- */
1466 /* Handle potential negative wc[0] (for alpha < 1) */
1467 bool wc0_negative = (ukf->wc[0] < 0.0);
1468 srukf_value *dev0 = NULL;
1469 if (wc0_negative) {
1470 /* Save first column of Dev for downdate */
1471 dev0 = ws->dev0_N;
1472 for (srukf_index i = 0; i < N; ++i)
1473 dev0[i] = SRUKF_ENTRY(Dev, i, 0);
1474 }
1475
1476 ret =
1477 srukf_sqrt_from_deviations_ex(Dev, ukf->Qsqrt, S_out, qr_work, ws->tau_N,
1478 ws->downdate_work, wc0_negative, dev0);
1479 if (ret != SRUKF_RETURN_OK) {
1480 diag_report("predict: sqrt_from_deviations (QR/downdate) failed");
1481 return ret;
1482 }
1483
1484 /* --- Write mean → x_out ---------------------------------------- */
1485 memcpy(x_out->data, x_mean->data, N * sizeof(srukf_value));
1486
1487 return SRUKF_RETURN_OK;
1488}
1489
1490/** @} */ /* end impl_predict */
1491
1493 void (*f)(const srukf_mat *, srukf_mat *, void *),
1494 void *user) {
1495 if (!ukf || !x || !S)
1497
1498 /* Validate dimensions match filter */
1500 if (N == 0)
1502 if (x->n_rows != N || x->n_cols != 1)
1504 if (S->n_rows != N || S->n_cols != N)
1506
1507 /* Ensure workspace is allocated */
1508 srukf_return ret = ensure_workspace(ukf);
1509 if (ret != SRUKF_RETURN_OK)
1510 return ret;
1511
1512 return srukf_predict_core(ukf, x, S, f, user, x, S);
1513}
1514
1516 void (*f)(const srukf_mat *, srukf_mat *, void *),
1517 void *user) {
1518 if (!ukf || !ukf->x || !ukf->S)
1520
1521 /* Ensure workspace is allocated */
1522 srukf_return ret = ensure_workspace(ukf);
1523 if (ret != SRUKF_RETURN_OK)
1524 return ret;
1525
1526 srukf_index N = ukf->x->n_rows;
1527
1528 /* Use workspace for output temporaries */
1529 srukf_mat *x_out = ukf->ws->x_pred;
1530 srukf_mat *S_out = ukf->ws->S_tmp;
1531
1532 /* Run core: read from ukf->x/S, write to temps */
1533 ret = srukf_predict_core(ukf, ukf->x, ukf->S, f, user, x_out, S_out);
1534
1535 /* Commit on success */
1536 if (ret == SRUKF_RETURN_OK) {
1537 memcpy(ukf->x->data, x_out->data, N * sizeof(srukf_value));
1538 memcpy(ukf->S->data, S_out->data, N * N * sizeof(srukf_value));
1539 }
1540
1541 return ret;
1542}
1543
1544/*============================================================================
1545 * @internal
1546 * @defgroup impl_correct Correct Implementation
1547 * @brief The correction step incorporates a measurement
1548 *
1549 * The correct step answers: "Given a measurement, how should we update
1550 * our estimate?"
1551 *
1552 * **Key insight:** The Kalman gain K determines how to blend prediction
1553 * and measurement:
1554 * @code
1555 * x_new = x_predicted + K * (z_actual - z_predicted)
1556 * @endcode
1557 *
1558 * K is large when:
1559 * - Measurement is precise (small R) → trust measurement
1560 * - Prediction is uncertain (large P) → don't trust prediction
1561 *
1562 * K is small when:
1563 * - Measurement is noisy (large R) → don't trust measurement
1564 * - Prediction is confident (small P) → trust prediction
1565 *
1566 * **Effect on uncertainty:**
1567 * Correction always decreases uncertainty (S shrinks) because:
1568 * - New information can only reduce our ignorance
1569 * - Mathematically: S_new² = S_prior² - (K*Syy)(K*Syy)' (a downdate)
1570 *
1571 * @{
1572 *============================================================================*/
1573
1574/**
1575 * @brief Core correct implementation
1576 *
1577 * Performs the SR-UKF correction step using QR for measurement covariance
1578 * and Cholesky downdates for state covariance update.
1579 *
1580 * **Algorithm:**
1581 * @code
1582 * 1. Xsig = generate_sigma_points(x_in, S_in)
1583 * 2. Zsig = h(Xsig) // propagate to measurement
1584 * space
1585 * 3. z_pred = weighted_mean(Zsig, wm) // predicted measurement
1586 * 4. Syy = qr([Dev_z'; Rsqrt'])' // measurement sqrt-covariance
1587 * 5. Pxz = cross_covariance(Xsig, Zsig) // state-measurement
1588 * correlation
1589 * 6. K = Pxz * inv(Syy' * Syy) // Kalman gain via triangular
1590 * solves
1591 * 7. innovation = z - z_pred
1592 * 8. x_out = x_in + K * innovation // state update
1593 * 9. U = K * Syy
1594 * 10. for each column u of U: // M Cholesky downdates
1595 * S_out = choldowndate(S_out, u)
1596 * @endcode
1597 *
1598 * **Why Cholesky downdates for covariance update?**
1599 * The covariance update formula is:
1600 * @code
1601 * P_new = P - K * Pyy * K'
1602 * = P - (K * Syy) * (K * Syy)'
1603 * @endcode
1604 * If U = K * Syy (N x M), then P_new = P - U * U'.
1605 * This is exactly M rank-1 downdates using the columns of U.
1606 *
1607 * @param ukf Filter (provides parameters and workspace)
1608 * @param x_in Predicted state (N x 1)
1609 * @param S_in Predicted sqrt-covariance (N x N)
1610 * @param z Measurement (M x 1)
1611 * @param h Measurement model function
1612 * @param user User data for h
1613 * @param x_out Output corrected state (N x 1, may alias x_in)
1614 * @param S_out Output corrected sqrt-covariance (N x N, may alias S_in)
1615 * @return SRUKF_RETURN_OK on success
1616 */
1617static srukf_return
1618srukf_correct_core(const srukf *ukf, const srukf_mat *x_in,
1619 const srukf_mat *S_in, srukf_mat *z,
1620 void (*h)(const srukf_mat *, srukf_mat *, void *),
1621 void *user, srukf_mat *x_out, srukf_mat *S_out) {
1623 if (!ukf || !h || !z || !x_in || !S_in || !x_out || !S_out)
1625 if (!ukf->Rsqrt || !ukf->wm || !ukf->wc || !ukf->ws)
1627
1628 srukf_index N = x_in->n_rows; /* state dimension */
1629 srukf_index M = ukf->Rsqrt->n_rows; /* measurement dimension */
1630
1631 if (z->n_rows != M || z->n_cols != 1)
1633 if (x_out->n_rows != N || x_out->n_cols != 1)
1635 if (S_out->n_rows != N || S_out->n_cols != N)
1637
1638 /* --- Use workspace temporaries ----------------------------------- */
1639 srukf_workspace *ws = ukf->ws;
1640 srukf_mat *Xsig = ws->Xsig;
1641 srukf_mat *Ysig = ws->Ysig_M;
1642 srukf_mat *x_mean = ws->x_pred;
1643 srukf_mat *y_mean = ws->y_mean;
1644 srukf_mat *Syy = ws->Syy;
1645 srukf_mat *Pxz = ws->Pxz;
1646 srukf_mat *K = ws->K;
1647 srukf_mat *innov = ws->innov;
1648 srukf_mat *x_new = ws->x_new;
1649 srukf_mat *dx = ws->dx;
1650 srukf_mat *Dev_M = ws->Dev_M;
1651 srukf_mat *qr_work = ws->qr_work_M;
1652 srukf_mat *tmp1 = ws->tmp1; /* Used for K*Syy (N x M) */
1653
1654 /* 1. Generate & propagate σ‑points -------------------------------- */
1655 ret = generate_sigma_points_from(x_in, S_in, ukf->lambda, Xsig);
1656 if (ret != SRUKF_RETURN_OK)
1657 return ret;
1658 ret = propagate_sigma_points(Xsig, Ysig, h, user);
1659 if (ret != SRUKF_RETURN_OK)
1660 return ret;
1661
1662 /* --- Validate callback output ----------------------------------- */
1663 if (!is_numeric_valid(Ysig)) {
1664 diag_report("correct: callback h produced NaN or Inf");
1666 }
1667
1668 /* 2. Copy prior state mean for cross-covariance computation ------ */
1669 for (srukf_index i = 0; i < N; ++i)
1670 SRUKF_ENTRY(x_mean, i, 0) = SRUKF_ENTRY(x_in, i, 0);
1671
1672 /* 3. Compute weighted mean for measurement ------------------------- */
1673 ret = compute_weighted_mean(Ysig, ukf->wm, y_mean);
1674 if (ret != SRUKF_RETURN_OK)
1675 return ret;
1676
1677 /* 4. Compute weighted deviations for measurement ------------------- */
1678 ret = compute_weighted_deviations(Ysig, y_mean, ukf->wc, Dev_M);
1679 if (ret != SRUKF_RETURN_OK)
1680 return ret;
1681
1682 /* 5. Compute Syy via QR of [sqrt(wc)*Dev_M'; Rsqrt'] --------------- */
1683 bool wc0_negative = (ukf->wc[0] < 0.0);
1684 srukf_value *dev0_M_buf = NULL;
1685 if (wc0_negative) {
1686 /* Save first column of Dev_M for downdate */
1687 dev0_M_buf = ws->dev0_M;
1688 for (srukf_index i = 0; i < M; ++i)
1689 dev0_M_buf[i] = SRUKF_ENTRY(Dev_M, i, 0);
1690 }
1691
1692 ret = srukf_sqrt_from_deviations_ex(Dev_M, ukf->Rsqrt, Syy, qr_work,
1693 ws->tau_M, ws->downdate_work,
1694 wc0_negative, dev0_M_buf);
1695 if (ret != SRUKF_RETURN_OK)
1696 return ret;
1697
1698 /* --- Check if Syy is essentially zero ----------------------------- */
1699 bool Syy_zero = true;
1700 for (srukf_index i = 0; i < M && Syy_zero; ++i)
1701 if (fabs(SRUKF_ENTRY(Syy, i, i)) > SRUKF_EPS)
1702 Syy_zero = false;
1703 if (Syy_zero) {
1704 memcpy(x_out->data, x_in->data, N * sizeof(srukf_value));
1705 memcpy(S_out->data, S_in->data, N * N * sizeof(srukf_value));
1706 return SRUKF_RETURN_OK;
1707 }
1708
1709 /* 6. Cross‑covariance between state & measurement σ‑points -------- */
1710 ret = compute_cross_covariance(Xsig, Ysig, x_mean, y_mean, ukf->wc, Pxz);
1711 if (ret != SRUKF_RETURN_OK)
1712 return ret;
1713
1714 /* 7. Kalman gain K = Pxz * inv(Syy') * inv(Syy) -------------------- */
1715 /* We have Syy as lower triangular.
1716 * K = Pxz * (Syy * Syy')^{-1} = Pxz * Syy'^{-1} * Syy^{-1}
1717 *
1718 * Using BLAS triangular solve:
1719 * Step 1: K = Pxz * Syy'^{-1} → solve K * Syy' = Pxz (Syy' is upper tri)
1720 * SRUKF_TRSM: side=Right, uplo=Lower, trans=Trans → K * Syy' = Pxz
1721 * Step 2: K = K * Syy^{-1} → solve K * Syy = K (Syy is lower tri)
1722 * SRUKF_TRSM: side=Right, uplo=Lower, trans=NoTrans → K * Syy = K
1723 */
1724 memcpy(K->data, Pxz->data, N * M * sizeof(srukf_value));
1725
1726 /* K * Syy' = Pxz → K = Pxz * Syy'^{-1} */
1727 SRUKF_TRSM(SRUKF_CBLAS_LAYOUT, CblasRight, CblasLower, CblasTrans,
1728 CblasNonUnit, (int)N, (int)M, (srukf_value)1.0, Syy->data,
1729 (int)SRUKF_LEADING_DIM(Syy), K->data, (int)SRUKF_LEADING_DIM(K));
1730
1731 /* K * Syy = K → K = K * Syy^{-1} */
1732 SRUKF_TRSM(SRUKF_CBLAS_LAYOUT, CblasRight, CblasLower, CblasNoTrans,
1733 CblasNonUnit, (int)N, (int)M, (srukf_value)1.0, Syy->data,
1734 (int)SRUKF_LEADING_DIM(Syy), K->data, (int)SRUKF_LEADING_DIM(K));
1735
1736 /* 8. Innovation ------------------------------------------------------- */
1737 for (srukf_index i = 0; i < M; ++i)
1738 SRUKF_ENTRY(innov, i, 0) = SRUKF_ENTRY(z, i, 0) - SRUKF_ENTRY(y_mean, i, 0);
1739
1740 /* 9. State update: dx = K * innov, x_new = x_in + dx ------------------ */
1741 /* dx (N x 1) = K (N x M) * innov (M x 1) */
1742 SRUKF_GEMM(SRUKF_CBLAS_LAYOUT, CblasNoTrans, CblasNoTrans, (int)N, 1, (int)M,
1743 (srukf_value)1.0, K->data, (int)SRUKF_LEADING_DIM(K), innov->data,
1744 (int)SRUKF_LEADING_DIM(innov), (srukf_value)0.0, dx->data,
1745 (int)SRUKF_LEADING_DIM(dx));
1746
1747 for (srukf_index i = 0; i < N; ++i)
1748 SRUKF_ENTRY(x_new, i, 0) = SRUKF_ENTRY(x_in, i, 0) + SRUKF_ENTRY(dx, i, 0);
1749
1750 /* 10. Covariance update via Cholesky downdates ----------------------- */
1751 /* S_out² = S_in² - K * Syy * (K * Syy)'
1752 * Let U = K * Syy (N x M), then S_out² = S_in² - U * U'
1753 * This is M rank-1 downdates using columns of U.
1754 */
1755
1756 /* First, copy S_in to S_out */
1757 memcpy(S_out->data, S_in->data, N * N * sizeof(srukf_value));
1758
1759 /* Compute U = K * Syy (N x M) */
1760 SRUKF_GEMM(SRUKF_CBLAS_LAYOUT, CblasNoTrans, CblasNoTrans, (int)N, (int)M,
1761 (int)M, (srukf_value)1.0, K->data, (int)SRUKF_LEADING_DIM(K),
1762 Syy->data, (int)SRUKF_LEADING_DIM(Syy), (srukf_value)0.0,
1763 tmp1->data, (int)SRUKF_LEADING_DIM(tmp1));
1764
1765 /* Apply M successive rank-1 Cholesky downdates */
1766 /* Use dev0_N as temporary buffer for column extraction (N elements) */
1767 srukf_value *u_col = ws->dev0_N;
1768
1769 for (srukf_index j = 0; j < M; ++j) {
1770 /* Extract column j of U */
1771 for (srukf_index i = 0; i < N; ++i)
1772 u_col[i] = SRUKF_ENTRY(tmp1, i, j);
1773
1774 /* Perform rank-1 downdate */
1775 ret = chol_downdate_rank1(S_out, u_col, ws->downdate_work);
1776 if (ret != SRUKF_RETURN_OK) {
1777 diag_report("correct: Cholesky downdate failed, matrix not SPD");
1778 return ret;
1779 }
1780 }
1781
1782 /* 11. Write state output --------------------------------------------- */
1783 memcpy(x_out->data, x_new->data, N * sizeof(srukf_value));
1784
1785 return SRUKF_RETURN_OK;
1786}
1787
1788/** @} */ /* end impl_correct */
1789
1791 srukf_mat *z,
1792 void (*h)(const srukf_mat *, srukf_mat *, void *),
1793 void *user) {
1794 if (!ukf || !x || !S || !z)
1796
1797 /* Validate dimensions match filter */
1799 srukf_index M = srukf_meas_dim(ukf);
1800 if (N == 0 || M == 0)
1802 if (x->n_rows != N || x->n_cols != 1)
1804 if (S->n_rows != N || S->n_cols != N)
1806 if (z->n_rows != M || z->n_cols != 1)
1808
1809 /* Ensure workspace is allocated */
1810 srukf_return ret = ensure_workspace(ukf);
1811 if (ret != SRUKF_RETURN_OK)
1812 return ret;
1813
1814 return srukf_correct_core(ukf, x, S, z, h, user, x, S);
1815}
1816
1818 void (*h)(const srukf_mat *, srukf_mat *, void *),
1819 void *user) {
1820 if (!ukf || !ukf->x || !ukf->S)
1822
1823 /* Ensure workspace is allocated */
1824 srukf_return ret = ensure_workspace(ukf);
1825 if (ret != SRUKF_RETURN_OK)
1826 return ret;
1827
1828 srukf_index N = ukf->x->n_rows;
1829
1830 /* Use workspace for output temporaries */
1831 srukf_mat *x_out = ukf->ws->x_new;
1832 srukf_mat *S_out = ukf->ws->S_new;
1833
1834 /* Run core: read from ukf->x/S, write to temps */
1835 ret = srukf_correct_core(ukf, ukf->x, ukf->S, z, h, user, x_out, S_out);
1836
1837 /* Commit on success */
1838 if (ret == SRUKF_RETURN_OK) {
1839 memcpy(ukf->x->data, x_out->data, N * sizeof(srukf_value));
1840 memcpy(ukf->S->data, S_out->data, N * N * sizeof(srukf_value));
1841 }
1842
1843 return ret;
1844}
static srukf_return srukf_predict_core(const srukf *ukf, const srukf_mat *x_in, const srukf_mat *S_in, void(*f)(const srukf_mat *, srukf_mat *, void *), void *user, srukf_mat *x_out, srukf_mat *S_out)
Core predict implementation.
Definition srukf.c:1405
static bool is_numeric_valid(const srukf_mat *M)
Check if matrix contains any NaN or Inf values.
Definition srukf.c:390
static srukf_return srukf_sqrt_from_deviations_ex(const srukf_mat *Dev, const srukf_mat *Noise_sqrt, srukf_mat *S, srukf_mat *work, srukf_value *tau, srukf_value *downdate_work, bool wc0_negative, const srukf_value *dev0)
Compute sqrt-covariance from weighted deviations via QR.
Definition srukf.c:1167
static srukf_return alloc_vector(srukf_value **vec, srukf_index len)
Allocate a vector of doubles.
Definition srukf.c:408
srukf_index srukf_state_dim(const srukf *ukf)
Get the state dimension N.
Definition srukf.c:562
static srukf_return srukf_compute_weights(srukf *ukf, const srukf_index n)
Compute sigma point weights.
Definition srukf.c:456
#define SRUKF_EPS
Numerical tolerance for near-zero checks.
Definition srukf.c:66
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
static void diag_report(const char *msg)
Report a diagnostic message.
Definition srukf.c:137
srukf * srukf_create(int N, int M)
Create a filter with uninitialized noise matrices.
Definition srukf.c:714
static srukf_return srukf_correct_core(const srukf *ukf, const srukf_mat *x_in, const srukf_mat *S_in, srukf_mat *z, void(*h)(const srukf_mat *, srukf_mat *, void *), void *user, srukf_mat *x_out, srukf_mat *S_out)
Core correct implementation.
Definition srukf.c:1618
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
static srukf_return generate_sigma_points_from(const srukf_mat *x, const srukf_mat *S, srukf_value lambda, srukf_mat *Xsig)
Generate sigma points from state and sqrt-covariance.
Definition srukf.c:862
srukf * srukf_create_from_noise(const srukf_mat *Qsqrt, const srukf_mat *Rsqrt)
Create a filter from noise covariance matrices.
Definition srukf.c:734
void srukf_free(srukf *ukf)
Free all memory allocated for the filter.
Definition srukf.c:530
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_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
static void srukf_mat_column_view(srukf_mat *V, const srukf_mat *M, srukf_index k)
Create a column view into a matrix (zero-copy)
Definition srukf.c:912
static srukf_return compute_weighted_deviations(const srukf_mat *Ysig, const srukf_mat *mean, const srukf_value *wc, srukf_mat *Dev)
Compute weighted deviations from sigma points.
Definition srukf.c:1097
static srukf_diag_fn g_diag_callback
Definition srukf.c:123
static srukf_return chol_downdate_rank1(srukf_mat *S, const srukf_value *v, srukf_value *work)
Cholesky rank-1 downdate.
Definition srukf.c:1014
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
#define DEFAULT_ALPHA
Default sigma point spread parameter.
Definition srukf.c:60
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
static srukf_return ensure_workspace(srukf *ukf)
Ensure workspace is allocated (lazy allocation)
Definition srukf.c:360
void srukf_free_workspace(srukf *ukf)
Free workspace to reclaim memory.
Definition srukf.c:240
static srukf_return compute_weighted_mean(const srukf_mat *Ysig, const srukf_value *wm, srukf_mat *mean)
Compute weighted mean from sigma points.
Definition srukf.c:1270
static srukf_return compute_cross_covariance(const srukf_mat *Xsig, const srukf_mat *Ysig, const srukf_mat *x_mean, const srukf_mat *y_mean, const srukf_value *weights, srukf_mat *Pxz)
Compute cross-covariance between state and measurement.
Definition srukf.c:1324
srukf_mat * srukf_mat_alloc(srukf_index rows, srukf_index cols, int alloc_data)
Allocate a matrix.
Definition srukf.c:75
#define DEFAULT_KAPPA
Default secondary scaling parameter.
Definition srukf.c:64
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
#define DEFAULT_BETA
Default prior distribution parameter (optimal for Gaussian)
Definition srukf.c:62
static srukf_return propagate_sigma_points(const srukf_mat *Xsig, srukf_mat *Ysig, void(*func)(const srukf_mat *, srukf_mat *, void *), void *user)
Propagate sigma points through a function.
Definition srukf.c:937
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
Square-Root Unscented Kalman Filter (SR-UKF) Library.
#define SRUKF_MAT_ALLOC(rows, cols)
Allocate a matrix with data buffer.
Definition srukf.h:357
#define SRUKF_LEADING_DIM(A)
Get leading dimension for BLAS/LAPACK (always n_rows for col-major)
Definition srukf.h:310
struct srukf_workspace srukf_workspace
Opaque workspace structure.
Definition srukf.h:375
#define SRUKF_GEMM
Definition srukf.h:325
#define SRUKF_SET_TYPE(A, t)
Set a type flag on a matrix.
Definition srukf.h:302
double srukf_value
Scalar value type.
Definition srukf.h:230
void(* srukf_diag_fn)(const char *msg)
Diagnostic callback function type.
Definition srukf.h:457
@ 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
#define SRUKF_IS_TYPE(A, t)
Test if a type flag is set.
Definition srukf.h:306
#define SRUKF_MAT_ALLOC_NO_DATA(rows, cols)
Allocate a matrix struct only (data pointer will be NULL)
Definition srukf.h:359
#define SRUKF_ENTRY(A, i, j)
Element access macro (column-major)
Definition srukf.h:298
#define SRUKF_TRSM
Definition srukf.h:326
#define SRUKF_CBLAS_LAYOUT
BLAS layout constant.
Definition srukf.h:313
#define SRUKF_LAPACK_LAYOUT
LAPACK layout constant.
Definition srukf.h:315
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
#define SRUKF_GEQRF
Definition srukf.h:328
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