1 // This is core/vnl/algo/vnl_ldl_cholesky.cxx
2 //:
3 // \file
4 // \brief Updateable Cholesky decomposition: A=LDL'
5 // \author Tim Cootes
6 // \date   29 Mar 2006
7 //
8 //-----------------------------------------------------------------------------
9 
10 #include <cmath>
11 #include <cassert>
12 #include <iostream>
13 #include "vnl_ldl_cholesky.h"
14 #include <vnl/algo/vnl_netlib.h> // dpofa_(), dposl_(), dpoco_(), dpodi_()
15 
16 //: Cholesky decomposition.
17 // Make cholesky decomposition of M optionally computing
18 // the reciprocal condition number.  If mode is estimate_condition, the
19 // condition number and an approximate nullspace are estimated, at a cost
20 // of a factor of (1 + 18/n).  Here's a table of 1 + 18/n:
21 // \verbatim
22 // n:              3      5     10     50    100    500   1000
23 // slowdown:     7.0    4.6    2.8    1.4   1.18   1.04   1.02
24 // \endverbatim
25 
vnl_ldl_cholesky(vnl_matrix<double> const & M,Operation mode)26 vnl_ldl_cholesky::vnl_ldl_cholesky(vnl_matrix<double> const & M, Operation mode)
27   : L_(M)
28 {
29   long n = M.columns();
30   assert(n == (int)(M.rows()));
31   num_dims_rank_def_ = -1;
32   if (std::fabs(M(0, n - 1) - M(n - 1, 0)) > 1e-8)
33   {
34     std::cerr << "vnl_ldl_cholesky: WARNING: non-symmetric: " << M << std::endl;
35   }
36 
37   if (mode != estimate_condition)
38   {
39     // Quick factorization
40     v3p_netlib_dpofa_(L_.data_block(), &n, &n, &num_dims_rank_def_);
41     if (mode == verbose && num_dims_rank_def_ != 0)
42       std::cerr << "vnl_ldl_cholesky: " << num_dims_rank_def_ << " dimensions of non-posdeffness\n";
43   }
44   else
45   {
46     vnl_vector<double> nullvec(n);
47     v3p_netlib_dpoco_(L_.data_block(), &n, &n, &rcond_, nullvec.data_block(), &num_dims_rank_def_);
48     if (num_dims_rank_def_ != 0)
49       std::cerr << "vnl_ldl_cholesky: rcond=" << rcond_ << " so " << num_dims_rank_def_
50                 << " dimensions of non-posdeffness\n";
51   }
52 
53   // L_ is currently part of plain decomposition, M=L_ * L_.transpose()
54   // Extract diagonal and tweak L_
55   d_.set_size(n);
56 
57   //: Sqrt of elements of diagonal matrix
58   vnl_vector<double> sqrt_d(n);
59 
60   for (int i = 0; i < n; ++i)
61   {
62     sqrt_d[i] = L_(i, i);
63     d_[i] = sqrt_d[i] * sqrt_d[i];
64   }
65 
66   // Scale column j by 1/sqrt_d_[i] and set upper triangular elements to zero
67   for (int i = 0; i < n; ++i)
68   {
69     double * row = L_[i];
70     for (int j = 0; j < i; ++j)
71       row[j] /= sqrt_d[j];
72     row[i] = 1.0;
73     for (int j = i + 1; j < n; ++j)
74       row[j] = 0.0; // Zero upper triangle
75   }
76 }
77 
78 //: Sum of v1[i]*v2[i]  (i=0..n-1)
79 inline double
dot(const double * v1,const double * v2,unsigned n)80 dot(const double * v1, const double * v2, unsigned n)
81 {
82   double sum = 0.0;
83   for (unsigned i = 0; i < n; ++i)
84     sum += v1[i] * v2[i];
85   return sum;
86 }
87 //: Sum of v1[i*s]*v2[i]  (i=0..n-1)
88 inline double
dot(const double * v1,unsigned s,const double * v2,unsigned n)89 dot(const double * v1, unsigned s, const double * v2, unsigned n)
90 {
91   double sum = 0.0;
92   for (unsigned i = 0; i < n; ++i, v1 += s)
93     sum += (*v1) * v2[i];
94   return sum;
95 }
96 
97 //: Solve Lx=y (in-place)
98 //  x is overwritten with solution
99 void
solve_lx(vnl_vector<double> & x)100 vnl_ldl_cholesky::solve_lx(vnl_vector<double> & x)
101 {
102   unsigned n = d_.size();
103   for (unsigned i = 1; i < n; ++i)
104     x[i] -= dot(L_[i], x.data_block(), i);
105 }
106 
107 //: Solve Mx=b, overwriting input vector with the solution.
108 //  x points to beginning of an n-element vector containing b
109 //  On exit, x[i] filled with solution vector.
110 void
inplace_solve(double * x) const111 vnl_ldl_cholesky::inplace_solve(double * x) const
112 {
113   unsigned n = d_.size();
114   // Solve Ly=b for y
115   for (unsigned i = 1; i < n; ++i)
116     x[i] -= dot(L_[i], x, i);
117 
118   // Scale by inverse of D
119   for (unsigned i = 0; i < n; ++i)
120     x[i] /= d_[i];
121 
122   // Solve L'x=y for x
123   const double * L_data = &L_(n - 1, n - 2);
124   const double * x_data = &x[n - 1];
125   unsigned c = 1;
126   for (int i = n - 2; i >= 0; --i, L_data -= (n + 1), --x_data, ++c)
127   {
128     x[i] -= dot(L_data, n, x_data, c);
129   }
130 }
131 
132 //: Efficient computation of x' * inv(M) * x
133 //  Useful when M is a covariance matrix!
134 //  Solves Ly=x for y, then returns sum y[i]*y[i]/d[i]
135 double
xt_m_inv_x(const vnl_vector<double> & x) const136 vnl_ldl_cholesky::xt_m_inv_x(const vnl_vector<double> & x) const
137 {
138   unsigned n = d_.size();
139   assert(x.size() == n);
140   vnl_vector<double> y = x;
141   // Solve Ly=x for y and compute sum as we go
142   double sum = y[0] * y[0] / d_[0];
143   for (unsigned i = 1; i < n; ++i)
144   {
145     y[i] -= dot(L_[i], y.data_block(), i);
146     sum += y[i] * y[i] / d_[i];
147   }
148   return sum;
149 }
150 
151 //: Efficient computation of x' * M * x
152 //  Twice as fast as explicitly computing x' * M * x
153 double
xt_m_x(const vnl_vector<double> & x) const154 vnl_ldl_cholesky::xt_m_x(const vnl_vector<double> & x) const
155 {
156   unsigned n = d_.size();
157   assert(x.size() == n);
158   double sum = 0.0;
159   const double * xd = x.data_block();
160   const double * L_col = L_.data_block();
161   unsigned c = n;
162   for (unsigned i = 0; i < n; ++i, ++xd, L_col += (n + 1), --c)
163   {
164     double xLi = dot(L_col, n, xd, c); // x * i-th column
165     sum += xLi * xLi * d_[i];
166   }
167   return sum;
168 }
169 
170 
171 //: Solve least squares problem M x = b.
172 //  The right-hand-side std::vector x may be b,
173 //  which will give a fractional increase in speed.
174 void
solve(vnl_vector<double> const & b,vnl_vector<double> * xp) const175 vnl_ldl_cholesky::solve(vnl_vector<double> const & b, vnl_vector<double> * xp) const
176 {
177   assert(b.size() == d_.size());
178   *xp = b;
179   inplace_solve(xp->data_block());
180 }
181 
182 //: Solve least squares problem M x = b.
183 vnl_vector<double>
solve(vnl_vector<double> const & b) const184 vnl_ldl_cholesky::solve(vnl_vector<double> const & b) const
185 {
186   assert(b.size() == L_.columns());
187 
188   vnl_vector<double> ret = b;
189   solve(b, &ret);
190   return ret;
191 }
192 
193 //: Compute determinant.
194 double
determinant() const195 vnl_ldl_cholesky::determinant() const
196 {
197   unsigned n = d_.size();
198   double det = 1.0;
199   for (unsigned i = 0; i < n; ++i)
200     det *= d_[i];
201   return det;
202 }
203 
204 //: Compute rank-1 update, ie the decomposition of (M+v.v')
205 //  If the initial state is the decomposition of M, then
206 //  L and D are updated so that on exit  LDL'=M+v.v'
207 //
208 //  Uses the algorithm given by Davis and Hager in
209 //  "Multiple-Rank Modifications of a Sparse Cholesky Factorization",2001.
210 void
rank1_update(const vnl_vector<double> & v)211 vnl_ldl_cholesky::rank1_update(const vnl_vector<double> & v)
212 {
213   unsigned n = d_.size();
214   assert(v.size() == n);
215   double a = 1.0;
216   vnl_vector<double> w = v; // Workspace, modified as algorithm goes along
217   for (unsigned j = 0; j < n; ++j)
218   {
219     double a2 = a + w[j] * w[j] / d_[j];
220     d_[j] *= a2;
221     double gamma = w[j] / d_[j];
222     d_[j] /= a;
223     a = a2;
224 
225     for (unsigned p = j + 1; p < n; ++p)
226     {
227       w[p] -= w[j] * L_(p, j);
228       L_(p, j) += gamma * w[p];
229     }
230   }
231 }
232 
233 //: Multi-rank update, ie the decomposition of (M+W.W')
234 //  If the initial state is the decomposition of M, then
235 //  L and D are updated so that on exit  LDL'=M+W.W'
236 void
update(const vnl_matrix<double> & W0)237 vnl_ldl_cholesky::update(const vnl_matrix<double> & W0)
238 {
239   unsigned n = d_.size();
240   assert(W0.rows() == n);
241   unsigned r = W0.columns();
242 
243   vnl_matrix<double> W(W0);               // Workspace
244   vnl_vector<double> a(r, 1.0), gamma(r); // Workspace
245   for (unsigned j = 0; j < n; ++j)
246   {
247     double * Wj = W[j];
248     for (unsigned i = 0; i < r; ++i)
249     {
250       double a2 = a[i] + Wj[i] * Wj[i] / d_[j];
251       d_[j] *= a2;
252       gamma[i] = Wj[i] / d_[j];
253       d_[j] /= a[i];
254       a[i] = a2;
255     }
256     for (unsigned p = j + 1; p < n; ++p)
257     {
258       double * Wp = W[p];
259       double * Lp = L_[p];
260       for (unsigned i = 0; i < r; ++i)
261       {
262         Wp[i] -= Wj[i] * Lp[j];
263         Lp[j] += gamma[i] * Wp[i];
264       }
265     }
266   }
267 }
268 
269 // : Compute inverse.  Not efficient.
270 vnl_matrix<double>
inverse() const271 vnl_ldl_cholesky::inverse() const
272 {
273   if (num_dims_rank_def_)
274   {
275     std::cerr << "vnl_ldl_cholesky: Calling inverse() on rank-deficient matrix\n";
276     return vnl_matrix<double>();
277   }
278 
279   unsigned int n = d_.size();
280   vnl_matrix<double> R(n, n);
281   R.set_identity();
282 
283   // Set each row to solution of Mx=(unit)
284   // Since result should be symmetric, this is OK
285   for (unsigned int i = 0; i < n; ++i)
286     inplace_solve(R[i]);
287 
288   return R;
289 }
290