LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
infer_lqn_ekf.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_API_INFER_INFER_LQN_EKF_H
6#define LINE_API_INFER_INFER_LQN_EKF_H
7
8/**
9 * @file
10 * @ingroup api_infer
11 * Extended Kalman Filter for LQN parameter identification.
12 *
13 * Templated port of matlab/src/api/infer/infer_lqn_ekf.m. The JAR carries the
14 * same recursion inside jline/api/infer/InferLqn.java.
15 *
16 * Tracks a hidden parameter vector across a measurement sequence, following
17 * Zheng, Yang, Woodside, Litoiu, Iszlai, "Tracking Time-Varying Parameters in
18 * Software Systems with Extended Kalman Filters", CASCON 2005, equations 1-9.
19 * The parameter follows a zero-mean random walk a_k = a_{k-1} + w and the
20 * measurement is z_k = h(a_k) + v, with h the nonlinear performance model.
21 * Per step:
22 *
23 * a_pred = a, P_pred = P + Q (prediction, eq 5)
24 * H, zpred = jacobian of h at a_pred (eq 4)
25 * e = z_k - zpred (innovation)
26 * S = H P_pred H' + R, K = P_pred H' S^-1 (gain, eq 6)
27 * a = a_pred + K e, P = (I - K H) P_pred (update, eq 7)
28 *
29 * h enters as a std::function, exactly as in infer_lqn_jacobian, so the model
30 * layer that evaluates an LQN stays outside this header: any caller that can
31 * produce z = h(a) can run the filter. That is the whole reason the MATLAB
32 * splits infer_lqn into infer_lqn_ekf plus a model-evaluating closure.
33 *
34 * MATLAB forms the gain as (Ppred*H')/S, a right division, i.e. it SOLVES
35 * K S = Ppred H' rather than inverting S. The port does the same, row by row
36 * through one LU factorization of S', which matters: S is the innovation
37 * covariance and is ill-conditioned exactly when a parameter is unobservable,
38 * which is the regime the filter is used in. The covariance is symmetrized
39 * after the update, as in MATLAB, because (I - K H) P_pred is symmetric only
40 * up to roundoff and an asymmetric P feeds back into every later gain.
41 *
42 * MATLAB's OPTIONS struct is read through infer_lqn_optget, a
43 * field-present-and-non-empty default helper. That helper has no counterpart
44 * here and needs none: EkfOptions below carries the same five defaults as
45 * member initializers, which is what a struct with defaulted fields IS in
46 * C++. The one option not carried is 'verbose', a console trace: the port
47 * writes nothing to any stream (an API-layer invariant), and the caller has
48 * the same information in the returned per-step innovations.
49 *
50 * ARITHMETIC: the recursion itself is rational, but Er and Ea are root mean
51 * squares, so the routine is gated on transcendental arithmetic and registered
52 * for Double and Real only. That gate is not merely about the two summary
53 * numbers: the filter is a fixed number of steps of a linear-algebraic
54 * recursion, so in exact arithmetic the numerators of P would grow without
55 * bound while the estimate it produces stays an approximation of a nonlinear
56 * problem, and the finite differences inside H carry a truncation error that
57 * exact arithmetic cannot remove.
58 */
59
60#include <cmath>
61#include <cstddef>
62#include <functional>
63#include <vector>
64
66#include "line/num/number.h"
67#include "line/util/error.h"
68#include "line/util/lu.h"
69#include "line/util/matrix.h"
70
71namespace line {
72namespace infer {
73
74/** MATLAB's OPTIONS struct, with the same defaults infer_lqn_optget supplies. */
75template <class T>
76struct EkfOptions {
77 double fd_step = 1e-3; ///< relative finite-difference step
78 double fd_floor = 1e-6; ///< minimum absolute perturbation scale
79 bool clamp_positive = true; ///< clamp each estimate to at least fd_floor
80 std::vector<T> a_true; ///< ground truth for Ea, empty for none
81};
82
83/** MATLAB's [ahat, info] return list. */
84template <class T>
85struct EkfResult {
86 Matrix<T> ahat; ///< (np x nsteps) estimate trajectory
87 Matrix<T> e; ///< (no x nsteps) prediction errors
88 Matrix<T> zpred; ///< (no x nsteps) predicted measurements
89 std::vector<Matrix<T>> Phist; ///< per-step posterior covariances
90 Matrix<T> P; ///< final covariance
91 T Er; ///< RMS prediction error
92 T Ea; ///< RMS parameter tracking error, if a_true given
93 bool has_Ea = false; ///< false mirrors MATLAB's info.Ea = []
94};
95
96/**
97 * @brief Extended Kalman Filter for LQN parameter identification.
98 *
99 * @param hfun observation map, a parameter vector to an observation vector
100 * @param a0 (np) initial parameter estimate
101 * @param P0 (np x np) initial estimation-error covariance
102 * @param Z (no x nsteps) measurements, one column per step
103 * @param Q (np x np) parameter-drift covariance
104 * @param R (no x no) measurement-error covariance
105 * @param opts filter options
106 */
107template <class T>
108EkfResult<T> infer_lqn_ekf(const std::function<std::vector<T>(const std::vector<T>&)>& hfun,
109 const std::vector<T>& a0, const Matrix<T>& P0, const Matrix<T>& Z,
110 const Matrix<T>& Q, const Matrix<T>& R,
111 const EkfOptions<T>& opts = EkfOptions<T>()) {
113 "infer_lqn_ekf requires transcendental arithmetic: its Er and Ea summaries "
114 "are root mean squares, and the sensitivity matrix it corrects on is a "
115 "finite difference whose truncation error no arithmetic can remove");
116
117 const std::size_t np = a0.size();
118 const std::size_t no = Z.rows();
119 const std::size_t nsteps = Z.cols();
120 if (np == 0) throw InputError("infer_lqn_ekf: empty parameter vector");
121 if (nsteps == 0) throw InputError("infer_lqn_ekf: no measurements");
122 if (P0.rows() != np || P0.cols() != np) throw InputError("infer_lqn_ekf: P0 is not np x np");
123 if (Q.rows() != np || Q.cols() != np) throw InputError("infer_lqn_ekf: Q is not np x np");
124 if (R.rows() != no || R.cols() != no) throw InputError("infer_lqn_ekf: R is not no x no");
125 if (!opts.a_true.empty() && opts.a_true.size() != np)
126 throw InputError("infer_lqn_ekf: aTrue has the wrong length");
127
128 const T zero = num_traits<T>::from_int(0);
129 const T half = num_traits<T>::from_double(0.5);
130 const T floor = num_traits<T>::from_double(opts.fd_floor);
131
132 EkfResult<T> out;
133 out.ahat = Matrix<T>(np, nsteps, zero);
134 out.e = Matrix<T>(no, nsteps, zero);
135 out.zpred = Matrix<T>(no, nsteps, zero);
136 out.Phist.reserve(nsteps);
137
138 std::vector<T> a = a0;
139 Matrix<T> P = P0;
140
141 for (std::size_t k = 0; k < nsteps; ++k) {
142 // (1) predict: the drift is zero mean, so only the covariance moves
143 const std::vector<T> aPred = a;
144 Matrix<T> Ppred(np, np, zero);
145 for (std::size_t i = 0; i < np; ++i)
146 for (std::size_t j = 0; j < np; ++j) Ppred(i, j) = P(i, j) + Q(i, j);
147
148 // (2,4) predicted measurement and sensitivity matrix at a_pred
149 const JacobianResult<T> jac =
150 infer_lqn_jacobian(hfun, aPred, opts.fd_step, opts.fd_floor);
151 if (jac.h0.size() != no)
152 throw InputError("infer_lqn_ekf: the observation map and Z disagree on the "
153 "observation count");
154 const Matrix<T>& H = jac.H;
155
156 // (3) prediction error
157 std::vector<T> e(no, zero);
158 for (std::size_t i = 0; i < no; ++i) e[i] = Z(i, k) - jac.h0[i];
159
160 // (6) gain: B = Ppred H' (np x no), S = H B + R (no x no), K S = B
161 Matrix<T> B(np, no, zero);
162 for (std::size_t i = 0; i < np; ++i)
163 for (std::size_t j = 0; j < no; ++j) {
164 T s = zero;
165 for (std::size_t l = 0; l < np; ++l) s += Ppred(i, l) * H(j, l);
166 B(i, j) = s;
167 }
168 Matrix<T> St(no, no, zero); // S transposed, the matrix of the row solves
169 for (std::size_t i = 0; i < no; ++i)
170 for (std::size_t j = 0; j < no; ++j) {
171 T s = R(i, j);
172 for (std::size_t l = 0; l < np; ++l) s += H(i, l) * B(l, j);
173 St(j, i) = s;
174 }
175 Matrix<T> LU = St;
176 const std::vector<std::size_t> piv = lu_factor(LU);
177 Matrix<T> K(np, no, zero);
178 for (std::size_t i = 0; i < np; ++i) {
179 std::vector<T> rhs(no, zero);
180 for (std::size_t j = 0; j < no; ++j) rhs[j] = B(i, j);
181 lu_solve(LU, piv, rhs);
182 for (std::size_t j = 0; j < no; ++j) K(i, j) = rhs[j];
183 }
184
185 // (4-update) improved parameter estimate
186 for (std::size_t i = 0; i < np; ++i) {
187 T s = aPred[i];
188 for (std::size_t j = 0; j < no; ++j) s += K(i, j) * e[j];
189 a[i] = s;
190 }
191 if (opts.clamp_positive)
192 for (std::size_t i = 0; i < np; ++i)
193 if (a[i] < floor) a[i] = floor;
194
195 // (7) covariance update, symmetrized as in MATLAB
196 Matrix<T> KH(np, np, zero);
197 for (std::size_t i = 0; i < np; ++i)
198 for (std::size_t j = 0; j < np; ++j) {
199 T s = zero;
200 for (std::size_t l = 0; l < no; ++l) s += K(i, l) * H(l, j);
201 KH(i, j) = s;
202 }
203 Matrix<T> Pnew(np, np, zero);
204 for (std::size_t i = 0; i < np; ++i)
205 for (std::size_t j = 0; j < np; ++j) {
206 T s = Ppred(i, j);
207 for (std::size_t l = 0; l < np; ++l) s -= KH(i, l) * Ppred(l, j);
208 Pnew(i, j) = s;
209 }
210 for (std::size_t i = 0; i < np; ++i)
211 for (std::size_t j = i; j < np; ++j) {
212 const T s = half * (Pnew(i, j) + Pnew(j, i));
213 Pnew(i, j) = s;
214 Pnew(j, i) = s;
215 }
216 P = Pnew;
217
218 for (std::size_t i = 0; i < np; ++i) out.ahat(i, k) = a[i];
219 for (std::size_t i = 0; i < no; ++i) {
220 out.e(i, k) = e[i];
221 out.zpred(i, k) = jac.h0[i];
222 }
223 out.Phist.push_back(P);
224 }
225 out.P = P;
226
227 using std::sqrt;
228 T se = zero;
229 for (std::size_t i = 0; i < no; ++i)
230 for (std::size_t k = 0; k < nsteps; ++k) se += out.e(i, k) * out.e(i, k);
231 const std::size_t ne = no * nsteps;
232 out.Er = ne == 0 ? zero
233 : T(sqrt(T(se / num_traits<T>::from_int(static_cast<long>(ne)))));
234 out.Ea = zero;
235 if (!opts.a_true.empty()) {
236 T sa = zero;
237 for (std::size_t i = 0; i < np; ++i)
238 for (std::size_t k = 0; k < nsteps; ++k) {
239 const T d = out.ahat(i, k) - opts.a_true[i];
240 sa += d * d;
241 }
242 const std::size_t na = np * nsteps;
243 out.Ea = T(sqrt(T(sa / num_traits<T>::from_int(static_cast<long>(na)))));
244 out.has_Ea = true;
245 }
246 return out;
247}
248
249} // namespace infer
250} // namespace line
251
252#endif // LINE_API_INFER_INFER_LQN_EKF_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
The exception types the port throws.
Forward finite-difference sensitivity matrix of an observation map.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
JacobianResult< T > infer_lqn_jacobian(const std::function< std::vector< T >(const std::vector< T > &)> &hfun, const std::vector< T > &a, double fd_step=1e-3, double fd_floor=1e-6)
Forward finite-difference sensitivity matrix of an observation map.
EkfResult< T > infer_lqn_ekf(const std::function< std::vector< T >(const std::vector< T > &)> &hfun, const std::vector< T > &a0, const Matrix< T > &P0, const Matrix< T > &Z, const Matrix< T > &Q, const Matrix< T > &R, const EkfOptions< T > &opts=EkfOptions< T >())
Extended Kalman Filter for LQN parameter identification.
void lu_solve(const Matrix< T > &LU, const std::vector< std::size_t > &piv, std::vector< T > &b)
Solve LUx = Pb in place on b, using the factors from lu_factor.
Definition lu.h:94
std::vector< std::size_t > lu_factor(Matrix< T > &A)
In-place LU of A (n x n).
Definition lu.h:48
Number-type abstraction for the templated API port.
MATLAB's OPTIONS struct, with the same defaults infer_lqn_optget supplies.
double fd_floor
minimum absolute perturbation scale
bool clamp_positive
clamp each estimate to at least fd_floor
std::vector< T > a_true
ground truth for Ea, empty for none
double fd_step
relative finite-difference step
MATLAB's [ahat, info] return list.
T Er
RMS prediction error.
Matrix< T > e
(no x nsteps) prediction errors
Matrix< T > ahat
(np x nsteps) estimate trajectory
Matrix< T > P
final covariance
T Ea
RMS parameter tracking error, if a_true given.
std::vector< Matrix< T > > Phist
per-step posterior covariances
bool has_Ea
false mirrors MATLAB's info.Ea = []
Matrix< T > zpred
(no x nsteps) predicted measurements
Mirrors the [H, h0] return list of the MATLAB function.
std::vector< T > h0
(no) observation at the base point
Matrix< T > H
(no x np) sensitivity matrix