diff --git a/src/java/org/apache/commons/math/linear/EigenDecomposition.java b/src/java/org/apache/commons/math/linear/EigenDecomposition.java index e2770195e..19e609c58 100644 --- a/src/java/org/apache/commons/math/linear/EigenDecomposition.java +++ b/src/java/org/apache/commons/math/linear/EigenDecomposition.java @@ -35,7 +35,11 @@ package org.apache.commons.math.linear; * been added (in the superinterface), *
  • a {@link DecompositionSolver#getInverse() getInverse} method has been * added (in the superinterface),
  • - *
  • a {@link #getVt() getVt} method has been added,
  • + *
  • a {@link #getVT() getVt} method has been added,
  • + *
  • a {@link #getEigenvalue(int) getEigenvalue} method to pick up a single + * eigenvalue has been added,
  • + *
  • a {@link #getEigenvector(int) getEigenvector} method to pick up a single + * eigenvector has been added,
  • *
  • the getRealEigenvalues method has been renamed as {@link * #getEigenValues() getEigenValues},
  • *
  • the getImagEigenvalues method has been removed
  • @@ -76,15 +80,43 @@ public interface EigenDecomposition extends DecompositionSolver { * @exception IllegalStateException if {@link * DecompositionSolver#decompose(RealMatrix) decompose} has not been called */ - RealMatrix getVt() throws IllegalStateException; + RealMatrix getVT() throws IllegalStateException; /** - * Returns the eigenvalues of the original matrix. - * @return eigenvalues of the original matrix + * Returns a copy of the eigenvalues of the original matrix. + * @return a copy of the eigenvalues of the original matrix * @exception IllegalStateException if {@link * DecompositionSolver#decompose(RealMatrix) decompose} has not been called * @see #getD() */ - double[] getEigenValues() throws IllegalStateException; + double[] getEigenvalues() throws IllegalStateException; + + /** + * Returns the ith eigenvalue of the original matrix. + * @return ith eigenvalue of the original matrix + * @exception IllegalStateException if {@link + * DecompositionSolver#decompose(RealMatrix) decompose} has not been called + * @exception ArrayIndexOutOfBoundsException if i is not + * @see #getD() + */ + double getEigenvalue(int i) throws IllegalStateException; + + /** + * Returns a copy of the ith eigenvector of the original matrix. + * @return copy of the ith eigenvector of the original matrix + * @exception IllegalStateException if {@link + * DecompositionSolver#decompose(RealMatrix) decompose} has not been called + * @see #getD() + */ + RealVector getEigenvector(int i) throws IllegalStateException; + + /** + * Return the determinant of the matrix + * @return determinant of the matrix + * @exception IllegalStateException if {@link + * DecompositionSolver#decompose(RealMatrix) decompose} has not been called + * @see #isNonSingular() + */ + double getDeterminant() throws IllegalStateException; } diff --git a/src/java/org/apache/commons/math/linear/EigenDecompositionImpl.java b/src/java/org/apache/commons/math/linear/EigenDecompositionImpl.java new file mode 100644 index 000000000..2e438dc87 --- /dev/null +++ b/src/java/org/apache/commons/math/linear/EigenDecompositionImpl.java @@ -0,0 +1,658 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.math.linear; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.commons.math.ConvergenceException; + +/** + * Calculates the eigen decomposition of a matrix. + *

    The eigen decomposition of matrix A is a set of two matrices: + * V and D such that A = V × D × VT. + * Let A be an m × n matrix, then V is an m × m orthogonal matrix + * and D is a m × n diagonal matrix.

    + *

    This implementation is based on Inderjit Singh Dhillon thesis + * A + * New O(n2) Algorithm for the Symmetric Tridiagonal Eigenvalue/Eigenvector + * Problem.

    + * @version $Revision$ $Date$ + * @since 2.0 + */ +public class EigenDecompositionImpl implements EigenDecomposition { + + /** Serializable version identifier. */ + private static final long serialVersionUID = -8550254713195393577L; + + /** Eigenvalues. */ + private double[] eigenvalues; + + /** Eigenvectors. */ + private RealVectorImpl[] eigenvectors; + + /** Cached value of V. */ + private RealMatrix cachedV; + + /** Cached value of D. */ + private RealMatrix cachedD; + + /** Cached value of Vt. */ + private RealMatrix cachedVt; + + /** + * Build a new instance. + *

    Note that {@link #decompose(RealMatrix)} must be called + * before any of the {@link #getV()}, {@link #getD()}, {@link #getVT()}, + * {@link #getEignevalues()}, {@link #solve(double[])}, {@link #solve(RealMatrix)}, + * {@link #solve(RealVector)} or {@link #solve(RealVectorImpl)} methods can be + * called.

    + * @see #decompose(RealMatrix) + */ + public EigenDecompositionImpl() { + } + + /** + * Calculates the eigen decomposition of the given matrix. + *

    Calling this constructor is equivalent to first call the no-arguments + * constructor and then call {@link #decompose(RealMatrix)}.

    + * @param matrix The matrix to decompose. + * @exception InvalidMatrixException (wrapping a {@link ConvergenceException} + * if algorithm fails to converge + */ + public EigenDecompositionImpl(RealMatrix matrix) + throws InvalidMatrixException { + decompose(matrix); + } + + /** {@inheritDoc} */ + public void decompose(RealMatrix matrix) + throws InvalidMatrixException { + + cachedV = null; + cachedD = null; + cachedVt = null; + + // transform the matrix to tridiagonal + TriDiagonalTransformer transformer = new TriDiagonalTransformer(matrix); + final double[] main = transformer.getMainDiagonalRef(); + final double[] secondary = transformer.getSecondaryDiagonalRef(); + final int m = main.length; + + // pre-compute the square of the secondary diagonal + double[] squaredSecondary = new double[secondary.length]; + for (int i = 0; i < squaredSecondary.length; ++i) { + final double s = secondary[i]; + squaredSecondary[i] = s * s; + } + + // compute the eigenvalues bounds + List bounds = + getEigenvaluesBounds(main, secondary); + + // TODO this implementation is not finished yet + // the MRRR algorithm is NOT implemented, Gershgorin circles are + // merged together when they could be separated, we only perform blindly + // the basic steps, we search all eigenvalues with an arbitrary + // threshold, we use twisted factorization afterwards with no + // heuristic to speed up the selection of the twist index ... + // The decomposition does work in its current state and seems reasonably + // efficient when eigenvalues are separated. However, it is expected to + // fail in difficult cases and its performances can obviously be improved + // for now, it is slower than JAMA for dimensions below 100 and faster + // for dimensions above 100. The speed gain with respect to JAMA increase + // regularly with dimension + + // find eigenvalues using bisection + eigenvalues = new double[m]; + final double low = bounds.get(0).getLow(); + final double high = bounds.get(bounds.size() - 1).getHigh(); + final double threshold = + 1.0e-15 * Math.max(Math.abs(low), Math.abs(high)); + findEigenvalues(main, squaredSecondary, low, high, threshold, 0, m); + + // find eigenvectors + eigenvectors = new RealVectorImpl[m]; + final double[] eigenvector = new double[m]; + final double[] lp = new double[m - 1]; + final double[] dp = new double[m]; + final double[] um = new double[m - 1]; + final double[] dm = new double[m]; + final double[] gamma = new double[m]; + for (int i = 0; i < m; ++i) { + + // find the eigenvector of the tridiagonal matrix + findEigenvector(eigenvalues[i], eigenvector, + main, secondary, lp, dp, um, dm, gamma); + + // find the eigenvector of the original matrix + eigenvectors[i] = + new RealVectorImpl(transformer.getQ().operate(eigenvector), true); + + } + + } + + /** {@inheritDoc} */ + public RealMatrix getV() + throws InvalidMatrixException { + + if (cachedV == null) { + cachedV = getVT().transpose(); + } + + // return the cached matrix + return cachedV; + + } + + /** {@inheritDoc} */ + public RealMatrix getD() + throws InvalidMatrixException { + + if (cachedD == null) { + + checkDecomposed(); + + final int m = eigenvalues.length; + final double[][] sData = new double[m][m]; + for (int i = 0; i < m; ++i) { + sData[i][i] = eigenvalues[i]; + } + + // cache the matrix for subsequent calls + cachedD = new RealMatrixImpl(sData, false); + + } + return cachedD; + } + + /** {@inheritDoc} */ + public RealMatrix getVT() + throws InvalidMatrixException { + + if (cachedVt == null) { + + checkDecomposed(); + + final double[][] vtData = new double[eigenvectors.length][]; + for (int k = 0; k < eigenvectors.length; ++k) { + vtData[k] = eigenvectors[k].getData(); + } + + // cache the matrix for subsequent calls + cachedVt = new RealMatrixImpl(vtData, false); + + } + + // return the cached matrix + return cachedVt; + + } + + /** {@inheritDoc} */ + public double[] getEigenvalues() + throws InvalidMatrixException { + checkDecomposed(); + return eigenvalues.clone(); + } + + /** {@inheritDoc} */ + public double getEigenvalue(final int i) + throws InvalidMatrixException, ArrayIndexOutOfBoundsException { + checkDecomposed(); + return eigenvalues[i]; + } + + /** {@inheritDoc} */ + public RealVector getEigenvector(final int i) + throws InvalidMatrixException, ArrayIndexOutOfBoundsException { + checkDecomposed(); + return eigenvectors[i].copy(); + } + + /** {@inheritDoc} */ + public boolean isNonSingular() + throws IllegalStateException { + for (double lambda : eigenvalues) { + if (lambda == 0) { + return false; + } + } + return true; + } + + /** {@inheritDoc} */ + public double[] solve(final double[] b) + throws IllegalArgumentException, InvalidMatrixException { + + checkNonSingular(); + + final int m = eigenvalues.length; + if (b.length != m) { + throw new IllegalArgumentException("constant vector has wrong length"); + } + + final double[] bp = new double[m]; + for (int i = 0; i < m; ++i) { + final RealVectorImpl v = eigenvectors[i]; + final double s = v.dotProduct(b) / eigenvalues[i]; + final double[] vData = v.getDataRef(); + for (int j = 0; j < m; ++j) { + bp[j] += s * vData[j]; + } + } + + return bp; + + } + + /** {@inheritDoc} */ + public RealVector solve(final RealVector b) + throws IllegalArgumentException, InvalidMatrixException { + try { + return solve((RealVectorImpl) b); + } catch (ClassCastException cce) { + + checkNonSingular(); + + final int m = eigenvalues.length; + if (b.getDimension() != m) { + throw new IllegalArgumentException("constant vector has wrong length"); + } + + final double[] bp = new double[m]; + for (int i = 0; i < m; ++i) { + final RealVectorImpl v = eigenvectors[i]; + final double s = v.dotProduct(b) / eigenvalues[i]; + final double[] vData = v.getDataRef(); + for (int j = 0; j < m; ++j) { + bp[j] += s * vData[j]; + } + } + + return new RealVectorImpl(bp, false); + + } + } + + /** Solve the linear equation A × X = B. + *

    The A matrix is implicit here. It is

    + * @param b right-hand side of the equation A × X = B + * @return a vector X such that A × X = B + * @throws IllegalArgumentException if matrices dimensions don't match + * @throws InvalidMatrixException if decomposed matrix is singular + */ + public RealVectorImpl solve(final RealVectorImpl b) + throws IllegalArgumentException, InvalidMatrixException { + return new RealVectorImpl(solve(b.getDataRef()), false); + } + + /** {@inheritDoc} */ + public RealMatrix solve(final RealMatrix b) + throws IllegalArgumentException, InvalidMatrixException { + + checkNonSingular(); + + final int m = eigenvalues.length; + if (b.getRowDimension() != m) { + throw new IllegalArgumentException("Incorrect row dimension"); + } + + final int nColB = b.getColumnDimension(); + final double[][] bp = new double[m][nColB]; + for (int k = 0; k < nColB; ++k) { + for (int i = 0; i < m; ++i) { + final double[] vData = eigenvectors[i].getDataRef(); + double s = 0; + for (int j = 0; j < m; ++j) { + s += vData[j] * b.getEntry(j, k); + } + s /= eigenvalues[i]; + for (int j = 0; j < m; ++j) { + bp[j][k] += s * vData[j]; + } + } + } + + return new RealMatrixImpl(bp, false); + + } + + /** {@inheritDoc} */ + public RealMatrix getInverse() + throws IllegalStateException, InvalidMatrixException { + + checkNonSingular(); + final int m = eigenvalues.length; + final double[][] invData = new double[m][m]; + + for (int i = 0; i < m; ++i) { + final double[] invI = invData[i]; + for (int j = 0; j < m; ++j) { + double invIJ = 0; + for (int k = 0; k < m; ++k) { + final double[] vK = eigenvectors[k].getDataRef(); + invIJ += vK[i] * vK[j] / eigenvalues[k]; + } + invI[j] = invIJ; + } + } + return new RealMatrixImpl(invData, false); + + } + + /** {@inheritDoc} */ + public double getDeterminant() + throws IllegalStateException { + double determinant = 1; + for (double lambda : eigenvalues) { + determinant *= lambda; + } + return determinant; + } + + /** + * Compute a set of possible bounding intervals for eigenvalues + * of a symmetric tridiagonal matrix. + *

    The intervals are computed by applying the Gershgorin circle theorem.

    + * @param main main diagonal + * @param secondary secondary diagonal of the tridiagonal matrix + * @return a collection of disjoint intervals where eigenvalues must lie, + * sorted in increasing order + */ + private List getEigenvaluesBounds(final double[] main, + final double[] secondary) { + + final SortedSet rawCircles = + new TreeSet(); + final int m = main.length; + + // compute all the Gershgorin circles independently + rawCircles.add(new GershgorinCirclesUnion(main[0], + Math.abs(secondary[0]))); + for (int i = 1; i < m - 1; ++i) { + rawCircles.add(new GershgorinCirclesUnion(main[i], + Math.abs(secondary[i - 1]) + + Math.abs(secondary[i]))); + } + rawCircles.add(new GershgorinCirclesUnion(main[m - 1], + Math.abs(secondary[m - 2]))); + + // combine intersecting circles + final ArrayList combined = + new ArrayList(); + GershgorinCirclesUnion current = null; + for (GershgorinCirclesUnion rawCircle : rawCircles) { + if (current == null) { + current = rawCircle; + } else if (current.intersects(rawCircle)) { + current.swallow(rawCircle); + } else { + combined.add(current); + current = rawCircle; + } + } + if (current != null) { + combined.add(current); + } + + return combined; + + } + + /** Find eigenvalues in an interval. + * @param main main diagonal of the tridiagonal matrix + * @param squaredSecondary squared secondary diagonal of the tridiagonal matrix + * @param low lower bound of the search interval + * @param high higher bound of the search interval + * @param threshold convergence threshold + * @param iStart index of the first eigenvalue to find + * @param iEnd index one unit past the last eigenvalue to find + */ + private void findEigenvalues(final double[] main, final double[] squaredSecondary, + final double low, final double high, final double threshold, + final int iStart, final int iEnd) { + + // use a simple loop to handle tail-recursion cases + double currentLow = low; + double currentHigh = high; + int currentStart = iStart; + while (true) { + + final double middle = 0.5 * (currentLow + currentHigh); + + if (currentHigh - currentLow < threshold) { + // we have found an elementary interval containing one or more eigenvalues + Arrays.fill(eigenvalues, currentStart, iEnd, middle); + return; + } + + // compute the number of eigenvalues below the middle interval point + final int iMiddle = countEigenValues(main, squaredSecondary, middle); + if (iMiddle == currentStart) { + // all eigenvalues are in the upper half of the search interval + // update the interval and iterate + currentLow = middle; + } else if (iMiddle == iEnd) { + // all eigenvalues are in the lower half of the search interval + // update the interval and iterate + currentHigh = middle; + } else { + // split the interval and search eigenvalues in both sub-intervals + findEigenvalues(main, squaredSecondary, currentLow, middle, threshold, + currentStart, iMiddle); + currentLow = middle; + currentStart = iMiddle; + } + + } + + } + + /** + * Count the number of eigenvalues below a point. + * @param main main diagonal of the tridiagonal matrix + * @param squaredSecondary squared secondary diagonal of the tridiagonal matrix + * @param mu value below which we must count the number of eigenvalues + * @return number of eigenvalues smaller than mu + */ + private int countEigenValues(final double[] main, final double[] squaredSecondary, + final double mu) { + double ratio = main[0] - mu; + int count = (ratio > 0) ? 0 : 1; + for (int i = 1; i < main.length; ++i) { + ratio = main[i] - squaredSecondary[i - 1] / ratio - mu; + if (ratio <= 0) { + ++count; + } + } + return count; + } + + /** + * Decompose the shifted tridiagonal matrix A - lambda I as L+ + * × D+ × U+. + *

    A shifted symmetric tridiagonal matrix can be decomposed as + * L+ × D+ × U+ where L+ + * is a lower bi-diagonal matrix with unit diagonal, D+ is a diagonal + * matrix and U+ is the transpose of L+. The '+' indice + * comes from Dhillon's notation since decomposition is done in + * increasing rows order).

    + * @param main main diagonal of the tridiagonal matrix + * @param secondary secondary diagonal of the tridiagonal matrix + * @param lambda shift to apply to the matrix before decomposing it + * @param r index at which factorization should stop (if r is + * main.length, complete factorization is performed) + * @param lp placeholder where to put the (r-1) first off-diagonal + * elements of the L+ matrix + * @param dp placeholder where to put the r first diagonal elements + * of the D+ matrix + */ + private void lduDecomposition(final double[] main, final double[] secondary, + final double lambda, final int r, + final double[] lp, final double[] dp) { + double di = main[0] - lambda; + dp[0] = di; + for (int i = 1; i < r; ++i) { + final double eiM1 = secondary[i - 1]; + final double ratio = eiM1 / di; + di = main[i] - lambda - eiM1 * ratio; + lp[i - 1] = ratio; + dp[i] = di; + } + } + + /** + * Decompose the shifted tridiagonal matrix A - lambda I as U- + * × D- × L-. + *

    A shifted symmetric tridiagonal matrix can be decomposed as + * U- × D- × L- where U- + * is an upper bi-diagonal matrix with unit diagonal, D- is a diagonal + * matrix and L- is the transpose of U-. The '-' indice + * comes from Dhillon's notation since decomposition is done in + * decreasing rows order).

    + * @param main main diagonal of the tridiagonal matrix + * @param secondary secondary diagonal of the tridiagonal matrix + * @param lambda shift to apply to the matrix before decomposing it + * @param r index at which factorization should stop (if r is 0, complete + * factorization is performed) + * @param um placeholder where to put the m-(r-1) last off-diagonal elements + * of the U- matrix, where m is the size of the original matrix + * @param dm placeholder where to put the m-r last diagonal elements + * of the D- matrix, where m is the size of the original matrix + */ + private void udlDecomposition(final double[] main, final double[] secondary, + final double lambda, final int r, + final double[] um, final double[] dm) { + final int mM1 = main.length - 1; + double di = main[mM1] - lambda; + dm[mM1] = di; + for (int i = mM1 - 1; i >= r; --i) { + final double ei = secondary[i]; + final double ratio = ei / di; + di = main[i] - lambda - ei * ratio; + um[i] = ratio; + dm[i] = di; + } + } + + /** + * Find an eigenvector corresponding to an eigenvalue. + * @param eigenvalue eigenvalue for which eigenvector is desired + * @param eigenvector placeholder where to put the eigenvector + * @param main main diagonal of the tridiagonal matrix + * @param secondary secondary diagonal of the tridiagonal matrix + * @param lp placeholder where to put the off-diagonal elements of the + * L+ matrix + * @param dp placeholder where to put the diagonal elements of the + * D+ matrix + * @param um placeholder where to put the off-diagonal elements of the + * U- matrix + * @param dm placeholder where to put the diagonal elements of the + * D- matrix + * @param gamma placeholder where to put the twist elements for all + * possible twist indices + */ + private void findEigenvector(final double eigenvalue, final double[] eigenvector, + final double[] main, final double[] secondary, + final double[] lp, final double[] dp, + final double[] um, final double[] dm, + final double[] gamma) { + + // compute the LDU and UDL decomposition of the + // perfectly shifted tridiagonal matrix + final int m = main.length; + lduDecomposition(main, secondary, eigenvalue, m, lp, dp); + udlDecomposition(main, secondary, eigenvalue, 0, um, dm); + + // select the twist index leading to + // the least diagonal element in the twisted factorization + int r = 0; + double g = dp[0] + dm[0] + eigenvalue - main[0]; + gamma[0] = g; + double minG = Math.abs(g); + for (int i = 1; i < m; ++i) { + if (i < m - 1) { + g *= dm[i + 1] / dp[i]; + } else { + g = dp[m - 1] + dm[m - 1] + eigenvalue - main[m - 1]; + } + gamma[i] = g; + final double absG = Math.abs(g); + if (absG < minG) { + r = i; + minG = absG; + } + } + + // solve the singular system by ignoring the equation + // at twist index and propagating upwards and downwards + double n2 = 1; + eigenvector[r] = 1; + double z = 1; + for (int i = r - 1; i >= 0; --i) { + z *= -lp[i]; + eigenvector[i] = z; + n2 += z * z; + } + z = 1; + for (int i = r + 1; i < m; ++i) { + z *= -um[i-1]; + eigenvector[i] = z; + n2 += z * z; + } + + // normalize vector + final double inv = 1.0 / Math.sqrt(n2); + for (int i = 0; i < m; ++i) { + eigenvector[i] *= inv; + } + + } + + /** + * Check if decomposition has been performed. + * @exception IllegalStateException if {@link #decompose(RealMatrix) decompose} + * has not been called + */ + private void checkDecomposed() + throws IllegalStateException { + if (eigenvalues == null) { + throw new IllegalStateException("no matrix have been decomposed yet"); + } + } + + /** + * Check if decomposed matrix is non singular. + * @exception IllegalStateException if {@link #decompose(RealMatrix) decompose} + * has not been called + * @exception InvalidMatrixException if decomposed matrix is singular + */ + private void checkNonSingular() + throws IllegalStateException, InvalidMatrixException { + checkDecomposed(); + if (!isNonSingular()) { + throw new IllegalStateException("matrix is singular"); + } + } + +} diff --git a/src/java/org/apache/commons/math/linear/GershgorinCirclesUnion.java b/src/java/org/apache/commons/math/linear/GershgorinCirclesUnion.java new file mode 100644 index 000000000..2f88aa89e --- /dev/null +++ b/src/java/org/apache/commons/math/linear/GershgorinCirclesUnion.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.math.linear; + +/** Class representing a union of Gershgorin circles. + *

    Gershgorin circles are bounding areas where eigenvalues must lie. + * They are used as starting values for eigen decomposition algorithms. + * In the real case, Gershgorin circles are simple intervals.

    + * @see EigenDecompositionImpl + * @version $Revision$ $Date$ + * @since 2.0 + */ +class GershgorinCirclesUnion implements Comparable { + + /** Lower bound of the interval. */ + private double low; + + /** Higher bound of the interval. */ + private double high; + + /** Create a simple Gershgorin circle. + * @param d diagonal element of the current row + * @param sum sum of the absolute values of the off-diagonal elements + * of the current row + */ + public GershgorinCirclesUnion(final double d, final double sum) { + low = d - sum; + high = d + sum; + } + + /** + * Get the lower bound of the interval. + * @return lower bound of the interval + */ + public double getLow() { + return low; + } + + /** + * Get the higher bound of the interval. + * @return higher bound of the interval + */ + public double getHigh() { + return high; + } + + /** + * Check if a Gershgorin circles union intersects instance. + * @param other Gershgorin circles union to test against instance + * @return true if the other Gershgorin circles union intersects instance + */ + public boolean intersects(final GershgorinCirclesUnion other) { + return (other.low <= this.high) && (other.high >= this.low); + } + + /** + * Swallow another Gershgorin circles union. + *

    Swallowing another Gershgorin circles union changes the + * instance such that it contains everything that was formerly in + * either circles union. It is mainly intended for circles unions + * that {@link #intersects(GershgorinCirclesUnion) intersect} + * each other beforehand.

    + */ + public void swallow(final GershgorinCirclesUnion other) { + low = Math.min(low, other.low); + high = Math.max(high, other.high); + } + + /** Comparator class for sorting intervals. */ + public int compareTo(GershgorinCirclesUnion other) { + return Double.compare(low, other.low); + } + +} diff --git a/src/test/org/apache/commons/math/linear/EigenDecompositionImplTest.java b/src/test/org/apache/commons/math/linear/EigenDecompositionImplTest.java new file mode 100644 index 000000000..d13e132a8 --- /dev/null +++ b/src/test/org/apache/commons/math/linear/EigenDecompositionImplTest.java @@ -0,0 +1,218 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.math.linear; + +import java.util.Random; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +public class EigenDecompositionImplTest extends TestCase { + + private double[] refValues; + private RealMatrix matrix; + + private static final double normTolerance = 1.e-10; + + public EigenDecompositionImplTest(String name) { + super(name); + } + + public static Test suite() { + TestSuite suite = new TestSuite(EigenDecompositionImplTest.class); + suite.setName("EigenDecompositionImpl Tests"); + return suite; + } + + /** test dimensions */ + public void testDimensions() { + final int m = matrix.getRowDimension(); + EigenDecomposition ed = new EigenDecompositionImpl(matrix); + assertEquals(m, ed.getV().getRowDimension()); + assertEquals(m, ed.getV().getColumnDimension()); + assertEquals(m, ed.getD().getColumnDimension()); + assertEquals(m, ed.getD().getColumnDimension()); + assertEquals(m, ed.getVT().getRowDimension()); + assertEquals(m, ed.getVT().getColumnDimension()); + } + + /** test eigenvalues */ + public void testEigenvalues() { + EigenDecomposition ed = new EigenDecompositionImpl(matrix); + double[] eigenValues = ed.getEigenvalues(); + assertEquals(refValues.length, eigenValues.length); + for (int i = 0; i < refValues.length; ++i) { + assertEquals(refValues[i], eigenValues[eigenValues.length - 1 - i], 3.0e-15); + } + } + + /** test eigenvectors */ + public void testEigenvectors() { + EigenDecomposition ed = new EigenDecompositionImpl(matrix); + for (int i = 0; i < matrix.getRowDimension(); ++i) { + double lambda = ed.getEigenvalue(i); + RealVector v = ed.getEigenvector(i); + RealVector mV = matrix.operate(v); + assertEquals(0, mV.subtract(v.mapMultiplyToSelf(lambda)).getNorm(), 1.0e-13); + } + } + + /** test A = VDVt */ + public void testAEqualVDVt() { + EigenDecomposition ed = new EigenDecompositionImpl(matrix); + RealMatrix v = ed.getV(); + RealMatrix d = ed.getD(); + RealMatrix vT = ed.getVT(); + double norm = v.multiply(d).multiply(vT).subtract(matrix).getNorm(); + assertEquals(0, norm, normTolerance); + } + + /** test that V is orthogonal */ + public void testVOrthogonal() { + RealMatrix v = new EigenDecompositionImpl(matrix).getV(); + RealMatrix vTv = v.transpose().multiply(v); + RealMatrix id = MatrixUtils.createRealIdentityMatrix(vTv.getRowDimension()); + assertEquals(0, vTv.subtract(id).getNorm(), normTolerance); + } + + /** test solve dimension errors */ + public void testSolveDimensionErrors() { + EigenDecomposition ed = new EigenDecompositionImpl(matrix); + RealMatrix b = new RealMatrixImpl(new double[2][2]); + try { + ed.solve(b); + fail("an exception should have been thrown"); + } catch (IllegalArgumentException iae) { + // expected behavior + } catch (Exception e) { + fail("wrong exception caught"); + } + try { + ed.solve(b.getColumn(0)); + fail("an exception should have been thrown"); + } catch (IllegalArgumentException iae) { + // expected behavior + } catch (Exception e) { + fail("wrong exception caught"); + } + try { + ed.solve(new RealVectorImplTest.RealVectorTestImpl(b.getColumn(0))); + fail("an exception should have been thrown"); + } catch (IllegalArgumentException iae) { + // expected behavior + } catch (Exception e) { + fail("wrong exception caught"); + } + } + + /** test solve */ + public void testSolve() { + RealMatrix m = new RealMatrixImpl(new double[][] { + { 91, 5, 29, 32, 40, 14 }, + { 5, 34, -1, 0, 2, -1 }, + { 29, -1, 12, 9, 21, 8 }, + { 32, 0, 9, 14, 9, 0 }, + { 40, 2, 21, 9, 51, 19 }, + { 14, -1, 8, 0, 19, 14 } + }); + EigenDecomposition ed = new EigenDecompositionImpl(m); + assertEquals(184041, ed.getDeterminant(), 2.0e-8); + RealMatrix b = new RealMatrixImpl(new double[][] { + { 1561, 269, 188 }, + { 69, -21, 70 }, + { 739, 108, 63 }, + { 324, 86, 59 }, + { 1624, 194, 107 }, + { 796, 69, 36 } + }); + RealMatrix xRef = new RealMatrixImpl(new double[][] { + { 1, 2, 1 }, + { 2, -1, 2 }, + { 4, 2, 3 }, + { 8, -1, 0 }, + { 16, 2, 0 }, + { 32, -1, 0 } + }); + + // using RealMatrix + assertEquals(0, ed.solve(b).subtract(xRef).getNorm(), normTolerance); + + // using double[] + for (int i = 0; i < b.getColumnDimension(); ++i) { + assertEquals(0, + new RealVectorImpl(ed.solve(b.getColumn(i))).subtract(xRef.getColumnVector(i)).getNorm(), + 2.0e-11); + } + + // using RealMatrixImpl + for (int i = 0; i < b.getColumnDimension(); ++i) { + assertEquals(0, + ed.solve(b.getColumnVector(i)).subtract(xRef.getColumnVector(i)).getNorm(), + 2.0e-11); + } + + // using RealMatrix with an alternate implementation + for (int i = 0; i < b.getColumnDimension(); ++i) { + RealVectorImplTest.RealVectorTestImpl v = + new RealVectorImplTest.RealVectorTestImpl(b.getColumn(i)); + assertEquals(0, + ed.solve(v).subtract(xRef.getColumnVector(i)).getNorm(), + 2.0e-11); + } + + } + + public void setUp() { + refValues = new double[] { + 2.003, 2.002, 2.001, 1.001, 1.000, 0.001 + }; + matrix = createTestMatrix(new Random(35992629946426l), refValues); + } + + public void tearDown() { + refValues = null; + matrix = null; + } + + private RealMatrix createTestMatrix(final Random r, final double[] eigenValues) { + final RealMatrix v = createOrthogonalMatrix(r, eigenValues.length); + final RealMatrix d = createDiagonalMatrix(eigenValues, eigenValues.length); + return v.multiply(d).multiply(v.transpose()); + } + + private RealMatrix createOrthogonalMatrix(final Random r, final int size) { + final double[][] data = new double[size][size]; + for (int i = 0; i < size; ++i) { + for (int j = 0; j < size; ++j) { + data[i][j] = 2 * r.nextDouble() - 1; + } + } + final RealMatrix m = new RealMatrixImpl(data, false); + return new QRDecompositionImpl(m).getQ(); + } + + private RealMatrix createDiagonalMatrix(final double[] data, final int rows) { + final double[][] dData = new double[rows][rows]; + for (int i = 0; i < data.length; ++i) { + dData[i][i] = data[i]; + } + return new RealMatrixImpl(dData, false); + } + +}