Skip to content

Commit e721c6a

Browse files
committed
feat(estimators): add incremental QR least squares
Deploy roadmap item 63: streaming linear least squares in fixed memory. Each row is rotated into an upper-triangular factor with Givens rotations, so YᵀY is never formed. Solve runs a rank-revealing SVD of that factor and returns the minimum-norm parameters, the rank, the identifiable row space and the residual RMS. Checked against double-precision normal equations on 300 random 3-parameter problems with 5–205 rows (parameters agree to ≈3e-6). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q53m7MZDmHwocmKbhhd2o5
1 parent 28b815a commit e721c6a

13 files changed

Lines changed: 414 additions & 182 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Refer to the documentation to quickly integrate and utilize the library's signal
1919
| [Analysis](doc/analysis/README.md) | FFT, Real-Input FFT (RFFT), Power Spectral Density, DCT, Discrete Wavelet Transform (Haar/Daubechies), Window Functions, Signal Detectors, Convolution & Correlation, Goertzel Algorithm, Decibels, Hilbert Transform / Analytic Signal, Mel Filterbank / MFCC |
2020
| [Control Analysis](doc/control_analysis/README.md) | Frequency Response, Root Locus, Controllability/Observability Matrices & Gramians, Continuous-to-Discrete, Transfer Function ↔ State Space |
2121
| [Controllers](doc/controllers/README.md) | Bang-Bang/Hysteresis, Deadbeat Control, PID, LQR, LQI (Integral/Servo State Feedback), MPC, Saturation, Rate Limiter, Slew-Limited Saturation, Feedforward/2-DOF, Gain-Scheduled Controller, Lead-Lag Compensator, Luenberger Observer, Friction Compensation (Coulomb + Viscous + Stribeck) |
22-
| [Estimators](doc/estimators/README.md) | Linear Regression, Polynomial Fitting, Total Least Squares, Yule-Walker (offline), Recursive Least Squares, Recursive Instrumental Variables, LMS / NLMS Adaptive Filter (online), Consistency Metrics / NEES / NIS |
22+
| [Estimators](doc/estimators/README.md) | Linear Regression, Polynomial Fitting, Total Least Squares, Yule-Walker (offline), Recursive Least Squares, Recursive Instrumental Variables, LMS / NLMS Adaptive Filter (online), Consistency Metrics / NEES / NIS, Incremental QR Least Squares (rank-revealing) |
2323
| [Filters](doc/filters/README.md) | Kalman, Extended Kalman, Unscented Kalman, Square-Root Kalman, Alpha-Beta/Alpha-Beta-Gamma, FIR, IIR, Exponential Moving Average, Moving Average, Complementary, Median Filter, CIC (Cascaded Integrator-Comb), Notch/Comb Filter, Savitzky-Golay Filter, Biquad/Second-Order-Section Cascade, IIR Filter Design (Butterworth/Chebyshev-I + Bilinear Transform), Madgwick/Mahony AHRS |
2424
| [Optimization](doc/optimization/README.md) | Gradient Descent, SGD / Adam Step Optimisers |
2525
| [Regularization](doc/regularization/README.md) | L1 (Lasso), L2 (Ridge) |

‎ROADMAP.md‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ Difficulty legend:
2727
| # | Component | Target module | Difficulty |
2828
|----|------------------------------------------------------|-------------------------|------------|
2929
| 53 | SE(3) transform, twists, wrenches, adjoint, exp/log | `math` | ★★☆☆☆ |
30-
| 63 | Incremental QR least squares (rank-revealing) | `estimators/offline` | ★★★☆☆ |
3130
| 64 | Levenberg–Marquardt (retraction, pluggable damping) | `optimization` | ★★★☆☆ |
3231
| 65 | Dense convex QP (Goldfarb–Idnani dual active set) | `solvers` | ★★★★☆ |
3332

@@ -56,15 +55,6 @@ the library does not yet expose. Detailed below under
5655

5756
## Tier 3 — Moderate ★★★☆☆
5857

59-
### 63. Incremental QR least squares
60-
- **What:** Streaming Givens-QR accumulation of an unbounded number of rows into a `P×P` factor, with a
61-
rank-revealing SVD solve (minimum-norm solution, identifiable directions, residual RMS).
62-
- **Embedded value:** Parameter identification from long logs in fixed memory without squaring the
63-
condition number.
64-
- **Algorithm / paper:** G. Golub, C. Van Loan, §5.1.8 and §5.5; Å. Björck (1996), Ch. 2.
65-
- **Reuses:** `GivensRotation`, `SingularValueDecomposition`.
66-
- **Spec:** [roadmap/estimators/offline/IncrementalQrLeastSquares](roadmap/estimators/offline/IncrementalQrLeastSquares/implementation.md).
67-
6858
### 64. Levenberg–Marquardt
6959
- **What:** Damped Gauss–Newton for small nonlinear least squares with a problem-supplied retraction
7060
(vectors, bounds, rotations) and a pluggable damping policy.
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Incremental QR Least Squares
2+
3+
## Overview & Motivation
4+
5+
Identification and calibration runs produce long logs — motor current against acceleration, joint
6+
torque against motion — that a microcontroller cannot store. Accumulating the normal equations fits
7+
in memory but squares the condition number, which single precision cannot afford. Real regressors
8+
are also often rank-deficient: a parameter never shows up in the data, or only in a fixed
9+
combination with another, and a plain solver then returns garbage.
10+
11+
Incremental QR least squares rotates each arriving row into a small triangular summary and forgets
12+
it: memory stays fixed however long the log runs and the data matrix is never squared. The final
13+
solve decomposes the summary into singular directions, inverts the strong ones, drops the weak ones,
14+
and reports which parameter combinations the data could actually determine.
15+
16+
## Mathematical Theory
17+
18+
### Streaming QR Factor
19+
20+
Rows $(\mathbf{y}_k^T, \tau_k)$ with $\mathbf{y}_k \in \mathbb{R}^P$ arrive one at a time, $m$
21+
unbounded. The task is $\min_{\boldsymbol{\pi}} \|Y\boldsymbol{\pi} - \boldsymbol{\tau}\|_2$ over the
22+
stacked rows, taking the minimum-norm minimizer. Write
23+
$Y = Q \begin{bmatrix} R \\ 0 \end{bmatrix}$ with $Q$ orthogonal and $R$ upper triangular
24+
$P \times P$, and split $Q^T\boldsymbol{\tau} = (\mathbf{d}, \boldsymbol{\tau}_\perp)$. Orthogonal maps
25+
preserve norms, so
26+
27+
$$\|Y\boldsymbol{\pi} - \boldsymbol{\tau}\|^2 = \|R\boldsymbol{\pi} - \mathbf{d}\|^2 + \rho,
28+
\qquad \rho = \|\boldsymbol{\tau}_\perp\|^2$$
29+
30+
The problem lives in $R$, $\mathbf{d}$ and $\rho$, the part of the right-hand side no
31+
$\boldsymbol{\pi}$ can reach: $P^2 + P + 2$ scalars with the row count. $Q$ is never formed.
32+
33+
**Row update.** Place the new row beneath $[\,R \mid \mathbf{d}\,]$. For $j = 1, \dots, P$, skip the
34+
step if $y_j = 0$; otherwise rotate row $j$ of $R$ and the incoming row in their common plane:
35+
36+
$$\begin{bmatrix} c & s \\ -s & c \end{bmatrix}
37+
\begin{bmatrix} R_{j,j:P} & d_j \\ y_{j:P} & \tau \end{bmatrix},
38+
\qquad c = \frac{R_{jj}}{\ell},\; s = \frac{y_j}{\ell},\; \ell = \sqrt{R_{jj}^2 + y_j^2}$$
39+
40+
This zeroes $y_j$, leaves $R_{jj} = \ell \ge 0$ and touches only columns $j$ to $P$. After the sweep
41+
the incoming row is zero in every column, so its leftover right-hand side $\tilde{\tau}$ reads
42+
$0 \cdot \boldsymbol{\pi} = \tilde{\tau}$ and adds $\tilde{\tau}^2$ to $\rho$ whatever
43+
$\boldsymbol{\pi}$ is. A zero pivot degenerates to a swap and a zero pair to the identity.
44+
45+
**Why not normal equations.** $Y^TY = R^TR$, yet the singular values of $R$ are those of $Y$: the
46+
factor works at $\kappa(Y)$, the normal equations at $\kappa(Y)^2$. With single-precision
47+
$\epsilon \approx 1.2 \times 10^{-7}$ they fail near $\kappa(Y) \approx \epsilon^{-1/2} \approx 3
48+
\times 10^3$; the factor lasts to about $\epsilon^{-1}$.
49+
50+
### Rank-Revealing Solve
51+
52+
Take $R = U\Sigma V^T$ with $\sigma_0 \ge \dots \ge \sigma_{P-1} \ge 0$. In the coordinates
53+
$\mathbf{z} = V^T\boldsymbol{\pi}$,
54+
55+
$$\|R\boldsymbol{\pi} - \mathbf{d}\|^2 = \sum_{i=0}^{P-1} \left(\sigma_i z_i - \mathbf{u}_i^T\mathbf{d}\right)^2$$
56+
57+
The rank is $r = \#\{\, i : \sigma_i > \text{tol} \cdot \sigma_0 \,\}$. For $i < r$ the term is zeroed by
58+
$z_i = \mathbf{u}_i^T\mathbf{d} / \sigma_i$; for $i \ge r$ it cannot be removed and $z_i$ is free, so
59+
the minimum-norm choice is $z_i = 0$. The discarded terms are charged to the residual:
60+
61+
$$\hat{\boldsymbol{\pi}} = \sum_{i < r} \mathbf{v}_i \, \frac{\mathbf{u}_i^T\mathbf{d}}{\sigma_i},
62+
\qquad
63+
\text{RMS} = \sqrt{\frac{\rho + \sum_{i \ge r} (\mathbf{u}_i^T\mathbf{d})^2}{m}}$$
64+
65+
The first $r$ rows of $V^T$ are the combinations $\mathbf{v}_i^T\boldsymbol{\pi}$ the data determine;
66+
the rest are the unidentifiable directions. Moving $\boldsymbol{\pi}$ along them leaves the fit
67+
unchanged, and $\hat{\boldsymbol{\pi}}$ is orthogonal to them.
68+
69+
Scaling a row and its right-hand side by $\sqrt{w_k}$ gives weighted least squares. Every step is
70+
orthogonal, so row order changes the result only by rounding. With no rows there is no result; a
71+
reset clears the state.
72+
73+
## Complexity Analysis
74+
75+
| Operation | Time | Space | Notes |
76+
|------------------------|-----------------|----------------|------------------------------------------------------|
77+
| Row, sparse (best) | $O(P)$ | $P^2 + P + 2$ | Zero entries skipped; one short rotation per nonzero |
78+
| Row, dense (worst) | $O(P^2)$ | $P^2 + P + 2$ | $P$ rotations on $P - j + 1$ pairs, $P$ square roots |
79+
| Solve | $O(P^3)$ | $O(P^2)$ extra | One $P \times P$ SVD, independent of $m$ |
80+
| Whole stream, $m$ rows | $O(mP^2 + P^3)$ | $P^2 + P + 2$ | Memory does not grow with $m$ |
81+
82+
At $P = 60$ the state is about 14 KB in single precision. Accumulation can run at the sample rate;
83+
the solve is an offline or commissioning step. No heap, no recursion.
84+
85+
## Step-by-Step Walkthrough
86+
87+
**Full rank.** Rows $(1, x)$ at $x = (0, 1, 2, 3)$, $\boldsymbol{\tau} = (2.1, 4.9, 8.1, 10.9)$:
88+
89+
| Rows | $R_{00}$ | $R_{01}$ | $R_{11}$ | $d_0$ | $d_1$ | $\rho$ |
90+
|------|----------|----------|----------|--------|--------|--------|
91+
| 1 | 1 | 0 | 0 | 2.1 | 0 | 0 |
92+
| 2 | 1.4142 | 0.7071 | 0.7071 | 4.9497 | 1.9799 | 0 |
93+
| 3 | 1.7321 | 1.7321 | 1.4142 | 8.7180 | 4.2426 | 0.0267 |
94+
| 4 | 2 | 3 | 2.2361 | 13 | 6.6188 | 0.0320 |
95+
96+
Row 1 meets a zero pivot and is adopted; row 2 rotates by $45^\circ$ and fits two points exactly;
97+
from row 3 on, $\rho$ grows. With $\sigma \approx (4.1000, 1.0908)$, $r = 2$ and back-substitution
98+
gives $\hat{\pi}_1 = d_1 / R_{11} = 2.96$, $\hat{\pi}_0 = (13 - 3 \cdot 2.96)/2 = 2.06$, with
99+
RMS $= \sqrt{0.032 / 4} = 0.089443$.
100+
101+
**Rank-deficient.** Rows $(1, x, x)$ with $\boldsymbol{\tau} = 2 + 3x$: two equal columns leave the
102+
third row of $R$ zero, $\rho = 0$, and $\sigma = (5.540482,\, 1.141517,\, 0)$, so $r = 2$. Only
103+
$\pi_1 + \pi_2 = 3$ is determined; the minimum-norm split is $\hat{\boldsymbol{\pi}} = (2, 1.5, 1.5)$.
104+
The last row of $V^T$ is the unidentifiable direction $(0, 1, -1)/\sqrt2$.
105+
106+
Verified against double-precision normal equations on random 3-parameter problems of 5 to 205 rows:
107+
parameters agree to about $3 \times 10^{-6}$.
108+
109+
## Pitfalls & Edge Cases
110+
111+
- **Column scaling.** The rank test is relative to $\sigma_0$, so a column in large units hides the
112+
others. Scale columns to comparable magnitude before accumulating.
113+
- **Tolerance.** Too small admits noise-level directions and the solution grows like $1/\sigma_i$; too
114+
large discards real ones, visible as a higher RMS. About $10^{-5}$ is a single-precision start.
115+
- **RMS is not a noise estimate.** It divides by $m$, not $m - r$; scale its square by $m / (m - r)$
116+
for an unbiased variance.
117+
- **Few or repeated rows.** Fewer rows than parameters is valid, with rank at most $m$; rows that
118+
repeat earlier ones leave the rank unchanged.
119+
- **Long runs.** $\rho$ only grows; once it is about $2^{24}$ times one term, new terms vanish and the
120+
RMS turns optimistic. One NaN row poisons the state until a reset.
121+
122+
## Variants & Generalizations
123+
124+
- **Ridge.** Feed $P$ extra rows $\sqrt{\lambda}\,\mathbf{e}_j$ with zero right-hand side. Replacing
125+
$1/\sigma_i$ by $\sigma_i / (\sigma_i^2 + \lambda)$ filters smoothly instead of cutting off.
126+
- **Forgetting.** Scaling $R$, $\mathbf{d}$ by $\sqrt{\lambda}$ and $\rho$ by $\lambda$ before each row
127+
gives exponentially weighted square-root recursive least squares.
128+
- **Merging logs.** Rotating the rows of one summary $[\,R \mid \mathbf{d}\,]$ into another, and adding
129+
the $\rho$ values and counts, combines separately accumulated segments.
130+
131+
## Applications
132+
133+
- Inertial-parameter identification of robot arms, rank-deficient by construction
134+
- Motor and drive parameter identification from long logs
135+
- Friction identification: if the axis only ever moves one way, the sign column duplicates the offset
136+
column and the unidentifiable direction says so
137+
- Sensor calibration from data streams too long to store
138+
139+
## Connections to Other Algorithms
140+
141+
- [Givens Rotation](../math/GivensRotation.md): the plane rotation that zeroes one entry per step.
142+
- [Singular Value Decomposition](../solvers/SingularValueDecomposition.md): reveals rank and directions.
143+
- [QR Decomposition](../solvers/QrDecomposition.md): the same factor, built row by row, no $Q$ kept.
144+
- [Recursive Least Squares](RecursiveLeastSquares.md): forgetting and a $P \times P$ covariance; here
145+
all rows weigh equally and rank is revealed.
146+
- [Linear Regression](LinearRegression.md): batch normal equations that store the samples.
147+
- [Total Least Squares](TotalLeastSquares.md): also SVD-based, but for noisy regressors, not rank.
148+
- [Friction Compensation](../controllers/FrictionCompensation.md): its Coulomb and viscous parameters
149+
are linear and identifiable this way.
150+
151+
## References & Further Reading
152+
153+
- G. H. Golub, C. F. Van Loan, *Matrix Computations*, 4th ed., Johns Hopkins University Press, 2013 —
154+
§5.1.8–5.3 (Givens QR, updating) and §5.5 (rank-deficient least squares).
155+
- Å. Björck, *Numerical Methods for Least Squares Problems*, SIAM, 1996.

‎doc/estimators/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Statistical estimation algorithms for fitting models to observed data and making
1111
| [Total Least Squares](TotalLeastSquares.md) | Errors-in-variables fit (noisy regressors) via the SVD of `[A | b]` |
1212
| [Yule-Walker](YuleWalker.md) | Autoregressive model parameter estimation via the Yule-Walker equations |
1313
| [Expectation-Maximization](ExpectationMaximization.md) | EM algorithm for Kalman filter parameter identification (Shumway-Stoffer) |
14+
| [Incremental QR Least Squares](IncrementalQrLeastSquares.md) | Streaming Givens QR in fixed memory with a rank-revealing SVD solve (minimum-norm, identifiable combinations) |
1415

1516
## Online Estimators (Streaming)
1617

‎numerical/estimators/offline/CMakeLists.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ target_link_libraries(numerical.estimators.offline ${NUMERICAL_VISIBILITY}
1414

1515
target_sources(numerical.estimators.offline PRIVATE
1616
ExpectationMaximization.hpp
17+
IncrementalQrLeastSquares.hpp
1718
LinearRegression.hpp
1819
PolynomialFitting.hpp
1920
TotalLeastSquares.hpp
@@ -22,6 +23,7 @@ target_sources(numerical.estimators.offline PRIVATE
2223

2324
numerical_add_coverage_sources(numerical.estimators.offline
2425
ExpectationMaximization.cpp
26+
IncrementalQrLeastSquares.cpp
2527
LinearRegression.cpp
2628
PolynomialFitting.cpp
2729
TotalLeastSquares.cpp
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#include "numerical/estimators/offline/IncrementalQrLeastSquares.hpp"
2+
3+
namespace estimators
4+
{
5+
template class IncrementalQrLeastSquares<float, 2>;
6+
template class IncrementalQrLeastSquares<float, 3>;
7+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#pragma once
2+
3+
#include "numerical/math/CompilerOptimizations.hpp"
4+
#include "numerical/math/GivensRotation.hpp"
5+
#include "numerical/math/Math.hpp"
6+
#include "numerical/math/Matrix.hpp"
7+
#include "numerical/solvers/SingularValueDecomposition.hpp"
8+
#include <cstddef>
9+
#include <optional>
10+
#include <type_traits>
11+
12+
namespace estimators
13+
{
14+
template<typename T, std::size_t Parameters>
15+
struct LeastSquaresResult
16+
{
17+
static_assert(std::is_floating_point_v<T>, "LeastSquaresResult supports floating-point types only");
18+
19+
math::Vector<T, Parameters> parameters{};
20+
std::size_t rank{ 0 };
21+
math::SquareMatrix<T, Parameters> rowSpace{};
22+
T residualRms{};
23+
};
24+
25+
template<typename T, std::size_t Parameters>
26+
class IncrementalQrLeastSquares
27+
{
28+
static_assert(std::is_floating_point_v<T>, "IncrementalQrLeastSquares supports floating-point types only");
29+
30+
public:
31+
using Regressor = math::Vector<T, Parameters>;
32+
using Result = LeastSquaresResult<T, Parameters>;
33+
34+
OPTIMIZE_FOR_SPEED void AddRow(const Regressor& row, T rhs);
35+
template<std::size_t BlockRows>
36+
void AddRows(const math::Matrix<T, BlockRows, Parameters>& block, const math::Vector<T, BlockRows>& rhs);
37+
std::optional<Result> Solve(T relativeTolerance) const;
38+
void Reset();
39+
std::size_t Rows() const;
40+
41+
private:
42+
math::SquareMatrix<T, Parameters> r{};
43+
Regressor d{};
44+
T residualSquares{};
45+
std::size_t rows{ 0 };
46+
};
47+
48+
template<typename T, std::size_t Parameters>
49+
OPTIMIZE_FOR_SPEED void IncrementalQrLeastSquares<T, Parameters>::AddRow(const Regressor& row, T rhs)
50+
{
51+
Regressor x{ row };
52+
for (std::size_t j = 0; j < Parameters; ++j)
53+
{
54+
if (x[j] == T{})
55+
continue;
56+
57+
const auto rotation{ math::ComputeGivens(r.at(j, j), x[j]) };
58+
for (std::size_t c = j; c < Parameters; ++c)
59+
math::ApplyGivens(rotation, r.at(j, c), x[c]);
60+
math::ApplyGivens(rotation, d[j], rhs);
61+
}
62+
63+
residualSquares += rhs * rhs;
64+
++rows;
65+
}
66+
67+
template<typename T, std::size_t Parameters>
68+
template<std::size_t BlockRows>
69+
void IncrementalQrLeastSquares<T, Parameters>::AddRows(const math::Matrix<T, BlockRows, Parameters>& block, const math::Vector<T, BlockRows>& rhs)
70+
{
71+
for (std::size_t i = 0; i < BlockRows; ++i)
72+
AddRow(block.template GetBlock<1, Parameters>(i, 0).Transpose(), rhs[i]);
73+
}
74+
75+
template<typename T, std::size_t Parameters>
76+
auto IncrementalQrLeastSquares<T, Parameters>::Solve(T relativeTolerance) const -> std::optional<Result>
77+
{
78+
solvers::SingularValueDecomposition<T, Parameters, Parameters> svd;
79+
if (rows == 0 || !svd.Decompose(r))
80+
return std::nullopt;
81+
82+
const auto& sigma{ svd.SingularValues() };
83+
const auto& u{ svd.U() };
84+
const auto& v{ svd.V() };
85+
const T threshold{ relativeTolerance * sigma[0] };
86+
87+
Result result{};
88+
T unexplained{ residualSquares };
89+
for (std::size_t i = 0; i < Parameters; ++i)
90+
{
91+
T projection{};
92+
for (std::size_t k = 0; k < Parameters; ++k)
93+
projection += u.at(k, i) * d[k];
94+
95+
if (sigma[i] > threshold)
96+
{
97+
++result.rank;
98+
for (std::size_t k = 0; k < Parameters; ++k)
99+
result.parameters[k] += v.at(k, i) * projection / sigma[i];
100+
}
101+
else
102+
unexplained += projection * projection;
103+
}
104+
105+
result.rowSpace = v.Transpose();
106+
result.residualRms = math::Sqrt(unexplained / static_cast<T>(rows));
107+
return result;
108+
}
109+
110+
template<typename T, std::size_t Parameters>
111+
void IncrementalQrLeastSquares<T, Parameters>::Reset()
112+
{
113+
r = math::SquareMatrix<T, Parameters>{};
114+
d = Regressor{};
115+
residualSquares = T{};
116+
rows = 0;
117+
}
118+
119+
template<typename T, std::size_t Parameters>
120+
std::size_t IncrementalQrLeastSquares<T, Parameters>::Rows() const
121+
{
122+
return rows;
123+
}
124+
125+
#ifdef NUMERICAL_TOOLBOX_COVERAGE_BUILD
126+
extern template class IncrementalQrLeastSquares<float, 2>;
127+
extern template class IncrementalQrLeastSquares<float, 3>;
128+
#endif
129+
}

‎numerical/estimators/offline/test/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ numerical_link_qemu_runtime(numerical.estimators.offline_test)
1111

1212
target_sources(numerical.estimators.offline_test PRIVATE
1313
TestExpectationMaximization.cpp
14+
TestIncrementalQrLeastSquares.cpp
1415
TestLinearRegression.cpp
1516
TestPolynomialFitting.cpp
1617
TestTotalLeastSquares.cpp

0 commit comments

Comments
 (0)