LSSTApplications  18.0.0+106,18.0.0+50,19.0.0,19.0.0+1,19.0.0+10,19.0.0+11,19.0.0+13,19.0.0+17,19.0.0+2,19.0.0-1-g20d9b18+6,19.0.0-1-g425ff20,19.0.0-1-g5549ca4,19.0.0-1-g580fafe+6,19.0.0-1-g6fe20d0+1,19.0.0-1-g7011481+9,19.0.0-1-g8c57eb9+6,19.0.0-1-gb5175dc+11,19.0.0-1-gdc0e4a7+9,19.0.0-1-ge272bc4+6,19.0.0-1-ge3aa853,19.0.0-10-g448f008b,19.0.0-12-g6990b2c,19.0.0-2-g0d9f9cd+11,19.0.0-2-g3d9e4fb2+11,19.0.0-2-g5037de4,19.0.0-2-gb96a1c4+3,19.0.0-2-gd955cfd+15,19.0.0-3-g2d13df8,19.0.0-3-g6f3c7dc,19.0.0-4-g725f80e+11,19.0.0-4-ga671dab3b+1,19.0.0-4-gad373c5+3,19.0.0-5-ga2acb9c+2,19.0.0-5-gfe96e6c+2,w.2020.01
LSSTDataManagementBasePackage
LeastSquares.cc
Go to the documentation of this file.
1 // -*- LSST-C++ -*-
2 
3 /*
4  * LSST Data Management System
5  * Copyright 2008, 2009, 2010, 2011 LSST Corporation.
6  *
7  * This product includes software developed by the
8  * LSST Project (http://www.lsst.org/).
9  *
10  * This program is free software: you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by
12  * the Free Software Foundation, either version 3 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the LSST License Statement and
21  * the GNU General Public License along with this program. If not,
22  * see <http://www.lsstcorp.org/LegalNotices/>.
23  */
24 
25 #include "Eigen/Eigenvalues"
26 #include "Eigen/SVD"
27 #include "Eigen/Cholesky"
28 #include "boost/format.hpp"
29 #include <memory>
30 
32 #include "lsst/pex/exceptions.h"
33 #include "lsst/log/Log.h"
34 
35 namespace {
36 LOG_LOGGER _log = LOG_GET("afw.math.LeastSquares");
37 }
38 
39 namespace lsst {
40 namespace afw {
41 namespace math {
42 
44 public:
45  enum StateFlags {
46  LOWER_FISHER_MATRIX = 0x001,
47  FULL_FISHER_MATRIX = 0x002,
48  RHS_VECTOR = 0x004,
49  SOLUTION_ARRAY = 0x008,
50  COVARIANCE_ARRAY = 0x010,
51  DIAGNOSTIC_ARRAY = 0x020,
52  DESIGN_AND_DATA = 0x040
53  };
54 
55  int state;
56  int dimension;
57  int rank;
60  double threshold;
61 
62  Eigen::MatrixXd design;
63  Eigen::VectorXd data;
64  Eigen::MatrixXd fisher;
65  Eigen::VectorXd rhs;
66 
67  ndarray::Array<double, 1, 1> solution;
68  ndarray::Array<double, 2, 2> covariance;
69  ndarray::Array<double, 1, 1> diagnostic;
70 
71  template <typename D>
72  void setRank(Eigen::MatrixBase<D> const& values) {
73  double cond = threshold * values[0];
74  if (cond <= 0.0) {
75  rank = 0;
76  } else {
77  for (rank = dimension; (rank > 1) && (values[rank - 1] < cond); --rank)
78  ;
79  }
80  }
81 
82  void ensure(int desired) {
83  if (state & FULL_FISHER_MATRIX) state |= LOWER_FISHER_MATRIX;
84  if (desired & FULL_FISHER_MATRIX) desired |= LOWER_FISHER_MATRIX;
85  int toAdd = ~state & desired;
86  if (toAdd & LOWER_FISHER_MATRIX) {
87  assert(state & DESIGN_AND_DATA);
88  fisher = Eigen::MatrixXd::Zero(design.cols(), design.cols());
89  fisher.selfadjointView<Eigen::Lower>().rankUpdate(design.adjoint());
90  }
91  if (toAdd & FULL_FISHER_MATRIX) {
92  fisher.triangularView<Eigen::StrictlyUpper>() = fisher.adjoint();
93  }
94  if (toAdd & RHS_VECTOR) {
95  assert(state & DESIGN_AND_DATA);
96  rhs = design.adjoint() * data;
97  }
98  if (toAdd & SOLUTION_ARRAY) {
99  if (solution.isEmpty()) solution = ndarray::allocate(dimension);
100  updateSolution();
101  }
102  if (toAdd & COVARIANCE_ARRAY) {
103  if (covariance.isEmpty()) covariance = ndarray::allocate(dimension, dimension);
104  updateCovariance();
105  }
106  if (toAdd & DIAGNOSTIC_ARRAY) {
107  if (diagnostic.isEmpty()) diagnostic = ndarray::allocate(dimension);
108  updateDiagnostic();
109  }
110  state |= toAdd;
111  }
112 
113  virtual void factor() = 0;
114 
115  virtual void updateRank() = 0;
116 
117  virtual void updateSolution() = 0;
118  virtual void updateCovariance() = 0;
119 
120  virtual void updateDiagnostic() = 0;
121 
122  Impl(int dimension_, double threshold_ = std::numeric_limits<double>::epsilon())
123  : state(0), dimension(dimension_), rank(dimension_), threshold(threshold_) {}
124 
125  virtual ~Impl() {}
126 };
127 
128 namespace {
129 
130 class EigensystemSolver : public LeastSquares::Impl {
131 public:
132  explicit EigensystemSolver(int dimension) : Impl(dimension), _eig(dimension), _svd(), _tmp(dimension) {}
133 
134  void factor() override {
135  ensure(LOWER_FISHER_MATRIX | RHS_VECTOR);
136  _eig.compute(fisher);
137  if (_eig.info() == Eigen::Success) {
138  setRank(_eig.eigenvalues().reverse());
139  LOGL_DEBUG(_log, "SelfAdjointEigenSolver succeeded: dimension=%d, rank=%d", dimension, rank);
140  } else {
141  // Note that the fallback is using SVD of the Fisher to compute the Eigensystem, because those
142  // are the same for a symmetric matrix; this is very different from doing a direct SVD of
143  // the design matrix.
144  ensure(FULL_FISHER_MATRIX);
145  _svd.compute(fisher, Eigen::ComputeFullU); // Matrix is symmetric, so V == U == eigenvectors
146  setRank(_svd.singularValues());
147  LOGL_DEBUG(_log,
148  "SelfAdjointEigenSolver failed; falling back to equivalent SVD: dimension=%d, rank=%d",
149  dimension, rank);
150  }
151  }
152 
153  void updateRank() override {
154  if (_eig.info() == Eigen::Success) {
155  setRank(_eig.eigenvalues().reverse());
156  } else {
157  setRank(_svd.singularValues());
158  }
159  }
160 
161  void updateDiagnostic() override {
162  if (whichDiagnostic == LeastSquares::NORMAL_CHOLESKY) {
163  throw LSST_EXCEPT(
165  "Cannot compute NORMAL_CHOLESKY diagnostic from NORMAL_EIGENSYSTEM factorization.");
166  }
167  if (_eig.info() == Eigen::Success) {
168  ndarray::asEigenMatrix(diagnostic) = _eig.eigenvalues().reverse();
169  } else {
170  ndarray::asEigenMatrix(diagnostic) = _svd.singularValues();
171  }
172  if (whichDiagnostic == LeastSquares::DIRECT_SVD) {
173  ndarray::asEigenArray(diagnostic) = ndarray::asEigenArray(diagnostic).sqrt();
174  }
175  }
176 
177  void updateSolution() override {
178  if (rank == 0) {
179  ndarray::asEigenMatrix(solution).setZero();
180  return;
181  }
182  if (_eig.info() == Eigen::Success) {
183  _tmp.head(rank) = _eig.eigenvectors().rightCols(rank).adjoint() * rhs;
184  _tmp.head(rank).array() /= _eig.eigenvalues().tail(rank).array();
185  ndarray::asEigenMatrix(solution) = _eig.eigenvectors().rightCols(rank) * _tmp.head(rank);
186  } else {
187  _tmp.head(rank) = _svd.matrixU().leftCols(rank).adjoint() * rhs;
188  _tmp.head(rank).array() /= _svd.singularValues().head(rank).array();
189  ndarray::asEigenMatrix(solution) = _svd.matrixU().leftCols(rank) * _tmp.head(rank);
190  }
191  }
192 
193  void updateCovariance() override {
194  if (rank == 0) {
195  ndarray::asEigenMatrix(covariance).setZero();
196  return;
197  }
198  if (_eig.info() == Eigen::Success) {
199  ndarray::asEigenMatrix(covariance) =
200  _eig.eigenvectors().rightCols(rank) *
201  _eig.eigenvalues().tail(rank).array().inverse().matrix().asDiagonal() *
202  _eig.eigenvectors().rightCols(rank).adjoint();
203  } else {
204  ndarray::asEigenMatrix(covariance) =
205  _svd.matrixU().leftCols(rank) *
206  _svd.singularValues().head(rank).array().inverse().matrix().asDiagonal() *
207  _svd.matrixU().leftCols(rank).adjoint();
208  }
209  }
210 
211 private:
212  Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> _eig;
213  Eigen::JacobiSVD<Eigen::MatrixXd> _svd; // only used if Eigendecomposition fails, should be very rare
214  Eigen::VectorXd _tmp;
215 };
216 
217 class CholeskySolver : public LeastSquares::Impl {
218 public:
219  explicit CholeskySolver(int dimension) : Impl(dimension, 0.0), _ldlt(dimension) {}
220 
221  void factor() override {
222  ensure(LOWER_FISHER_MATRIX | RHS_VECTOR);
223  _ldlt.compute(fisher);
224  }
225 
226  void updateRank() override {}
227 
228  void updateDiagnostic() override {
229  if (whichDiagnostic != LeastSquares::NORMAL_CHOLESKY) {
230  throw LSST_EXCEPT(
232  "Can only compute NORMAL_CHOLESKY diagnostic from NORMAL_CHOLESKY factorization.");
233  }
234  ndarray::asEigenMatrix(diagnostic) = _ldlt.vectorD();
235  }
236 
237  void updateSolution() override { ndarray::asEigenMatrix(solution) = _ldlt.solve(rhs); }
238 
239  void updateCovariance() override {
240  auto cov = ndarray::asEigenMatrix(covariance);
241  cov.setIdentity();
242  cov = _ldlt.solve(cov);
243  }
244 
245 private:
246  Eigen::LDLT<Eigen::MatrixXd> _ldlt;
247 };
248 
249 class SvdSolver : public LeastSquares::Impl {
250 public:
251  explicit SvdSolver(int dimension) : Impl(dimension), _svd(), _tmp(dimension) {}
252 
253  void factor() override {
254  if (!(state & DESIGN_AND_DATA)) {
256  "Cannot initialize DIRECT_SVD solver with normal equations.");
257  }
258  _svd.compute(design, Eigen::ComputeThinU | Eigen::ComputeThinV);
259  setRank(_svd.singularValues());
260  LOGL_DEBUG(_log, "Using direct SVD method; dimension=%d, rank=%d", dimension, rank);
261  }
262 
263  void updateRank() override { setRank(_svd.singularValues()); }
264 
265  void updateDiagnostic() override {
266  switch (whichDiagnostic) {
267  case LeastSquares::NORMAL_EIGENSYSTEM:
268  ndarray::asEigenArray(diagnostic) = _svd.singularValues().array().square();
269  break;
270  case LeastSquares::NORMAL_CHOLESKY:
271  throw LSST_EXCEPT(
273  "Can only compute NORMAL_CHOLESKY diagnostic from DIRECT_SVD factorization.");
274  case LeastSquares::DIRECT_SVD:
275  ndarray::asEigenMatrix(diagnostic) = _svd.singularValues();
276  break;
277  }
278  }
279 
280  void updateSolution() override {
281  if (rank == 0) {
282  ndarray::asEigenMatrix(solution).setZero();
283  return;
284  }
285  _tmp.head(rank) = _svd.matrixU().leftCols(rank).adjoint() * data;
286  _tmp.head(rank).array() /= _svd.singularValues().head(rank).array();
287  ndarray::asEigenMatrix(solution) = _svd.matrixV().leftCols(rank) * _tmp.head(rank);
288  }
289 
290  void updateCovariance() override {
291  if (rank == 0) {
292  ndarray::asEigenMatrix(covariance).setZero();
293  return;
294  }
295  ndarray::asEigenMatrix(covariance) =
296  _svd.matrixV().leftCols(rank) *
297  _svd.singularValues().head(rank).array().inverse().square().matrix().asDiagonal() *
298  _svd.matrixV().leftCols(rank).adjoint();
299  }
300 
301 private:
302  Eigen::JacobiSVD<Eigen::MatrixXd> _svd;
303  Eigen::VectorXd _tmp;
304 };
305 
306 } // namespace
307 
308 void LeastSquares::setThreshold(double threshold) {
309  _impl->threshold = threshold;
310  _impl->state &= ~Impl::SOLUTION_ARRAY;
311  _impl->state &= ~Impl::COVARIANCE_ARRAY;
312  _impl->updateRank();
313 }
314 
315 double LeastSquares::getThreshold() const { return _impl->threshold; }
316 
317 ndarray::Array<double const, 1, 1> LeastSquares::getSolution() {
318  _impl->ensure(Impl::SOLUTION_ARRAY);
319  return _impl->solution;
320 }
321 
322 ndarray::Array<double const, 2, 2> LeastSquares::getCovariance() {
323  _impl->ensure(Impl::COVARIANCE_ARRAY);
324  return _impl->covariance;
325 }
326 
327 ndarray::Array<double const, 2, 2> LeastSquares::getFisherMatrix() {
328  _impl->ensure(Impl::FULL_FISHER_MATRIX);
329  // Wrap the Eigen::MatrixXd in an ndarray::Array, using _impl as the reference-counted owner.
330  // Doesn't matter if we swap strides, because it's symmetric.
331  return ndarray::external(_impl->fisher.data(), ndarray::makeVector(_impl->dimension, _impl->dimension),
332  ndarray::makeVector(_impl->dimension, 1), _impl);
333 }
334 
335 ndarray::Array<double const, 1, 1> LeastSquares::getDiagnostic(Factorization factorization) {
336  if (_impl->whichDiagnostic != factorization) {
337  _impl->state &= ~Impl::DIAGNOSTIC_ARRAY;
338  _impl->whichDiagnostic = factorization;
339  }
340  _impl->ensure(Impl::DIAGNOSTIC_ARRAY);
341  return _impl->diagnostic;
342 }
343 
344 int LeastSquares::getDimension() const { return _impl->dimension; }
345 
346 int LeastSquares::getRank() const { return _impl->rank; }
347 
348 LeastSquares::Factorization LeastSquares::getFactorization() const { return _impl->factorization; }
349 
350 LeastSquares::LeastSquares(Factorization factorization, int dimension) {
351  switch (factorization) {
352  case NORMAL_EIGENSYSTEM:
353  _impl = std::make_shared<EigensystemSolver>(dimension);
354  break;
355  case NORMAL_CHOLESKY:
356  _impl = std::make_shared<CholeskySolver>(dimension);
357  break;
358  case DIRECT_SVD:
359  _impl = std::make_shared<SvdSolver>(dimension);
360  break;
361  }
362  _impl->factorization = factorization;
363 }
364 
365 LeastSquares::LeastSquares(LeastSquares const&) = default;
366 LeastSquares::LeastSquares(LeastSquares&&) = default;
367 LeastSquares& LeastSquares::operator=(LeastSquares const&) = default;
368 LeastSquares& LeastSquares::operator=(LeastSquares&&) = default;
369 
370 LeastSquares::~LeastSquares() = default;
371 
372 Eigen::MatrixXd& LeastSquares::_getDesignMatrix() { return _impl->design; }
373 Eigen::VectorXd& LeastSquares::_getDataVector() { return _impl->data; }
374 
375 Eigen::MatrixXd& LeastSquares::_getFisherMatrix() { return _impl->fisher; }
376 Eigen::VectorXd& LeastSquares::_getRhsVector() { return _impl->rhs; }
377 
378 void LeastSquares::_factor(bool haveNormalEquations) {
379  if (haveNormalEquations) {
380  if (_getFisherMatrix().rows() != _impl->dimension) {
382  (boost::format("Number of rows of Fisher matrix (%d) does not match"
383  " dimension of LeastSquares solver.") %
384  _getFisherMatrix().rows() % _impl->dimension)
385  .str());
386  }
387  if (_getFisherMatrix().cols() != _impl->dimension) {
389  (boost::format("Number of columns of Fisher matrix (%d) does not match"
390  " dimension of LeastSquares solver.") %
391  _getFisherMatrix().cols() % _impl->dimension)
392  .str());
393  }
394  if (_getRhsVector().size() != _impl->dimension) {
396  (boost::format("Number of elements in RHS vector (%d) does not match"
397  " dimension of LeastSquares solver.") %
398  _getRhsVector().size() % _impl->dimension)
399  .str());
400  }
401  _impl->state = Impl::RHS_VECTOR | Impl::FULL_FISHER_MATRIX | Impl::LOWER_FISHER_MATRIX;
402  } else {
403  if (_getDesignMatrix().cols() != _impl->dimension) {
404  throw LSST_EXCEPT(
406  "Number of columns of design matrix does not match dimension of LeastSquares solver.");
407  }
408  if (_getDesignMatrix().rows() != _getDataVector().size()) {
410  (boost::format("Number of rows of design matrix (%d) does not match number of "
411  "data points (%d)") %
412  _getDesignMatrix().rows() % _getDataVector().size())
413  .str());
414  }
415  if (_getDesignMatrix().cols() > _getDataVector().size()) {
416  throw LSST_EXCEPT(
418  (boost::format("Number of columns of design matrix (%d) must be smaller than number of "
419  "data points (%d)") %
420  _getDesignMatrix().cols() % _getDataVector().size())
421  .str());
422  }
423  _impl->state = Impl::DESIGN_AND_DATA;
424  }
425  _impl->factor();
426 }
427 } // namespace math
428 } // namespace afw
429 } // namespace lsst
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:174
#define LOG_LOGGER
Definition: Log.h:703
Impl(int dimension_, double threshold_=std::numeric_limits< double >::epsilon())
Solver for linear least-squares problems.
Definition: LeastSquares.h:67
#define LOGL_DEBUG(logger, message...)
Log a debug-level message using a varargs/printf style interface.
Definition: Log.h:504
LSST DM logging module built on log4cxx.
ndarray::Array< double, 1, 1 > diagnostic
Definition: LeastSquares.cc:69
A base class for image defects.
char * data
Definition: BaseRecord.cc:62
ndarray::Array< double, 1, 1 > solution
Definition: LeastSquares.cc:67
Reports errors in the logical structure of the program.
Definition: Runtime.h:46
ndarray::Array< double, 2, 2 > covariance
Definition: LeastSquares.cc:68
#define LSST_EXCEPT(type,...)
Create an exception with a given type.
Definition: Exception.h:48
Factorization
Private implementation; forward-declared publicly so we can inherit from it in .cc.
Definition: LeastSquares.h:71
Reports invalid arguments.
Definition: Runtime.h:66
void setRank(Eigen::MatrixBase< D > const &values)
Definition: LeastSquares.cc:72
#define LOG_GET(logger)
Returns a Log object associated with logger.
Definition: Log.h:75