1 //==============================================================================
2 //
3 // This file is part of GPSTk, the GPS Toolkit.
4 //
5 // The GPSTk is free software; you can redistribute it and/or modify
6 // it under the terms of the GNU Lesser General Public License as published
7 // by the Free Software Foundation; either version 3.0 of the License, or
8 // any later version.
9 //
10 // The GPSTk is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with GPSTk; if not, write to the Free Software Foundation,
17 // Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110, USA
18 //
19 // This software was developed by Applied Research Laboratories at the
20 // University of Texas at Austin.
21 // Copyright 2004-2020, The Board of Regents of The University of Texas System
22 //
23 //==============================================================================
24
25 //==============================================================================
26 //
27 // This software was developed by Applied Research Laboratories at the
28 // University of Texas at Austin, under contract to an agency or agencies
29 // within the U.S. Department of Defense. The U.S. Government retains all
30 // rights to use, duplicate, distribute, disclose, or release this software.
31 //
32 // Pursuant to DoD Directive 523024
33 //
34 // DISTRIBUTION STATEMENT A: This software has been approved for public
35 // release, distribution is unlimited.
36 //
37 //==============================================================================
38
39 /// @file SRIleastSquares.cpp
40 /// Include file defining class SRIleastSquares, which inherits class SRI and
41 /// implements a general least squares algorithm that includes linear or linearized
42 /// problems, weighting, robust estimation, and sequential estimation.
43
44 //------------------------------------------------------------------------------------
45 // GPSTk includes
46 #include "SRIleastSquares.hpp"
47 #include "RobustStats.hpp"
48 #include "StringUtils.hpp"
49
50 //------------------------------------------------------------------------------------
51 using namespace std;
52
53 namespace gpstk {
54 using namespace StringUtils;
55
56 //------------------------------------------------------------------------------------
57 // empty constructor
SRIleastSquares(void)58 SRIleastSquares::SRIleastSquares(void) throw()
59 { defaults(); }
60
61 //------------------------------------------------------------------------------------
62 // constructor given the dimension N.
SRIleastSquares(const unsigned int N)63 SRIleastSquares::SRIleastSquares(const unsigned int N)
64 throw()
65 {
66 defaults();
67 R = Matrix<double>(N,N,0.0);
68 Z = Vector<double>(N,0.0);
69 names = Namelist(N);
70 }
71
72 //------------------------------------------------------------------------------------
73 // constructor given a Namelist, its dimension determines the SRI dimension.
SRIleastSquares(const Namelist & NL)74 SRIleastSquares::SRIleastSquares(const Namelist& NL)
75 throw()
76 {
77 defaults();
78 if(NL.size() <= 0) return;
79 R = Matrix<double>(NL.size(),NL.size(),0.0);
80 Z = Vector<double>(NL.size(),0.0);
81 names = NL;
82 }
83
84 //------------------------------------------------------------------------------------
85 // explicit constructor - throw if the dimensions are inconsistent.
SRIleastSquares(const Matrix<double> & Rin,const Vector<double> & Zin,const Namelist & NLin)86 SRIleastSquares::SRIleastSquares(const Matrix<double>& Rin,
87 const Vector<double>& Zin,
88 const Namelist& NLin)
89 {
90 defaults();
91 if(Rin.rows() != Rin.cols() ||
92 Rin.rows() != Zin.size() ||
93 Rin.rows() != NLin.size()) {
94 MatrixException me("Invalid input dimensions: R is "
95 + asString<int>(Rin.rows()) + "x"
96 + asString<int>(Rin.cols()) + ", Z has length "
97 + asString<int>(Zin.size()) + ", and NL has length "
98 + asString<int>(NLin.size())
99 );
100 GPSTK_THROW(me);
101 }
102 R = Rin;
103 Z = Zin;
104 names = NLin;
105 }
106
107 //------------------------------------------------------------------------------------
108 // operator=
operator =(const SRIleastSquares & right)109 SRIleastSquares& SRIleastSquares::operator=(const SRIleastSquares& right)
110 throw()
111 {
112 R = right.R;
113 Z = right.Z;
114 names = right.names;
115 iterationsLimit = right.iterationsLimit;
116 convergenceLimit = right.convergenceLimit;
117 divergenceLimit = right.divergenceLimit;
118 doWeight = right.doWeight;
119 doRobust = right.doRobust;
120 doLinearize = right.doLinearize;
121 doSequential = right.doSequential;
122 doVerbose = right.doVerbose;
123 valid = right.valid;
124 number_iterations = right.number_iterations;
125 number_batches = right.number_batches;
126 rms_convergence = right.rms_convergence;
127 condition_number = right.condition_number;
128 Xsave = right.Xsave;
129 return *this;
130 }
131
132 //------------------------------------------------------------------------------------
133 // SRI least squares update (not the Kalman measurement update).
134 // Given data and measurement covariance, compute a solution and
135 // covariance using the appropriate least squares algorithm.
136 // @param D Data vector, length M
137 // Input: raw data
138 // Output: post-fit residuals
139 // @param X Solution vector, length N
140 // Input: nominal solution X0 (zero when doLinearized is false)
141 // Output: final solution
142 // @param Cov Covariance matrix, dimension (N,N)
143 // Input: (If doWeight is true) inverse measurement covariance
144 // or weight matrix(M,M)
145 // Output: Solution covariance matrix (N,N)
146 // @param LSF Pointer to a function which is used to define the equation to be solved.
147 // LSF arguments are:
148 // X Nominal solution (input)
149 // f Values of the equation f(X) (length M) (output)
150 // P Partials matrix df/dX evaluated at X (dimension M,N) (output)
151 // When doLinearize is false, LSF should ignore X and return the (constant)
152 // partials matrix in P and zero in f.
153 // @return 0 ok
154 // -1 Problem is underdetermined (M<N) // TD -- naturalized sol?
155 // -2 Problem is singular
156 // -3 Algorithm failed to converge
157 // -4 Algorithm diverged
158 //
159 // Reference for robust least squares: Mason, Gunst and Hess,
160 // "Statistical Design and Analysis of Experiments," Wiley, New York, 1989, pg 593.
161 //
162 // Notes on the algorithm:
163 // Least squares, including linearized (iterative) and sequential processing.
164 // This class will solve the equation f(X) = D, a vector equation in which the
165 // solution vector X is of length N, and the data vector D is of length M.
166 // The function f(X) may be linear, in which case it is of the form
167 // P*X=D where P is a constant matrix,
168 // or non-linear, in which case it will be linearized by expanding about a given
169 // nominal solution X0:
170 // df |
171 // -- | * dX = D - f(X0),
172 // dX |X=X0
173 // where dX is defined as (X-X0), the new solution is X, and the partials matrix is
174 // P=(df/dX)|X=X0. Dimensions are P(M,N)*dX(N) = D(M) - f(X0)(M).
175 // Linearized problems are iterated until the solution converges (stops changing).
176 //
177 // The solution may be weighted by a measurement covariance matrix MCov,
178 // or weight matrix W (in which case MCov = inverse(W)). MCov must be non-singular.
179 //
180 // Options are to make the algorithm linearized (via the boolean input variable
181 // doLinearize) and/or sequential (doSequential).
182 //
183 // - linearized. When doLinearize is true, the algorithm solves the linearized
184 // version of the measurement equation (see above), rather than the simple
185 // linear version P*X=D. Also when doLinearize is true, the code will iterate
186 // (repeat until convergence) the linearized algorithm; if you don't want to
187 // iterate, set the limit on the number of iterations to zero.
188 // NB In this case, a solution must be found for each nominal solution
189 // (i.e. the information matrix must be non-singular); otherwise there can be
190 // no iteration.
191 //
192 // - sequential. When doSequential is true, the class will save the accumulated
193 // information from all the calls to this routine since the last reset()
194 // within the class. This means the resulting solution is determined by ALL the
195 // data fed to the class since the last reset(). In this case the data is fed
196 // to the algorithm in 'batches', which may be of any size.
197 //
198 // NB When doLinearize is true, the information stored in the class has a
199 // different interpretation than it does in the linear case.
200 // Calling Solve(X,Cov) will NOT give the solution vector X, but rather the
201 // latest update (X-X0) = (X-Xsave).
202 //
203 // NB In the linear case, the result you get from sequentially processing
204 // a large dataset in many small batches is identical to what you would get
205 // by processing all the data in one big batch. This is NOT true in the
206 // linearized case, because the information at each batch is dependent on the
207 // nominal state. See the next comment.
208 //
209 // NB Sequential, linearized LS really makes sense only when the state is
210 // changing. It is difficult to get a good solution in this case with small
211 // batches, because the stored information is dependent on the (final) state
212 // solution at each batch. Start with a good nominal state, or with a large
213 // batch of data that will produce one.
214 //
215 // The general Least Squares algorithm is:
216 // 0. set i=0.
217 // 1. If non-sequential, or if this is the first call, set R=z=0
218 // (However doing this prevents you from adding apriori/constraint information)
219 // 2. Let X = X0 (X0 = initial nominal solution - input). if linear, X0==0.
220 // 3. Save SRIsave=SRI and X0save=X0 (SRI is the pair R,z)
221 // 4. start iteration i here.
222 // 5. increment the number of iterations i
223 // 6. Compute partials matrix P and f(X0) by calling LSF(X0,f,P).
224 // if linear, LSF returns the constant P and f(X0)=0.
225 // 7. Set R = SRIsave.R + P(T)*inverse(MCov)*P (T means transpose)
226 // 8. Set z = SRIsave.z + P(T)*inverse(MCov)*(D-f(X0))
227 // 9. [The measurement equation is now P*DX=d-F(X0)
228 // where DX=(X-X0save); in the linear case it is PX = d and DX = X ]
229 // 10. Solve z = Rx to get
230 // Cov = inverse(R)
231 // and DX = inverse(R)*z OR
232 // 11. Set X = X0save + DX
233 // [or in the linear case X = DX
234 // 12. Compute RMS change in X: rms = ||X-X0||/N (not X-X0save)
235 // 13. if linear goto quit [else linearized]
236 // 14. If rms > divergence limit, goto quit(failure).
237 // 15. If i > 1 and rms < convergence limit, goto quit(success)
238 // 16. If i (number of iterations) >= iteration limit, goto quit(failure)
239 // 17. Set X0 = X
240 // 18. Return to step 5.
241 // 19. quit: if(sequential and failed) set SRI=SRIsave.
242 //
243 // From the code:
244 // 1a. Save SRI (i.e. R, Z) in Rapriori, Zapriori
245 // 2a. If non-sequential, or if this is the first call, set R=z=0 -- DON'T
246 // 3a. If sequential and not the first call, X = Xsave
247 // 4a. if linear, X0=0; else X0 is input. Let NominalX = X0
248 // 5a. set number_iterations = 0
249 // 6a. start iteration
250 // 7a. increment number_iterations
251 // 8a. get partials and f from LSfunc using NominalX
252 // 9a. if robust, compute weight matrix
253 // 10a. if number_iterations > 1, restore (R,Z) = (Rapriori,Zapriori)
254 // 11a. MU : R,Z,Partials,D-f(NominalX),MeasCov(if weighted)
255 // 12a. Invert to get Xsol [ Xsol = X-NominalX or, if linear = X]
256 // 13a. if linearized, add NominalX to Xsol; Xsol now == X = new estimate
257 // 14a. if linear and not robust, quit here
258 // 15a. if linearized, compute rms_convergence = RMS(Xsol - NominalX)
259 // 16a. if robust, recompute weights and define rms_convergence = RMS(old-new wts)
260 // 17a. failed? if so, and sequential, restore (R,Z) = (Rapriori,Zapriori); quit
261 // 18a. success? quit
262 // 19a. if linearized NominalX = Xsol; if robust NominalX = X
263 // 20a. iterate - return to 6a.
264 // 21a. set X = Xsol for return value
265 // 22a. save X for next time : Xsave = X
266 //
dataUpdate(Vector<double> & D,Vector<double> & X,Matrix<double> & Cov,void (LSF)(Vector<double> & X,Vector<double> & f,Matrix<double> & P))267 int SRIleastSquares::dataUpdate(Vector<double>& D,
268 Vector<double>& X,
269 Matrix<double>& Cov,
270 void (LSF)(Vector<double>& X,
271 Vector<double>& f,
272 Matrix<double>& P))
273 {
274 const int M = D.size();
275 const int N = R.rows();
276 if(doVerbose) cout << "\nSRIleastSquares::leastSquaresUpdate : M,N are "
277 << M << "," << N << endl;
278
279 // errors
280 if(N == 0) {
281 MatrixException me("Called with zero-sized SRIleastSquares");
282 GPSTK_THROW(me);
283 }
284 if(doLinearize && M < N) {
285 MatrixException me(
286 string("When linearizing, problem must not be underdetermined:\n")
287 + string(" data dimension is ") + asString(M)
288 + string(" while state dimension is ") + asString(N));
289 GPSTK_THROW(me);
290 }
291 if(doSequential && R.rows() != X.size()) {
292 MatrixException me("Sequential problem has inconsistent dimensions:\n SRI is "
293 + asString<int>(R.rows()) + "x"
294 + asString<int>(R.cols()) + " while X has length "
295 + asString<int>(X.size()));
296 GPSTK_THROW(me);
297 }
298 if(doWeight && doRobust) {
299 MatrixException me("Cannot have doWeight and doRobust both true.");
300 GPSTK_THROW(me);
301 }
302 // TD disallow Robust and Linearized ? why?
303 // TD disallow Robust and Sequential ? why?
304
305 try {
306 int i,iret;
307 double big,small;
308 Vector<double> f(M),Xsol(N),NominalX,Res(M),Wts(M,1.0),OldWts(M,1.0);
309 Matrix<double> Partials(M,N),MeasCov(M,M);
310 const Matrix<double> Rapriori(R);
311 const Vector<double> Zapriori(Z);
312
313 // save measurement covariance matrix
314 if(doWeight) MeasCov=Cov;
315
316 // NO ... this prevents you from giving it apriori information...
317 // if the first time, clear the stored information
318 //if(!doSequential || number_batches==0)
319 // zeroAll();
320
321 // if sequential and not the first call, NominalX must be the last solution
322 if(doSequential && number_batches != 0) X = Xsave;
323
324 // nominal solution
325 if(!doLinearize) {
326 if((int)X.size() != N) X=Vector<double>(N);
327 X = 0.0;
328 }
329 NominalX = X;
330
331 valid = false;
332 condition_number = 0.0;
333 rms_convergence = 0.0;
334 number_iterations = 0;
335 iret = 0;
336
337 // iteration loop
338 do {
339 number_iterations++;
340
341 // call LSF to get f(NominalX) and Partials(NominalX)
342 LSF(NominalX,f,Partials);
343
344 // Res will be both pre- and post-fit data residuals
345 Res = D-f;
346 if(doVerbose) {
347 cout << "\nSRIleastSquares::leastSquaresUpdate :";
348 if(doLinearize || doRobust)
349 cout << " Iteration " << number_iterations;
350 cout << endl;
351 LabeledVector LNX(names,NominalX);
352 LNX.message(" Nominal X:");
353 cout << LNX << endl;
354 cout << " Pre-fit data residuals: "
355 << fixed << setprecision(6) << Res << endl;
356 }
357
358 // build measurement covariance matrix for robust LS
359 if(doRobust) {
360 MeasCov = 0.0;
361 for(i=0; i<M; i++) MeasCov(i,i) = 1.0 / (Wts(i)*Wts(i));
362 }
363
364 // restore apriori information
365 if(number_iterations > 1) {
366 R = Rapriori;
367 Z = Zapriori;
368 }
369
370 // update information with simple MU
371 if(doVerbose) {
372 cout << " Meas Cov:";
373 for(i=0; i<M; i++) cout << " " << MeasCov(i,i);
374 cout << endl;
375 cout << " Partials:\n" << Partials << endl;
376 }
377 //if(doRobust || doWeight)
378 // measurementUpdate(Partials,Res,MeasCov);
379 //else
380 // measurementUpdate(Partials,Res);
381 {
382 Matrix<double> P(Partials);
383 Matrix<double> CHL;
384 if(doRobust || doWeight) {
385 CHL = lowerCholesky(MeasCov);
386 Matrix<double> L = inverseLT(CHL);
387 P = L * P;
388 Res = L * Res;
389 }
390
391 // update with whitened information
392 SrifMU(R, Z, P, Res);
393
394 // un-whiten the residuals
395 if(doRobust || doWeight) // NB same if above creates CHL
396 Res = CHL * Res;
397 }
398
399 if(doVerbose) {
400 cout << " Updated information matrix\n" << LabeledMatrix(names,R) << endl;
401 cout << " Updated information vector\n" << LabeledVector(names,Z) << endl;
402 }
403
404 // invert
405 try { getStateAndCovariance(Xsol,Cov,&small,&big); }
406 catch(SingularMatrixException& sme) {
407 iret = -2;
408 break;
409 }
410 condition_number = big/small;
411 if(doVerbose) {
412 cout << " Condition number: " << scientific << condition_number
413 << fixed << endl;
414 cout << " Post-fit data residuals: "
415 << fixed << setprecision(6) << Res << endl;
416 }
417
418 // update X: when linearized, solution = dX
419 if(doLinearize) {
420 Xsol += NominalX;
421 }
422 if(doVerbose) {
423 LabeledVector LXsol(names,Xsol);
424 LXsol.message(" Updated X:");
425 cout << LXsol << endl;
426 }
427
428 // linear non-robust is done..
429 if(!doLinearize && !doRobust) break;
430
431 // test for convergence of linearization
432 if(doLinearize) {
433 rms_convergence = RMS(Xsol - NominalX);
434 if(doVerbose) {
435 cout << " RMS convergence : "
436 << scientific << rms_convergence << fixed << endl;
437 }
438 }
439
440 // test for convergence of robust weighting, and compute new weights
441 if(doRobust) {
442 // must de-weight post-fit residuals
443 LSF(Xsol,f,Partials);
444 Res = D-f;
445
446 // compute a new set of weights
447 double mad,median;
448 //for(mad=0.0,i=0; i<M; i++)
449 // mad += Wts(i)*Res(i)*Res(i);
450 //mad = sqrt(mad)/sqrt(Robust::TuningA*(M-1));
451 mad = Robust::MedianAbsoluteDeviation(&(Res[0]),Res.size(),median);
452
453 OldWts = Wts;
454 for(i=0; i<M; i++) {
455 if(Res(i) < -RobustTuningT*mad)
456 Wts(i) = -RobustTuningT*mad/Res(i);
457 else if(Res(i) > RobustTuningT*mad)
458 Wts(i) = RobustTuningT*mad/Res(i);
459 else
460 Wts(i) = 1.0;
461 }
462
463 // test for convergence
464 rms_convergence = RMS(OldWts - Wts);
465 if(doVerbose) cout << " Convergence: "
466 << scientific << setprecision(3) << rms_convergence << endl;
467 }
468
469 // failures
470 if(rms_convergence > divergenceLimit) iret=-4;
471 if(number_iterations >= iterationsLimit) iret=-3;
472 if(iret) {
473 if(doSequential) {
474 R = Rapriori;
475 Z = Zapriori;
476 }
477 break;
478 }
479
480 // success
481 if(number_iterations > 1 && rms_convergence < convergenceLimit) break;
482
483 // prepare for another iteration
484 if(doLinearize)
485 NominalX = Xsol;
486 if(doRobust)
487 NominalX = X;
488
489 } while(1); // end iteration loop
490
491 number_batches++;
492 if(doVerbose) cout << "Return from SRIleastSquares::leastSquaresUpdate\n\n";
493
494 if(iret) return iret;
495 valid = true;
496
497 // output the solution
498 Xsave = X = Xsol;
499
500 // put residuals of fit into data vector, or weights if Robust
501 if(doRobust) D = OldWts;
502 else D = Res;
503
504 return iret;
505 }
506 catch(Exception& e) { GPSTK_RETHROW(e); }
507 }
508
509 //------------------------------------------------------------------------------------
510 // output operator
operator <<(ostream & os,const SRIleastSquares & srif)511 ostream& operator<<(ostream& os, const SRIleastSquares& srif)
512 {
513 Namelist NL(srif.names);
514 NL += string("State");
515 Matrix<double> A;
516 A = srif.R || srif.Z;
517 LabeledMatrix LM(NL,A);
518 LM.setw(os.width());
519 LM.setprecision(os.precision());
520 os << LM;
521 return os;
522 }
523
524 //------------------------------------------------------------------------------------
525 // reset the computation, i.e. remove all stored information
zeroAll(void)526 void SRIleastSquares::zeroAll(void)
527 {
528 SRI::zeroAll();
529 Xsave = 0.0;
530 number_batches = 0;
531 }
532
533 //------------------------------------------------------------------------------------
534 // reset the computation, i.e. remove all stored information, and
535 // optionally change the dimension. If N is not input, the
536 // dimension is not changed.
537 // @param N new SRIleastSquares dimension (optional).
Reset(const int N)538 void SRIleastSquares::Reset(const int N)
539 {
540 try {
541 if(N > 0 && N != (int)R.rows()) {
542 R.resize(N,N,0.0);
543 Z.resize(N,0.0);
544 }
545 else
546 SRI::zeroAll(N);
547 if(N > 0) Xsave.resize(N);
548 Xsave = 0.0;
549 number_batches = 0;
550 }
551 catch(Exception& e) { GPSTK_RETHROW(e); }
552 }
553
554 //------------------------------------------------------------------------------------
555 } // end namespace gpstk
556