Isis 3 Programmer Reference
LeastSquares.cpp
1
6/* SPDX-License-Identifier: CC0-1.0 */
7#include "jama/jama_svd.h"
8#include "jama/jama_qr.h"
9
10#include <armadillo>
11
12#include "LeastSquares.h"
13#include "IException.h"
14#include "IString.h"
15
16namespace Isis {
24 int sparseRows, int sparseCols, bool jigsaw) {
25 p_jigsaw = jigsaw;
26 p_basis = &basis;
27 p_solved = false;
28 p_sparse = sparse;
29 p_sigma0 = 0.;
30
31 p_sparseRows = sparseRows;
32 p_sparseCols = sparseCols;
33
34
35 if (p_sparse) {
36
37 // make sure sparse nrows/ncols have been set
38 if (sparseRows == 0 || sparseCols == 0) {
39 QString msg = "If solving using sparse matrices, you must enter the "
40 "number of rows/columns";
41 throw IException(IException::Programmer, msg, _FILEINFO_);
42 }
43
44
45 p_sparseA.set_size(sparseRows, sparseCols);
46 p_normals.set_size(sparseCols, sparseCols);
47 p_ATb.resize(sparseCols, 1);
48 p_xSparse.resize(sparseCols);
49
50 if( p_jigsaw ) {
51 p_epsilonsSparse.resize(sparseCols);
52 std::fill_n(p_epsilonsSparse.begin(), sparseCols, 0.0);
53
54 p_parameterWeights.resize(sparseCols);
55 }
56
57 }
58 p_currentFillRow = -1;
59 }
60
64
96 void LeastSquares::AddKnown(const std::vector<double> &data, double result,
97 double weight) {
98 if((int) data.size() != p_basis->Variables()) {
99 QString msg = "Number of elements in data does not match basis [" +
100 p_basis->Name() + "] requirements";
101 throw IException(IException::Programmer, msg, _FILEINFO_);
102 }
103
104 p_expected.push_back(result);
105
106 if (weight == 1) {
107 p_sqrtWeight.push_back(weight);
108 }
109 else {
110 p_sqrtWeight.push_back(sqrt(weight));
111 }
112
113 if(p_sparse) {
114 FillSparseA(data);
115 }
116 else {
117 p_input.push_back(data);
118 }
119 }
120
121
122
137 void LeastSquares::FillSparseA(const std::vector<double> &data) {
138
139 p_basis->Expand(data);
140
141 p_currentFillRow++;
142
143 int ncolumns = (int)data.size();
144
145 for(int c = 0; c < ncolumns; c++) {
146 p_sparseA(p_currentFillRow, c) = p_basis->Term(c) * p_sqrtWeight[p_currentFillRow];
147 }
148 }
149
150
158 std::vector<double> LeastSquares::GetInput(int row) const {
159 if((row >= Rows()) || (row < 0)) {
160 QString msg = "Index out of bounds [Given = " + toString(row) + "]";
161 throw IException(IException::Programmer, msg, _FILEINFO_);
162 }
163 return p_input[row];
164 }
165
173 double LeastSquares::GetExpected(int row) const {
174 if((row >= Rows()) || (row < 0)) {
175 QString msg = "Index out of bounds [Given = " + toString(row) + "]";
176 throw IException(IException::Programmer, msg, _FILEINFO_);
177 }
178 return p_expected[row];
179 }
180
187 int LeastSquares::Rows() const {
188 return (int)p_input.size();
189 }
190
206
207 if((method == SPARSE && p_sparseRows == 0) ||
208 (method != SPARSE && Rows() == 0 )) {
209 p_solved = false;
210 QString msg = "No solution available because no input data was provided";
211 throw IException(IException::Unknown, msg, _FILEINFO_);
212 }
213
214 if(method == SVD) {
215 SolveSVD();
216 }
217 else if(method == QRD) {
218 SolveQRD();
219 }
220 else if(method == SPARSE) {
221 int column = SolveSparse();
222 return column;
223 }
224 return 0;
225 }
226
237
238 // We are solving Ax=b ... start by creating A
239 TNT::Array2D<double> A(p_input.size(), p_basis->Coefficients());
240 for(int r = 0; r < A.dim1(); r++) {
242 for(int c = 0; c < A.dim2(); c++) {
243 A[r][c] = p_basis->Term(c) * p_sqrtWeight[r];
244 }
245 }
246
247 // Ok use singular value decomposition to solve for the coefficients
248 // A = [U][S][V'] where [U] is MxN, [S] is NxN, [V'] is NxN transpose
249 // of [V]. We are solving for [A]x=b and need inverse of [A] such
250 // that x = [invA]b. Since inverse may not exist we use the
251 // pseudo-inverse [A+] from SVD which is [A+] = [V][invS][U']
252 // Our coefficents are then x = [A+]b where b is p_b.
253 JAMA::SVD<double> svd(A);
254
255 TNT::Array2D<double> V;
256 svd.getV(V);
257
258 // The inverse of S is the 1 over each diagonal element of S
259 TNT::Array2D<double> invS;
260 svd.getS(invS);
261
262 for(int i = 0; i < invS.dim1(); i++) {
263 if(invS[i][i] != 0.0) invS[i][i] = 1.0 / invS[i][i];
264 }
265
266 // Transpose U
267 TNT::Array2D<double> U;
268 svd.getU(U);
269 TNT::Array2D<double> transU(U.dim2(), U.dim1());
270
271 for(int r = 0; r < U.dim1(); r++) {
272 for(int c = 0; c < U.dim2(); c++) {
273 transU[c][r] = U[r][c];
274 }
275 }
276
277 // Now multiply everything together to get [A+]
278 TNT::Array2D<double> VinvS = TNT::matmult(V, invS);
279 TNT::Array2D<double> Aplus = TNT::matmult(VinvS, transU);
280
281 // Using Aplus and our b we can solve for the coefficients
282 TNT::Array2D<double> b(p_expected.size(), 1);
283
284 for(int r = 0; r < (int)p_expected.size(); r++) {
285 b[r][0] = p_expected[r] * p_sqrtWeight[r];
286 }
287
288 TNT::Array2D<double> coefs = TNT::matmult(Aplus, b);
289
290 // If the rank of the matrix is not large enough we don't
291 // have enough coefficients for the solution
292 if (coefs.dim1() < p_basis->Coefficients()) {
293 QString msg = "Unable to solve least-squares using SVD method. No "
294 "solution available. Not enough knowns or knowns are "
295 "co-linear ... [Unknowns = "
296 + toString(p_basis->Coefficients()) + "] [Knowns = "
297 + toString(coefs.dim1()) + "]";
298 throw IException(IException::Unknown, msg, _FILEINFO_);
299 }
300
301 // Set the coefficients in our basis equation
302 std::vector<double> bcoefs;
303 for (int i = 0; i < coefs.dim1(); i++) bcoefs.push_back(coefs[i][0]);
304
305 p_basis->SetCoefficients(bcoefs);
306
307 // Compute the errors
308 for(int i = 0; i < (int)p_input.size(); i++) {
309 double value = p_basis->Evaluate(p_input[i]);
310 p_residuals.push_back(value - p_expected[i]);
312 }
313 // calculate degrees of freedom (or redundancy)
314 // DOF = # observations + # constrained parameters - # unknown parameters
315 p_degreesOfFreedom = p_basis->Coefficients() - coefs.dim1();
316
317 if( p_degreesOfFreedom > 0.0 ) {
319 }
320
321 // check for p_sigma0 < 0
322 p_sigma0 = sqrt(p_sigma0);
323
324 // All done
325 p_solved = true;
326 }
327
340
341 // We are solving Ax=b ... start by creating an MxN matrix, A
342 TNT::Array2D<double> A(p_input.size(), p_basis->Coefficients());
343 for(int r = 0; r < A.dim1(); r++) {
345 for(int c = 0; c < A.dim2(); c++) {
346 A[r][c] = p_basis->Term(c) * p_sqrtWeight[r];
347 }
348 }
349
350 // Ok use to solve for the coefficients
351 // [A] = [Q][R] where [Q] is MxN and orthogonal and [R] is an NxN,
352 // upper triangular matrix. TNT provides the solve method that inverts
353 // [Q] and backsolves [R] to get the coefficients in the vector x.
354 // That is, we solve the system Rx = Q^T b
355 JAMA::QR<double> qr(A);
356
357 // Using A and our b we can solve for the coefficients
358 TNT::Array1D<double> b(p_expected.size());
359 for(int r = 0; r < (int)p_expected.size(); r++) {
360 b[r] = p_expected[r] * p_sqrtWeight[r];
361 }// by construction, we know the size of b is equal to M, so b is conformant
362
363 // Check to make sure the matrix is full rank before solving
364 // -- rectangular matrices must be full rank in order for the solve method
365 // to be successful
366 int full = qr.isFullRank();
367 if(full == 0) {
368 QString msg = "Unable to solve-least squares using QR Decomposition. "
369 "The upper triangular R matrix is not full rank";
370 throw IException(IException::Unknown, msg, _FILEINFO_);
371 }
372
373 TNT::Array1D<double> coefs = qr.solve(b);
374
375 // Set the coefficients in our basis equation
376 std::vector<double> bcoefs;
377 for(int i = 0; i < coefs.dim1(); i++) {
378 bcoefs.push_back(coefs[i]);
379 }
380 p_basis->SetCoefficients(bcoefs);
381
382 // Compute the errors
383 for(int i = 0; i < (int)p_input.size(); i++) {
384 double value = p_basis->Evaluate(p_input[i]);
385 p_residuals.push_back(value - p_expected[i]);
386 }
387
388 // All done
389 p_solved = true;
390 }
391
392
393
394
429
430 // form "normal equations" matrix by multiplying ATA
432
433 // Create the right-hand-side column vector 'b'
434 arma::mat b(p_sparseRows, 1);
435
436 // multiply each element of 'b' by it's associated weight
437 for ( int r = 0; r < p_sparseRows; r++ )
438 b(r,0) = p_expected[r] * p_sqrtWeight[r];
439
440 // form ATb
441 p_ATb = p_sparseA.t()*b;
442
443 // apply parameter weighting if Jigsaw (bundle adjustment)
444 if ( p_jigsaw ) {
445 for( int i = 0; i < p_sparseCols; i++) {
446 double weight = p_parameterWeights[i];
447
448 if( weight <= 0.0 )
449 continue;
450
451 p_normals(i, i) += weight;
452 p_ATb(i, 0) -= p_epsilonsSparse[i]*weight;
453 }
454 }
455
456 bool status = spsolve(p_xSparse, p_normals, p_ATb, "superlu");
457
458 if (status == false) {
459 QString msg = "Could not solve sparse least squares problem.";
460 throw IException(IException::Unknown, msg, _FILEINFO_);
461 }
462
463 // Set the coefficients in our basis equation
464 p_basis->SetCoefficients(arma::conv_to< std::vector<double> >::from(p_xSparse));
465
466 // if Jigsaw (bundle adjustment)
467 // add corrections into epsilon vector (keeping track of total corrections)
468 if ( p_jigsaw ) {
469 for( int i = 0; i < p_sparseCols; i++ )
471 }
472
473 // Compute the image coordinate residuals and sum into Sigma0
474 // (note this is exactly what was being done before, but with less overhead - I think)
475 // ultimately, we should not be using the A matrix but forming the normals
476 // directly. Then we'll have to compute the residuals by back projection
477
478 p_residuals.resize(p_sparseRows);
479 p_residuals = arma::conv_to< std::vector<double> >::from(p_sparseA*p_xSparse);
480 p_sigma0 = 0.0;
481
482 for ( int i = 0; i < p_sparseRows; i++ ) {
484 p_residuals[i] -= p_expected[i];
486 }
487
488 // if Jigsaw (bundle adjustment)
489 // add contibution to Sigma0 from constrained parameters
490 if ( p_jigsaw ) {
491 double constrained_vTPv = 0.0;
492
493 for ( int i = 0; i < p_sparseCols; i++ ) {
494 double weight = p_parameterWeights[i];
495
496 if ( weight <= 0.0 )
497 continue;
498
499 constrained_vTPv += p_epsilonsSparse[i]*p_epsilonsSparse[i]*weight;
500 }
501 p_sigma0 += constrained_vTPv;
502 }
503 // calculate degrees of freedom (or redundancy)
504 // DOF = # observations + # constrained parameters - # unknown parameters
505 p_degreesOfFreedom = p_sparseRows + p_constrainedParameters - p_sparseCols;
506
507 if( p_degreesOfFreedom <= 0.0 ) {
508 p_sigma0 = 1.0;
509 }
510 else {
512 }
513
514 // check for p_sigma0 < 0
515 p_sigma0 = sqrt(p_sigma0);
516
517 // All done
518 p_solved = true;
519 return 0;
520 }
521
522
523 void LeastSquares::Reset ()
524 {
525 if ( p_sparse ) {
526 p_sparseA.zeros();
527 p_ATb.zeros();
528 p_normals.zeros();
529 p_currentFillRow = -1;
530 }
531 else {
532 p_input.clear();
533 }
534 p_sigma0 = 0.;
535 p_residuals.clear();
536 p_expected.clear();
537 p_sqrtWeight.clear();
538 p_solved = false;
539 }
540
541
542
553 double LeastSquares::Evaluate(const std::vector<double> &data) {
554 if(!p_solved) {
555 QString msg = "Unable to evaluate until a solution has been computed";
556 throw IException(IException::Programmer, msg, _FILEINFO_);
557 }
558 return p_basis->Evaluate(data);
559 }
560
570 std::vector<double> LeastSquares::Residuals() const {
571 if(!p_solved) {
572 QString msg = "Unable to return residuals until a solution has been computed";
573 throw IException(IException::Programmer, msg, _FILEINFO_);
574 }
575 return p_residuals;
576 }
577
590 double LeastSquares::Residual(int i) const {
591 if(!p_solved) {
592 QString msg = "Unable to return residuals until a solution has been computed";
593 throw IException(IException::Programmer, msg, _FILEINFO_);
594 }
595 return p_residuals[i];
596 }
597
614 void LeastSquares::Weight(int index, double weight) {
615 if(weight == 1) {
616 p_sqrtWeight[index] = weight;
617 }
618 else {
619 p_sqrtWeight[index] = sqrt(weight);
620 }
621 }
622
623} // end namespace isis
Generic linear equation class.
QString Name() const
Returns the name of the equation.
virtual void Expand(const std::vector< double > &vars)
This is the function you should replace depending on your needs.
double Evaluate(const std::vector< double > &vars)
Compute the equation using the input variables.
int Coefficients() const
Returns the number of coefficients for the equation.
void SetCoefficients(const std::vector< double > &coefs)
Set the coefficients for the equation.
int Variables() const
Returns the number of variables in the equation.
double Term(int c) const
Returns the cth term.
Isis exception class.
Definition IException.h:91
@ Unknown
A type of error that cannot be classified as any of the other error types.
Definition IException.h:118
@ Programmer
This error is for when a programmer made an API call that was illegal.
Definition IException.h:146
arma::mat p_xSparse
sparse solution matrix
arma::SpMat< double > p_normals
normal equations matrix 'N'
double GetExpected(int row) const
This method returns the expected value at the given row.
void Weight(int index, double weight)
Reset the weight for the ith known.
double p_sigma0
sigma nought - reference variance
int SolveSparse()
Solve using sparse class.
arma::SpMat< double > p_sparseA
design matrix 'A'
std::vector< double > p_parameterWeights
vector of parameter weights
double Residual(int i) const
Returns the ith residual.
bool p_solved
Boolean value indicating solution is complete.
int Rows() const
This methods returns the number of rows in the matrix.
Isis::BasisFunction * p_basis
Pointer to the BasisFunction object.
~LeastSquares()
Destroys the LeastSquares object.
@ QRD
QR Decomposition.
@ SVD
Singular Value Decomposition.
double Evaluate(const std::vector< double > &input)
Invokes the BasisFunction Evaluate method.
std::vector< std::vector< double > > p_input
A vector of the input variables to evaluate.
int p_constrainedParameters
constrained parameters
arma::mat p_ATb
right-hand side vector
int Solve(Isis::LeastSquares::SolveMethod method=SVD)
After all the data has been registered through AddKnown, invoke this method to solve the system of eq...
void SolveQRD()
After all the data has been registered through AddKnown, invoke this method to solve the system of eq...
std::vector< double > p_epsilonsSparse
sparse vector of total parameter corrections
void SolveSVD()
After all the data has been registered through AddKnown, invoke this method to solve the system of eq...
LeastSquares(Isis::BasisFunction &basis, bool sparse=false, int sparseRows=0, int sparseCols=0, bool jigsaw=false)
Creates a LeastSquares Object.
std::vector< double > Residuals() const
Returns a vector of residuals (errors).
void FillSparseA(const std::vector< double > &data)
Invoke this method for each set of knowns for sparse solutions.
std::vector< double > p_sqrtWeight
A vector of the square roots of the weights for each known value.
std::vector< double > p_residuals
A vector of the residuals (or difference between expected and solved values).
void AddKnown(const std::vector< double > &input, double expected, double weight=1.0)
Invoke this method for each set of knowns.
std::vector< double > p_expected
A vector of the expected values when solved.
std::vector< double > GetInput(int row) const
This method returns the data at the given row.
int p_degreesOfFreedom
degrees of freedom (redundancy)
This is free and unencumbered software released into the public domain.
Definition Apollo.h:16
QString toString(bool boolToConvert)
Global function to convert a boolean to a string.
Definition IString.cpp:211