| // Copyright ©2016 The Gonum Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package testlapack |
| |
| import ( |
| "fmt" |
| "math" |
| "testing" |
| |
| "golang.org/x/exp/rand" |
| |
| "gonum.org/v1/gonum/blas" |
| "gonum.org/v1/gonum/blas/blas64" |
| "gonum.org/v1/gonum/lapack" |
| ) |
| |
| type Dtrevc3er interface { |
| Dtrevc3(side lapack.EVSide, howmny lapack.EVHowMany, selected []bool, n int, t []float64, ldt int, vl []float64, ldvl int, vr []float64, ldvr int, mm int, work []float64, lwork int) int |
| } |
| |
| func Dtrevc3Test(t *testing.T, impl Dtrevc3er) { |
| rnd := rand.New(rand.NewSource(1)) |
| |
| for _, side := range []lapack.EVSide{lapack.EVRight, lapack.EVLeft, lapack.EVBoth} { |
| var name string |
| switch side { |
| case lapack.EVRight: |
| name = "EVRigth" |
| case lapack.EVLeft: |
| name = "EVLeft" |
| case lapack.EVBoth: |
| name = "EVBoth" |
| } |
| t.Run(name, func(t *testing.T) { |
| runDtrevc3Test(t, impl, rnd, side) |
| }) |
| } |
| } |
| |
| func runDtrevc3Test(t *testing.T, impl Dtrevc3er, rnd *rand.Rand, side lapack.EVSide) { |
| for _, n := range []int{0, 1, 2, 3, 4, 5, 6, 7, 10, 34} { |
| for _, extra := range []int{0, 11} { |
| for _, optwork := range []bool{true, false} { |
| for cas := 0; cas < 10; cas++ { |
| dtrevc3Test(t, impl, side, n, extra, optwork, rnd) |
| } |
| } |
| } |
| } |
| } |
| |
| // dtrevc3Test tests Dtrevc3 by generating a random matrix T in Schur canonical |
| // form and performing the following checks: |
| // 1. Compute all eigenvectors of T and check that they are indeed correctly |
| // normalized eigenvectors |
| // 2. Compute selected eigenvectors and check that they are exactly equal to |
| // eigenvectors from check 1. |
| // 3. Compute all eigenvectors multiplied into a matrix Q and check that the |
| // result is equal to eigenvectors from step 1 multiplied by Q and scaled |
| // appropriately. |
| func dtrevc3Test(t *testing.T, impl Dtrevc3er, side lapack.EVSide, n, extra int, optwork bool, rnd *rand.Rand) { |
| const tol = 1e-15 |
| |
| name := fmt.Sprintf("n=%d,extra=%d,optwk=%v", n, extra, optwork) |
| |
| right := side != lapack.EVLeft |
| left := side != lapack.EVRight |
| |
| // Generate a random matrix in Schur canonical form possibly with tiny or zero eigenvalues. |
| // Zero elements of wi signify a real eigenvalue. |
| tmat, wr, wi := randomSchurCanonical(n, n+extra, true, rnd) |
| tmatCopy := cloneGeneral(tmat) |
| |
| // 1. Compute all eigenvectors of T and check that they are indeed correctly |
| // normalized eigenvectors |
| |
| howmny := lapack.EVAll |
| |
| var vr, vl blas64.General |
| if right { |
| // Fill VR and VL with NaN because they should be completely overwritten in Dtrevc3. |
| vr = nanGeneral(n, n, n+extra) |
| } |
| if left { |
| vl = nanGeneral(n, n, n+extra) |
| } |
| |
| var work []float64 |
| if optwork { |
| work = []float64{0} |
| impl.Dtrevc3(side, howmny, nil, n, tmat.Data, tmat.Stride, |
| vl.Data, max(1, vl.Stride), vr.Data, max(1, vr.Stride), n, work, -1) |
| work = make([]float64, int(work[0])) |
| } else { |
| work = make([]float64, max(1, 3*n)) |
| } |
| |
| mGot := impl.Dtrevc3(side, howmny, nil, n, tmat.Data, tmat.Stride, |
| vl.Data, max(1, vl.Stride), vr.Data, max(1, vr.Stride), n, work, len(work)) |
| |
| if !generalOutsideAllNaN(tmat) { |
| t.Errorf("%v: out-of-range write to T", name) |
| } |
| if !equalGeneral(tmat, tmatCopy) { |
| t.Errorf("%v: unexpected modification of T", name) |
| } |
| if !generalOutsideAllNaN(vr) { |
| t.Errorf("%v: out-of-range write to VR", name) |
| } |
| if !generalOutsideAllNaN(vl) { |
| t.Errorf("%v: out-of-range write to VL", name) |
| } |
| |
| mWant := n |
| if mGot != mWant { |
| t.Errorf("%v: unexpected value of m=%d, want %d", name, mGot, mWant) |
| } |
| |
| if right { |
| resid := residualRightEV(tmat, vr, wr, wi) |
| if resid > tol { |
| t.Errorf("%v: unexpected right eigenvectors; residual=%v, want<=%v", name, resid, tol) |
| } |
| resid = residualEVNormalization(vr, wi) |
| if resid > tol { |
| t.Errorf("%v: unexpected normalization of right eigenvectors; residual=%v, want<=%v", name, resid, tol) |
| } |
| } |
| if left { |
| resid := residualLeftEV(tmat, vl, wr, wi) |
| if resid > tol { |
| t.Errorf("%v: unexpected left eigenvectors; residual=%v, want<=%v", name, resid, tol) |
| } |
| resid = residualEVNormalization(vl, wi) |
| if resid > tol { |
| t.Errorf("%v: unexpected normalization of left eigenvectors; residual=%v, want<=%v", name, resid, tol) |
| } |
| } |
| |
| // 2. Compute selected eigenvectors and check that they are exactly equal to |
| // eigenvectors from check 1. |
| |
| howmny = lapack.EVSelected |
| |
| // Follow DCHKHS and select last max(1,n/4) real, max(1,n/4) complex |
| // eigenvectors instead of selecting them randomly. |
| selected := make([]bool, n) |
| selectedWant := make([]bool, n) |
| var nselr, nselc int |
| for j := n - 1; j > 0; { |
| if wi[j] == 0 { |
| if nselr < max(1, n/4) { |
| nselr++ |
| selected[j] = true |
| selectedWant[j] = true |
| } |
| j-- |
| } else { |
| if nselc < max(1, n/4) { |
| nselc++ |
| // Select all columns to check that Dtrevc3 normalizes 'selected' correctly. |
| selected[j] = true |
| selected[j-1] = true |
| selectedWant[j] = false |
| selectedWant[j-1] = true |
| } |
| j -= 2 |
| } |
| } |
| mWant = nselr + 2*nselc |
| |
| var vrSel, vlSel blas64.General |
| if right { |
| vrSel = nanGeneral(n, mWant, n+extra) |
| } |
| if left { |
| vlSel = nanGeneral(n, mWant, n+extra) |
| } |
| |
| if optwork { |
| // Reallocate optimal work in case it depends on howmny and selected. |
| work = []float64{0} |
| impl.Dtrevc3(side, howmny, selected, n, tmat.Data, tmat.Stride, |
| vlSel.Data, max(1, vlSel.Stride), vrSel.Data, max(1, vrSel.Stride), mWant, work, -1) |
| work = make([]float64, int(work[0])) |
| } |
| |
| mGot = impl.Dtrevc3(side, howmny, selected, n, tmat.Data, tmat.Stride, |
| vlSel.Data, max(1, vlSel.Stride), vrSel.Data, max(1, vrSel.Stride), mWant, work, len(work)) |
| |
| if !generalOutsideAllNaN(tmat) { |
| t.Errorf("%v: out-of-range write to T", name) |
| } |
| if !equalGeneral(tmat, tmatCopy) { |
| t.Errorf("%v: unexpected modification of T", name) |
| } |
| if !generalOutsideAllNaN(vrSel) { |
| t.Errorf("%v: out-of-range write to selected VR", name) |
| } |
| if !generalOutsideAllNaN(vlSel) { |
| t.Errorf("%v: out-of-range write to selected VL", name) |
| } |
| |
| if mGot != mWant { |
| t.Errorf("%v: unexpected value of selected m=%d, want %d", name, mGot, mWant) |
| } |
| |
| for i := range selected { |
| if selected[i] != selectedWant[i] { |
| t.Errorf("%v: unexpected selected[%v]", name, i) |
| } |
| } |
| |
| // Check that selected columns of vrSel are equal to the corresponding |
| // columns of vr. |
| var k int |
| match := true |
| if right { |
| loopVR: |
| for j := 0; j < n; j++ { |
| if selected[j] && wi[j] == 0 { |
| for i := 0; i < n; i++ { |
| if vrSel.Data[i*vrSel.Stride+k] != vr.Data[i*vr.Stride+j] { |
| match = false |
| break loopVR |
| } |
| } |
| k++ |
| } else if selected[j] && wi[j] != 0 { |
| for i := 0; i < n; i++ { |
| if vrSel.Data[i*vrSel.Stride+k] != vr.Data[i*vr.Stride+j] || |
| vrSel.Data[i*vrSel.Stride+k+1] != vr.Data[i*vr.Stride+j+1] { |
| match = false |
| break loopVR |
| } |
| } |
| k += 2 |
| } |
| } |
| } |
| if !match { |
| t.Errorf("%v: unexpected selected VR", name) |
| } |
| |
| // Check that selected columns of vlSel are equal to the corresponding |
| // columns of vl. |
| match = true |
| k = 0 |
| if left { |
| loopVL: |
| for j := 0; j < n; j++ { |
| if selected[j] && wi[j] == 0 { |
| for i := 0; i < n; i++ { |
| if vlSel.Data[i*vlSel.Stride+k] != vl.Data[i*vl.Stride+j] { |
| match = false |
| break loopVL |
| } |
| } |
| k++ |
| } else if selected[j] && wi[j] != 0 { |
| for i := 0; i < n; i++ { |
| if vlSel.Data[i*vlSel.Stride+k] != vl.Data[i*vl.Stride+j] || |
| vlSel.Data[i*vlSel.Stride+k+1] != vl.Data[i*vl.Stride+j+1] { |
| match = false |
| break loopVL |
| } |
| } |
| k += 2 |
| } |
| } |
| } |
| if !match { |
| t.Errorf("%v: unexpected selected VL", name) |
| } |
| |
| // 3. Compute all eigenvectors multiplied into a matrix Q and check that the |
| // result is equal to eigenvectors from step 1 multiplied by Q and scaled |
| // appropriately. |
| |
| howmny = lapack.EVAllMulQ |
| |
| var vrMul, qr blas64.General |
| var vlMul, ql blas64.General |
| if right { |
| vrMul = randomGeneral(n, n, n+extra, rnd) |
| qr = cloneGeneral(vrMul) |
| } |
| if left { |
| vlMul = randomGeneral(n, n, n+extra, rnd) |
| ql = cloneGeneral(vlMul) |
| } |
| |
| if optwork { |
| // Reallocate optimal work in case it depends on howmny and selected. |
| work = []float64{0} |
| impl.Dtrevc3(side, howmny, nil, n, tmat.Data, tmat.Stride, |
| vlMul.Data, max(1, vlMul.Stride), vrMul.Data, max(1, vrMul.Stride), n, work, -1) |
| work = make([]float64, int(work[0])) |
| } |
| |
| mGot = impl.Dtrevc3(side, howmny, selected, n, tmat.Data, tmat.Stride, |
| vlMul.Data, max(1, vlMul.Stride), vrMul.Data, max(1, vrMul.Stride), n, work, len(work)) |
| |
| if !generalOutsideAllNaN(tmat) { |
| t.Errorf("%v: out-of-range write to T", name) |
| } |
| if !equalGeneral(tmat, tmatCopy) { |
| t.Errorf("%v: unexpected modification of T", name) |
| } |
| if !generalOutsideAllNaN(vrMul) { |
| t.Errorf("%v: out-of-range write to VRMul", name) |
| } |
| if !generalOutsideAllNaN(vlMul) { |
| t.Errorf("%v: out-of-range write to VLMul", name) |
| } |
| |
| mWant = n |
| if mGot != mWant { |
| t.Errorf("%v: unexpected value of m=%d, want %d", name, mGot, mWant) |
| } |
| |
| if right { |
| // Compute Q * VR explicitly and normalize to match Dtrevc3 output. |
| qvWant := zeros(n, n, n) |
| blas64.Gemm(blas.NoTrans, blas.NoTrans, 1, qr, vr, 0, qvWant) |
| normalizeEV(qvWant, wi) |
| |
| // Compute the difference between Dtrevc3 output and Q * VR. |
| r := zeros(n, n, n) |
| for i := 0; i < n; i++ { |
| for j := 0; j < n; j++ { |
| r.Data[i*r.Stride+j] = vrMul.Data[i*vrMul.Stride+j] - qvWant.Data[i*qvWant.Stride+j] |
| } |
| } |
| qvNorm := dlange(lapack.MaxColumnSum, n, n, qvWant.Data, qvWant.Stride) |
| resid := dlange(lapack.MaxColumnSum, n, n, r.Data, r.Stride) / qvNorm / float64(n) |
| if resid > tol { |
| t.Errorf("%v: unexpected VRMul; resid=%v, want <=%v", name, resid, tol) |
| } |
| } |
| if left { |
| // Compute Q * VL explicitly and normalize to match Dtrevc3 output. |
| qvWant := zeros(n, n, n) |
| blas64.Gemm(blas.NoTrans, blas.NoTrans, 1, ql, vl, 0, qvWant) |
| normalizeEV(qvWant, wi) |
| |
| // Compute the difference between Dtrevc3 output and Q * VL. |
| r := zeros(n, n, n) |
| for i := 0; i < n; i++ { |
| for j := 0; j < n; j++ { |
| r.Data[i*r.Stride+j] = vlMul.Data[i*vlMul.Stride+j] - qvWant.Data[i*qvWant.Stride+j] |
| } |
| } |
| qvNorm := dlange(lapack.MaxColumnSum, n, n, qvWant.Data, qvWant.Stride) |
| resid := dlange(lapack.MaxColumnSum, n, n, r.Data, r.Stride) / qvNorm / float64(n) |
| if resid > tol { |
| t.Errorf("%v: unexpected VLMul; resid=%v, want <=%v", name, resid, tol) |
| } |
| } |
| } |
| |
| // residualEVNormalization returns the maximum normalization error in E: |
| // max |max-norm(E[:,j]) - 1| |
| func residualEVNormalization(emat blas64.General, wi []float64) float64 { |
| n := emat.Rows |
| if n == 0 { |
| return 0 |
| } |
| var ( |
| e = emat.Data |
| lde = emat.Stride |
| enrmin = math.Inf(1) |
| enrmax float64 |
| ipair int |
| ) |
| for j := 0; j < n; j++ { |
| if ipair == 0 && j < n-1 && wi[j] != 0 { |
| ipair = 1 |
| } |
| var nrm float64 |
| switch ipair { |
| case 0: |
| // Real eigenvector |
| for i := 0; i < n; i++ { |
| nrm = math.Max(nrm, math.Abs(e[i*lde+j])) |
| } |
| enrmin = math.Min(enrmin, nrm) |
| enrmax = math.Max(enrmax, nrm) |
| case 1: |
| // Complex eigenvector |
| for i := 0; i < n; i++ { |
| nrm = math.Max(nrm, math.Abs(e[i*lde+j])+math.Abs(e[i*lde+j+1])) |
| } |
| enrmin = math.Min(enrmin, nrm) |
| enrmax = math.Max(enrmax, nrm) |
| ipair = 2 |
| case 2: |
| ipair = 0 |
| } |
| } |
| return math.Max(math.Abs(enrmin-1), math.Abs(enrmin-1)) |
| } |
| |
| // normalizeEV normalizes eigenvectors in the columns of E so that the element |
| // of largest magnitude has magnitude 1. |
| func normalizeEV(emat blas64.General, wi []float64) { |
| n := emat.Rows |
| if n == 0 { |
| return |
| } |
| var ( |
| bi = blas64.Implementation() |
| e = emat.Data |
| lde = emat.Stride |
| ipair int |
| ) |
| for j := 0; j < n; j++ { |
| if ipair == 0 && j < n-1 && wi[j] != 0 { |
| ipair = 1 |
| } |
| switch ipair { |
| case 0: |
| // Real eigenvector |
| ii := bi.Idamax(n, e[j:], lde) |
| remax := 1 / math.Abs(e[ii*lde+j]) |
| bi.Dscal(n, remax, e[j:], lde) |
| case 1: |
| // Complex eigenvector |
| var emax float64 |
| for i := 0; i < n; i++ { |
| emax = math.Max(emax, math.Abs(e[i*lde+j])+math.Abs(e[i*lde+j+1])) |
| } |
| bi.Dscal(n, 1/emax, e[j:], lde) |
| bi.Dscal(n, 1/emax, e[j+1:], lde) |
| ipair = 2 |
| case 2: |
| ipair = 0 |
| } |
| } |
| } |