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