gonum.org/v1/gonum@v0.14.0/lapack/testlapack/dgesvd.go (about) 1 // Copyright ©2015 The Gonum Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package testlapack 6 7 import ( 8 "fmt" 9 "math" 10 "sort" 11 "testing" 12 13 "golang.org/x/exp/rand" 14 15 "gonum.org/v1/gonum/blas" 16 "gonum.org/v1/gonum/blas/blas64" 17 "gonum.org/v1/gonum/floats" 18 "gonum.org/v1/gonum/lapack" 19 ) 20 21 type Dgesvder interface { 22 Dgesvd(jobU, jobVT lapack.SVDJob, m, n int, a []float64, lda int, s, u []float64, ldu int, vt []float64, ldvt int, work []float64, lwork int) (ok bool) 23 } 24 25 func DgesvdTest(t *testing.T, impl Dgesvder, tol float64) { 26 for _, m := range []int{0, 1, 2, 3, 4, 5, 10, 150, 300} { 27 for _, n := range []int{0, 1, 2, 3, 4, 5, 10, 150} { 28 for _, mtype := range []int{1, 2, 3, 4, 5} { 29 dgesvdTest(t, impl, m, n, mtype, tol) 30 } 31 } 32 } 33 } 34 35 // dgesvdTest tests a Dgesvd implementation on an m×n matrix A generated 36 // according to mtype as: 37 // - the zero matrix if mtype == 1, 38 // - the identity matrix if mtype == 2, 39 // - a random matrix with a given condition number and singular values if mtype == 3, 4, or 5. 40 // 41 // It first computes the full SVD A = U*Sigma*Vᵀ and checks that 42 // - U has orthonormal columns, and Vᵀ has orthonormal rows, 43 // - U*Sigma*Vᵀ multiply back to A, 44 // - the singular values are non-negative and sorted in decreasing order. 45 // 46 // Then all combinations of partial SVD results are computed and checked whether 47 // they match the full SVD result. 48 func dgesvdTest(t *testing.T, impl Dgesvder, m, n, mtype int, tol float64) { 49 const tolOrtho = 1e-15 50 51 rnd := rand.New(rand.NewSource(1)) 52 53 // Use a fixed leading dimension to reduce testing time. 54 lda := n + 3 55 ldu := m + 5 56 ldvt := n + 7 57 58 minmn := min(m, n) 59 60 // Allocate A and fill it with random values. The in-range elements will 61 // be overwritten below according to mtype. 62 a := make([]float64, m*lda) 63 for i := range a { 64 a[i] = rnd.NormFloat64() 65 } 66 67 var aNorm float64 68 switch mtype { 69 default: 70 panic("unknown test matrix type") 71 case 1: 72 // Zero matrix. 73 for i := 0; i < m; i++ { 74 for j := 0; j < n; j++ { 75 a[i*lda+j] = 0 76 } 77 } 78 aNorm = 0 79 case 2: 80 // Identity matrix. 81 for i := 0; i < m; i++ { 82 for j := 0; j < n; j++ { 83 if i == j { 84 a[i*lda+i] = 1 85 } else { 86 a[i*lda+j] = 0 87 } 88 } 89 } 90 aNorm = 1 91 case 3, 4, 5: 92 // Scaled random matrix. 93 // Generate singular values. 94 s := make([]float64, minmn) 95 Dlatm1(s, 96 4, // s[i] = 1 - i*(1-1/cond)/(minmn-1) 97 float64(max(1, minmn)), // where cond = max(1,minmn) 98 false, // signs of s[i] are not randomly flipped 99 1, rnd) // random numbers are drawn uniformly from [0,1) 100 // Decide scale factor for the singular values based on the matrix type. 101 aNorm = 1 102 if mtype == 4 { 103 aNorm = smlnum 104 } 105 if mtype == 5 { 106 aNorm = bignum 107 } 108 // Scale singular values so that the maximum singular value is 109 // equal to aNorm (we know that the singular values are 110 // generated above to be spread linearly between 1/cond and 1). 111 floats.Scale(aNorm, s) 112 // Generate A by multiplying S by random orthogonal matrices 113 // from left and right. 114 Dlagge(m, n, max(0, m-1), max(0, n-1), s, a, lda, rnd, make([]float64, m+n)) 115 } 116 aCopy := make([]float64, len(a)) 117 copy(aCopy, a) 118 119 for _, wl := range []worklen{minimumWork, mediumWork, optimumWork} { 120 // Restore A because Dgesvd overwrites it. 121 copy(a, aCopy) 122 123 // Allocate slices that will be used below to store the results of full 124 // SVD and fill them. 125 uAll := make([]float64, m*ldu) 126 for i := range uAll { 127 uAll[i] = rnd.NormFloat64() 128 } 129 vtAll := make([]float64, n*ldvt) 130 for i := range vtAll { 131 vtAll[i] = rnd.NormFloat64() 132 } 133 sAll := make([]float64, min(m, n)) 134 for i := range sAll { 135 sAll[i] = math.NaN() 136 } 137 138 prefix := fmt.Sprintf("m=%v,n=%v,work=%v,mtype=%v", m, n, wl, mtype) 139 140 // Determine workspace size based on wl. 141 minwork := max(1, max(5*min(m, n), 3*min(m, n)+max(m, n))) 142 var lwork int 143 switch wl { 144 case minimumWork: 145 lwork = minwork 146 case mediumWork: 147 work := make([]float64, 1) 148 impl.Dgesvd(lapack.SVDAll, lapack.SVDAll, m, n, a, lda, sAll, uAll, ldu, vtAll, ldvt, work, -1) 149 lwork = (int(work[0]) + minwork) / 2 150 case optimumWork: 151 work := make([]float64, 1) 152 impl.Dgesvd(lapack.SVDAll, lapack.SVDAll, m, n, a, lda, sAll, uAll, ldu, vtAll, ldvt, work, -1) 153 lwork = int(work[0]) 154 } 155 work := make([]float64, max(1, lwork)) 156 for i := range work { 157 work[i] = math.NaN() 158 } 159 160 // Compute the full SVD which will be used later for checking the partial results. 161 ok := impl.Dgesvd(lapack.SVDAll, lapack.SVDAll, m, n, a, lda, sAll, uAll, ldu, vtAll, ldvt, work, len(work)) 162 if !ok { 163 t.Fatalf("Case %v: unexpected failure in full SVD", prefix) 164 } 165 166 // Check that uAll, sAll, and vtAll multiply back to A by computing a residual 167 // |A - U*S*VT| / (n*aNorm) 168 if resid := svdFullResidual(m, n, aNorm, aCopy, lda, uAll, ldu, sAll, vtAll, ldvt); resid > tol { 169 t.Errorf("Case %v: original matrix not recovered for full SVD, |A - U*D*VT|=%v", prefix, resid) 170 } 171 if minmn > 0 { 172 // Check that uAll is orthogonal. 173 q := blas64.General{Rows: m, Cols: m, Data: uAll, Stride: ldu} 174 if resid := residualOrthogonal(q, false); resid > tolOrtho*float64(m) { 175 t.Errorf("Case %v: UAll is not orthogonal; resid=%v, want<=%v", prefix, resid, tolOrtho*float64(m)) 176 } 177 // Check that vtAll is orthogonal. 178 q = blas64.General{Rows: n, Cols: n, Data: vtAll, Stride: ldvt} 179 if resid := residualOrthogonal(q, false); resid > tolOrtho*float64(n) { 180 t.Errorf("Case %v: VTAll is not orthogonal; resid=%v, want<=%v", prefix, resid, tolOrtho*float64(n)) 181 } 182 } 183 // Check that singular values are decreasing. 184 if !sort.IsSorted(sort.Reverse(sort.Float64Slice(sAll))) { 185 t.Errorf("Case %v: singular values from full SVD are not decreasing", prefix) 186 } 187 // Check that singular values are non-negative. 188 if minmn > 0 && floats.Min(sAll) < 0 { 189 t.Errorf("Case %v: some singular values from full SVD are negative", prefix) 190 } 191 192 // Do partial SVD and compare the results to sAll, uAll, and vtAll. 193 for _, jobU := range []lapack.SVDJob{lapack.SVDAll, lapack.SVDStore, lapack.SVDOverwrite, lapack.SVDNone} { 194 for _, jobVT := range []lapack.SVDJob{lapack.SVDAll, lapack.SVDStore, lapack.SVDOverwrite, lapack.SVDNone} { 195 if jobU == lapack.SVDOverwrite || jobVT == lapack.SVDOverwrite { 196 // Not implemented. 197 continue 198 } 199 if jobU == lapack.SVDAll && jobVT == lapack.SVDAll { 200 // Already checked above. 201 continue 202 } 203 204 prefix := prefix + ",job=" + svdJobString(jobU) + "U-" + svdJobString(jobVT) + "VT" 205 206 // Restore A to its original values. 207 copy(a, aCopy) 208 209 // Allocate slices for the results of partial SVD and fill them. 210 u := make([]float64, m*ldu) 211 for i := range u { 212 u[i] = rnd.NormFloat64() 213 } 214 vt := make([]float64, n*ldvt) 215 for i := range vt { 216 vt[i] = rnd.NormFloat64() 217 } 218 s := make([]float64, min(m, n)) 219 for i := range s { 220 s[i] = math.NaN() 221 } 222 223 for i := range work { 224 work[i] = math.NaN() 225 } 226 227 ok := impl.Dgesvd(jobU, jobVT, m, n, a, lda, s, u, ldu, vt, ldvt, work, len(work)) 228 if !ok { 229 t.Fatalf("Case %v: unexpected failure in partial Dgesvd", prefix) 230 } 231 232 if minmn == 0 { 233 // No panic and the result is ok, there is 234 // nothing else to check. 235 continue 236 } 237 238 // Check that U has orthogonal columns and that it matches UAll. 239 switch jobU { 240 case lapack.SVDStore: 241 q := blas64.General{Rows: m, Cols: minmn, Data: u, Stride: ldu} 242 if resid := residualOrthogonal(q, false); resid > tolOrtho*float64(m) { 243 t.Errorf("Case %v: columns of U are not orthogonal; resid=%v, want<=%v", prefix, resid, tolOrtho*float64(m)) 244 } 245 if res := svdPartialUResidual(m, minmn, u, uAll, ldu); res > tol { 246 t.Errorf("Case %v: columns of U do not match UAll", prefix) 247 } 248 case lapack.SVDAll: 249 q := blas64.General{Rows: m, Cols: m, Data: u, Stride: ldu} 250 if resid := residualOrthogonal(q, false); resid > tolOrtho*float64(m) { 251 t.Errorf("Case %v: columns of U are not orthogonal; resid=%v, want<=%v", prefix, resid, tolOrtho*float64(m)) 252 } 253 if res := svdPartialUResidual(m, m, u, uAll, ldu); res > tol { 254 t.Errorf("Case %v: columns of U do not match UAll", prefix) 255 } 256 } 257 // Check that VT has orthogonal rows and that it matches VTAll. 258 switch jobVT { 259 case lapack.SVDStore: 260 q := blas64.General{Rows: minmn, Cols: n, Data: vtAll, Stride: ldvt} 261 if resid := residualOrthogonal(q, true); resid > tolOrtho*float64(n) { 262 t.Errorf("Case %v: rows of VT are not orthogonal; resid=%v, want<=%v", prefix, resid, tolOrtho*float64(n)) 263 } 264 if res := svdPartialVTResidual(minmn, n, vt, vtAll, ldvt); res > tol { 265 t.Errorf("Case %v: rows of VT do not match VTAll", prefix) 266 } 267 case lapack.SVDAll: 268 q := blas64.General{Rows: n, Cols: n, Data: vtAll, Stride: ldvt} 269 if resid := residualOrthogonal(q, true); resid > tolOrtho*float64(n) { 270 t.Errorf("Case %v: rows of VT are not orthogonal; resid=%v, want<=%v", prefix, resid, tolOrtho*float64(n)) 271 } 272 if res := svdPartialVTResidual(n, n, vt, vtAll, ldvt); res > tol { 273 t.Errorf("Case %v: rows of VT do not match VTAll", prefix) 274 } 275 } 276 // Check that singular values are decreasing. 277 if !sort.IsSorted(sort.Reverse(sort.Float64Slice(s))) { 278 t.Errorf("Case %v: singular values from full SVD are not decreasing", prefix) 279 } 280 // Check that singular values are non-negative. 281 if floats.Min(s) < 0 { 282 t.Errorf("Case %v: some singular values from full SVD are negative", prefix) 283 } 284 if !floats.EqualApprox(s, sAll, tol/10) { 285 t.Errorf("Case %v: singular values differ between full and partial SVD\n%v\n%v", prefix, s, sAll) 286 } 287 } 288 } 289 } 290 } 291 292 // svdFullResidual returns 293 // 294 // |A - U*D*VT| / (n * aNorm) 295 // 296 // where U, D, and VT are as computed by Dgesvd with jobU = jobVT = lapack.SVDAll. 297 func svdFullResidual(m, n int, aNorm float64, a []float64, lda int, u []float64, ldu int, d []float64, vt []float64, ldvt int) float64 { 298 // The implementation follows TESTING/dbdt01.f from the reference. 299 300 minmn := min(m, n) 301 if minmn == 0 { 302 return 0 303 } 304 305 // j-th column of A - U*D*VT. 306 aMinusUDVT := make([]float64, m) 307 // D times the j-th column of VT. 308 dvt := make([]float64, minmn) 309 // Compute the residual |A - U*D*VT| one column at a time. 310 var resid float64 311 for j := 0; j < n; j++ { 312 // Copy j-th column of A to aj. 313 blas64.Copy(blas64.Vector{N: m, Data: a[j:], Inc: lda}, blas64.Vector{N: m, Data: aMinusUDVT, Inc: 1}) 314 // Multiply D times j-th column of VT. 315 for i := 0; i < minmn; i++ { 316 dvt[i] = d[i] * vt[i*ldvt+j] 317 } 318 // Compute the j-th column of A - U*D*VT. 319 blas64.Gemv(blas.NoTrans, 320 -1, blas64.General{Rows: m, Cols: minmn, Data: u, Stride: ldu}, blas64.Vector{N: minmn, Data: dvt, Inc: 1}, 321 1, blas64.Vector{N: m, Data: aMinusUDVT, Inc: 1}) 322 resid = math.Max(resid, blas64.Asum(blas64.Vector{N: m, Data: aMinusUDVT, Inc: 1})) 323 } 324 if aNorm == 0 { 325 if resid != 0 { 326 // Original matrix A is zero but the residual is non-zero, 327 // return infinity. 328 return math.Inf(1) 329 } 330 // Original matrix A is zero, residual is zero, return 0. 331 return 0 332 } 333 // Original matrix A is non-zero. 334 if aNorm >= resid { 335 resid = resid / aNorm / float64(n) 336 } else { 337 if aNorm < 1 { 338 resid = math.Min(resid, float64(n)*aNorm) / aNorm / float64(n) 339 } else { 340 resid = math.Min(resid/aNorm, float64(n)) / float64(n) 341 } 342 } 343 return resid 344 } 345 346 // svdPartialUResidual compares U and URef to see if their columns span the same 347 // spaces. It returns the maximum over columns of 348 // 349 // |URef(i) - S*U(i)| 350 // 351 // where URef(i) and U(i) are the i-th columns of URef and U, respectively, and 352 // S is ±1 chosen to minimize the expression. 353 func svdPartialUResidual(m, n int, u, uRef []float64, ldu int) float64 { 354 var res float64 355 for j := 0; j < n; j++ { 356 imax := blas64.Iamax(blas64.Vector{N: m, Data: uRef[j:], Inc: ldu}) 357 s := math.Copysign(1, uRef[imax*ldu+j]) * math.Copysign(1, u[imax*ldu+j]) 358 for i := 0; i < m; i++ { 359 diff := math.Abs(uRef[i*ldu+j] - s*u[i*ldu+j]) 360 res = math.Max(res, diff) 361 } 362 } 363 return res 364 } 365 366 // svdPartialVTResidual compares VT and VTRef to see if their rows span the same 367 // spaces. It returns the maximum over rows of 368 // 369 // |VTRef(i) - S*VT(i)| 370 // 371 // where VTRef(i) and VT(i) are the i-th columns of VTRef and VT, respectively, and 372 // S is ±1 chosen to minimize the expression. 373 func svdPartialVTResidual(m, n int, vt, vtRef []float64, ldvt int) float64 { 374 var res float64 375 for i := 0; i < m; i++ { 376 jmax := blas64.Iamax(blas64.Vector{N: n, Data: vtRef[i*ldvt:], Inc: 1}) 377 s := math.Copysign(1, vtRef[i*ldvt+jmax]) * math.Copysign(1, vt[i*ldvt+jmax]) 378 for j := 0; j < n; j++ { 379 diff := math.Abs(vtRef[i*ldvt+j] - s*vt[i*ldvt+j]) 380 res = math.Max(res, diff) 381 } 382 } 383 return res 384 }