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