From ec77f54bef7e60de920307bd75ce44a2fa104670 Mon Sep 17 00:00:00 2001 From: Gilles Sadowski Date: Sun, 27 Oct 2019 15:10:11 +0100 Subject: [PATCH] MATH-1500: LU decomposition for a matrix whose entries are elements of a field. --- .../linalg/FieldDecompositionSolver.java | 59 +++ .../field/linalg/FieldLUDecomposition.java | 361 ++++++++++++++++++ .../linalg/FieldLUDecompositionTest.java | 267 +++++++++++++ 3 files changed, 687 insertions(+) create mode 100644 src/main/java/org/apache/commons/math4/field/linalg/FieldDecompositionSolver.java create mode 100644 src/main/java/org/apache/commons/math4/field/linalg/FieldLUDecomposition.java create mode 100644 src/test/java/org/apache/commons/math4/field/linalg/FieldLUDecompositionTest.java diff --git a/src/main/java/org/apache/commons/math4/field/linalg/FieldDecompositionSolver.java b/src/main/java/org/apache/commons/math4/field/linalg/FieldDecompositionSolver.java new file mode 100644 index 000000000..0136062ca --- /dev/null +++ b/src/main/java/org/apache/commons/math4/field/linalg/FieldDecompositionSolver.java @@ -0,0 +1,59 @@ +/* + * 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.math4.field.linalg; + +import java.util.List; + +/** + * Interface handling decomposition algorithms that can solve {@code A X = B}. + * + *

Decomposition algorithms decompose an A matrix has a product of several specific + * matrices from which they can solve the above system of equations in a least-squares + * sense: Find X such that {@code ||A X - B||} is minimal.

+ * + *

Some solvers like {@link FieldLUDecomposition} can only find the solution for + * square matrices and when the solution is an exact linear solution, i.e. when + * {@code ||A X - B||} is exactly 0. + * Other solvers can also find solutions with non-square matrix {@code A} and with + * non-zero minimal norm. + * If an exact linear solution exists it is also the minimal norm solution.

+ * + * @param Type of the field elements. + * + * @since 4.0 + */ +public interface FieldDecompositionSolver { + /** + * Solves the linear equation {@code A X = B}. + * + *

Matrix {@code A} is implicit: It is provided by the underlying + * decomposition algorithm.

+ * + * @param b Right-hand side of the equation. + * @return the matrix {@code X} that minimizes {@code ||A X - B||}. + * @throws IllegalArgumentException if the dimensions do not match. + */ + FieldDenseMatrix solve(final FieldDenseMatrix b); + + /** + * Computes the inverse of a decomposed (square) matrix. + * + * @return the inverse matrix. + */ + FieldDenseMatrix getInverse(); +} diff --git a/src/main/java/org/apache/commons/math4/field/linalg/FieldLUDecomposition.java b/src/main/java/org/apache/commons/math4/field/linalg/FieldLUDecomposition.java new file mode 100644 index 000000000..ab53e291d --- /dev/null +++ b/src/main/java/org/apache/commons/math4/field/linalg/FieldLUDecomposition.java @@ -0,0 +1,361 @@ +/* + * 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.math4.field.linalg; + +import java.util.List; +import java.util.ArrayList; +import org.apache.commons.numbers.field.Field; +import org.apache.commons.math4.linear.SingularMatrixException; +import org.apache.commons.math4.exception.DimensionMismatchException; + +/** + * Calculates the LUP-decomposition of a square matrix. + * + *

The LUP-decomposition of a matrix A consists of three matrices + * L, U and P that satisfy: PA = LU, L is lower triangular, and U is + * upper triangular and P is a permutation matrix. All matrices are + * m×m.

+ * + *

Since {@link Field field} elements do not provide an ordering + * operator, the permutation matrix is computed here only in order to + * avoid a zero pivot element, no attempt is done to get the largest + * pivot element.

+ * + * @param Type of the field elements. + * + * @see MathWorld + * @see Wikipedia + * + * @since 4.0 + */ +public class FieldLUDecomposition { + /** Field to which the elements belong. */ + private final Field field; + /** Entries of LU decomposition. */ + private final FieldDenseMatrix mLU; + /** Pivot permutation associated with LU decomposition. */ + private final int[] pivot; + /** Singularity indicator. */ + private final boolean isSingular; + /** Parity of the permutation associated with the LU decomposition. */ + private final boolean isEven; + + /** + * Calculates the LU-decomposition of the given {@code matrix}. + * + * @param matrix Matrix to decompose. + * @throws IllegalArgumentException if the matrix is not square. + */ + private FieldLUDecomposition(FieldDenseMatrix matrix) { + matrix.checkMultiply(matrix); + + field = matrix.getField(); + final int m = matrix.getRowDimension(); + pivot = new int[m]; + + // Initialize permutation array and parity. + for (int row = 0; row < m; row++) { + pivot[row] = row; + } + mLU = matrix.copy(); + + boolean even = true; + boolean singular = false; + // Loop over columns. + for (int col = 0; col < m; col++) { + T sum = field.zero(); + + // Upper. + for (int row = 0; row < col; row++) { + sum = mLU.get(row, col); + for (int i = 0; i < row; i++) { + sum = field.subtract(sum, + field.multiply(mLU.get(row, i), + mLU.get(i, col))); + } + mLU.set(row, col, sum); + } + + // Lower. + int nonZero = col; // Permutation row. + for (int row = col; row < m; row++) { + sum = mLU.get(row, col); + for (int i = 0; i < col; i++) { + sum = field.subtract(sum, + field.multiply(mLU.get(row, i), + mLU.get(i, col))); + } + mLU.set(row, col, sum); + + if (mLU.get(nonZero, col).equals(field.zero())) { + // try to select a better permutation choice + ++nonZero; + } + } + + // Singularity check. + if (nonZero >= m) { + singular = true; + } else { + // Pivot if necessary. + if (nonZero != col) { + T tmp = field.zero(); + for (int i = 0; i < m; i++) { + tmp = mLU.get(nonZero, i); + mLU.set(nonZero, i, mLU.get(col, i)); + mLU.set(col, i, tmp); + } + int temp = pivot[nonZero]; + pivot[nonZero] = pivot[col]; + pivot[col] = temp; + even = !even; + } + + // Divide the lower elements by the "winning" diagonal element. + final T luDiag = mLU.get(col, col); + for (int row = col + 1; row < m; row++) { + mLU.set(row, col, field.divide(mLU.get(row, col), + luDiag)); + } + } + } + + isSingular = singular; + isEven = even; + } + + /** + * Factory method. + * + * @param m Matrix to decompose. + * @return a new instance. + */ + public static FieldLUDecomposition of(FieldDenseMatrix m) { + return new FieldLUDecomposition<>(m); + } + + /** + * @return {@code true} if the matrix is singular. + */ + public boolean isSingular() { + return isSingular; + } + + /** + * Builds the "L" matrix of the decomposition. + * + * @return the lower triangular matrix. + * @throws SingularMatrixException if the matrix is singular. + */ + public FieldDenseMatrix getL() { + if (isSingular) { + throw new SingularMatrixException(); + } + + final int m = pivot.length; + final FieldDenseMatrix mL = FieldDenseMatrix.zero(field, m, m); + for (int i = 0; i < m; i++) { + for (int j = 0; j < i; j++) { + mL.set(i, j, mLU.get(i, j)); + } + mL.set(i, i, field.one()); + } + + return mL; + } + + /** + * Builds the "U" matrix of the decomposition. + * + * @return the upper triangular matrix. + * @throws SingularMatrixException if the matrix is singular. + */ + public FieldDenseMatrix getU() { + if (isSingular) { + throw new SingularMatrixException(); + } + + final int m = pivot.length; + final FieldDenseMatrix mU = FieldDenseMatrix.zero(field, m, m); + for (int i = 0; i < m; i++) { + for (int j = i; j < m; j++) { + mU.set(i, j, mLU.get(i, j)); + } + } + + return mU; + } + + /** + * Builds the "P" matrix. + * + *

P is a matrix with exactly one element set to {@link Field#one() one} in + * each row and each column, all other elements being set to {@link Field#zero() zero}. + * The positions of the "one" elements are given by the {@link #getPivot() + * pivot permutation vector}.

+ * @return the "P" rows permutation matrix. + * @throws SingularMatrixException if the matrix is singular. + * + * @see #getPivot() + */ + public FieldDenseMatrix getP() { + if (isSingular) { + throw new SingularMatrixException(); + } + + final int m = pivot.length; + final FieldDenseMatrix mP = FieldDenseMatrix.zero(field, m, m); + + for (int i = 0; i < m; i++) { + mP.set(i, pivot[i], field.one()); + } + + return mP; + } + + /** + * Gets the pivot permutation vector. + * + * @return the pivot permutation vector. + * + * @see #getP() + */ + public int[] getPivot() { + return pivot.clone(); + } + + /** + * Return the determinant of the matrix. + * @return determinant of the matrix + */ + public T getDeterminant() { + if (isSingular) { + return field.zero(); + } else { + final int m = pivot.length; + T determinant = isEven ? + field.one() : + field.negate(field.one()); + + for (int i = 0; i < m; i++) { + determinant = field.multiply(determinant, + mLU.get(i, i)); + } + + return determinant; + } + } + + /** + * Creates a solver for finding the solution {@code X} of the linear + * system of equations {@code A X = B}. + * + * @return a solver. + * @throws SingularMatrixException if the matrix is singular. + */ + public FieldDecompositionSolver getSolver() { + if (isSingular) { + throw new SingularMatrixException(); + } + + return new Solver<>(mLU, pivot); + } + + /** + * Specialized solver. + * + * @param Type of the field elements. + */ + private static class Solver implements FieldDecompositionSolver { + /** Field to which the elements belong. */ + private final Field field; + /** LU decomposition. */ + private final FieldDenseMatrix mLU; + /** Pivot permutation associated with LU decomposition. */ + private final int[] pivot; + + /** + * Builds a solver from a LU-decomposed matrix. + * + * @param mLU LU matrix. + * @param pivot Pivot permutation associated with the decomposition. + */ + private Solver(final FieldDenseMatrix mLU, + final int[] pivot) { + field = mLU.getField(); + this.mLU = mLU.copy(); + this.pivot = pivot.clone(); + } + + /** {@inheritDoc} */ + @Override + public FieldDenseMatrix solve(final FieldDenseMatrix b) { + mLU.checkMultiply(b); + + final FieldDenseMatrix bp = b.copy(); + final int nColB = b.getColumnDimension(); + final int m = pivot.length; + + // Apply permutations. + for (int row = 0; row < m; row++) { + final int pRow = pivot[row]; + for (int col = 0; col < nColB; col++) { + bp.set(row, col, + b.get(row, col)); + } + } + + // Solve LY = b + for (int col = 0; col < m; col++) { + for (int i = col + 1; i < m; i++) { + for (int j = 0; j < nColB; j++) { + bp.set(i, j, + field.subtract(bp.get(i, j), + field.multiply(bp.get(col, j), + mLU.get(i, col)))); + } + } + } + + // Solve UX = Y + for (int col = m - 1; col >= 0; col--) { + for (int j = 0; j < nColB; j++) { + bp.set(col, j, + field.divide(bp.get(col, j), + mLU.get(col, col))); + } + for (int i = 0; i < col; i++) { + for (int j = 0; j < nColB; j++) { + bp.set(i, j, + field.subtract(bp.get(i, j), + field.multiply(bp.get(col, j), + mLU.get(i, col)))); + } + } + } + + return bp; + } + + /** {@inheritDoc} */ + @Override + public FieldDenseMatrix getInverse() { + return solve(FieldDenseMatrix.identity(field, pivot.length)); + } + } +} diff --git a/src/test/java/org/apache/commons/math4/field/linalg/FieldLUDecompositionTest.java b/src/test/java/org/apache/commons/math4/field/linalg/FieldLUDecompositionTest.java new file mode 100644 index 000000000..b75664332 --- /dev/null +++ b/src/test/java/org/apache/commons/math4/field/linalg/FieldLUDecompositionTest.java @@ -0,0 +1,267 @@ +/* + * 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.math4.field.linalg; + +import org.apache.commons.numbers.fraction.Fraction; +import org.apache.commons.numbers.field.FractionField; +import org.junit.Test; +import org.junit.Assert; +import org.apache.commons.math4.linear.NonSquareMatrixException; +import org.apache.commons.math4.linear.SingularMatrixException; + +public class FieldLUDecompositionTest { + private final Fraction[][] testData = { + { Fraction.of(1), Fraction.of(2), Fraction.of(3)}, + { Fraction.of(2), Fraction.of(5), Fraction.of(3)}, + { Fraction.of(1), Fraction.of(0), Fraction.of(8)} + }; + private final Fraction[][] testDataMinus = { + { Fraction.of(-1), Fraction.of(-2), Fraction.of(-3)}, + { Fraction.of(-2), Fraction.of(-5), Fraction.of(-3)}, + { Fraction.of(-1), Fraction.of(0), Fraction.of(-8)} + }; + private final Fraction[][] luData = { + { Fraction.of(2), Fraction.of(3), Fraction.of(3) }, + { Fraction.of(2), Fraction.of(3), Fraction.of(7) }, + { Fraction.of(6), Fraction.of(6), Fraction.of(8) } + }; + + // singular matrices + private final Fraction[][] singular = { + { Fraction.of(2), Fraction.of(3) }, + { Fraction.of(2), Fraction.of(3) } + }; + private final Fraction[][] bigSingular = { + { Fraction.of(1), Fraction.of(2), Fraction.of(3), Fraction.of(4) }, + { Fraction.of(2), Fraction.of(5), Fraction.of(3), Fraction.of(4) }, + { Fraction.of(7), Fraction.of(3), Fraction.of(256), Fraction.of(1930) }, + { Fraction.of(3), Fraction.of(7), Fraction.of(6), Fraction.of(8) } + }; // 4th row = 1st + 2nd + + /** + * @param data Matrix. + * @return a {@link FieldDenseMatrix} instance. + */ + private FieldDenseMatrix create(Fraction[][] data) { + final FieldDenseMatrix m = FieldDenseMatrix.create(FractionField.get(), + data.length, + data[0].length); + for (int i = 0; i < data.length; i++) { + for (int j = 0; j < data.length; j++) { + m.set(i, j, data[i][j]); + } + } + + return m; + } + + /** test dimensions */ + @Test + public void testDimensions() { + FieldDenseMatrix matrix = create(testData); + FieldLUDecomposition LU = FieldLUDecomposition.of(matrix); + Assert.assertEquals(testData.length, LU.getL().getRowDimension()); + Assert.assertEquals(testData.length, LU.getU().getRowDimension()); + Assert.assertEquals(testData.length, LU.getP().getRowDimension()); + } + + /** test PA = LU */ + @Test + public void testPAEqualLU() { + FieldDenseMatrix matrix = create(testData); + FieldLUDecomposition lu = FieldLUDecomposition.of(matrix); + FieldDenseMatrix l = lu.getL(); + FieldDenseMatrix u = lu.getU(); + FieldDenseMatrix p = lu.getP(); + Assert.assertEquals(p.multiply(matrix), l.multiply(u)); + + matrix = create(testDataMinus); + lu = FieldLUDecomposition.of(matrix); + l = lu.getL(); + u = lu.getU(); + p = lu.getP(); + Assert.assertEquals(p.multiply(matrix), l.multiply(u)); + + matrix = FieldDenseMatrix.identity(FractionField.get(), 17); + lu = FieldLUDecomposition.of(matrix); + l = lu.getL(); + u = lu.getU(); + p = lu.getP(); + Assert.assertEquals(p.multiply(matrix), l.multiply(u)); + } + + /** test that L is lower triangular with unit diagonal */ + @Test + public void testLLowerTriangular() { + FieldDenseMatrix matrix = create(testData); + FieldDenseMatrix l = FieldLUDecomposition.of(matrix).getL(); + for (int i = 0; i < l.getRowDimension(); i++) { + Assert.assertEquals(Fraction.ONE, l.get(i, i)); + for (int j = i + 1; j < l.getColumnDimension(); j++) { + Assert.assertEquals(Fraction.ZERO, l.get(i, j)); + } + } + } + + /** test that U is upper triangular */ + @Test + public void testUUpperTriangular() { + FieldDenseMatrix matrix = create(testData); + FieldDenseMatrix u = FieldLUDecomposition.of(matrix).getU(); + for (int i = 0; i < u.getRowDimension(); i++) { + for (int j = 0; j < i; j++) { + Assert.assertEquals(Fraction.ZERO, u.get(i, j)); + } + } + } + + /** test that P is a permutation matrix */ + @Test + public void testPPermutation() { + FieldDenseMatrix matrix = create(testData); + FieldDenseMatrix p = FieldLUDecomposition.of(matrix).getP(); + + FieldDenseMatrix ppT = p.multiply(p.transpose()); + FieldDenseMatrix id = FieldDenseMatrix.identity(FractionField.get(), + p.getRowDimension()); + Assert.assertEquals(id, ppT); + + for (int i = 0; i < p.getRowDimension(); i++) { + int zeroCount = 0; + int oneCount = 0; + int otherCount = 0; + for (int j = 0; j < p.getColumnDimension(); j++) { + final Fraction e = p.get(i, j); + if (e.equals(Fraction.ZERO)) { + ++zeroCount; + } else if (e.equals(Fraction.ONE)) { + ++oneCount; + } else { + ++otherCount; + } + } + Assert.assertEquals(p.getRowDimension() - 1, zeroCount); + Assert.assertEquals(1, oneCount); + Assert.assertEquals(0, otherCount); + } + + for (int j = 0; j < p.getRowDimension(); j++) { + int zeroCount = 0; + int oneCount = 0; + int otherCount = 0; + for (int i = 0; i < p.getColumnDimension(); i++) { + final Fraction e = p.get(i, j); + if (e.equals(Fraction.ZERO)) { + ++zeroCount; + } else if (e.equals(Fraction.ONE)) { + ++oneCount; + } else { + ++otherCount; + } + } + Assert.assertEquals(p.getRowDimension() - 1, zeroCount); + Assert.assertEquals(1, oneCount); + Assert.assertEquals(0, otherCount); + } + } + + @Test + public void testIsSingular1() { + FieldLUDecomposition lu = FieldLUDecomposition.of(create(testData)); + Assert.assertFalse(lu.isSingular()); + lu.getSolver(); + } + @Test(expected=SingularMatrixException.class) + public void testIsSingular2() { + FieldLUDecomposition lu = FieldLUDecomposition.of(create(singular)); + Assert.assertTrue(lu.isSingular()); + lu.getSolver(); + } + @Test(expected=SingularMatrixException.class) + public void testIsSingular3() { + FieldLUDecomposition lu = FieldLUDecomposition.of(create(bigSingular)); + Assert.assertTrue(lu.isSingular()); + lu.getSolver(); + } + + @Test + public void testMatricesValues1() { + FieldLUDecomposition lu = FieldLUDecomposition.of(create(testData)); + FieldDenseMatrix lRef = create(new Fraction[][] { + { Fraction.of(1), Fraction.of(0), Fraction.of(0) }, + { Fraction.of(2), Fraction.of(1), Fraction.of(0) }, + { Fraction.of(1), Fraction.of(-2), Fraction.of(1) } + }); + FieldDenseMatrix uRef = create(new Fraction[][] { + { Fraction.of(1), Fraction.of(2), Fraction.of(3) }, + { Fraction.of(0), Fraction.of(1), Fraction.of(-3) }, + { Fraction.of(0), Fraction.of(0), Fraction.of(-1) } + }); + FieldDenseMatrix pRef = create(new Fraction[][] { + { Fraction.of(1), Fraction.of(0), Fraction.of(0) }, + { Fraction.of(0), Fraction.of(1), Fraction.of(0) }, + { Fraction.of(0), Fraction.of(0), Fraction.of(1) } + }); + int[] pivotRef = { 0, 1, 2 }; + + // check values against known references + FieldDenseMatrix l = lu.getL(); + Assert.assertEquals(lRef, l); + FieldDenseMatrix u = lu.getU(); + Assert.assertEquals(uRef, u); + FieldDenseMatrix p = lu.getP(); + Assert.assertEquals(pRef, p); + int[] pivot = lu.getPivot(); + for (int i = 0; i < pivotRef.length; ++i) { + Assert.assertEquals(pivotRef[i], pivot[i]); + } + } + + @Test + public void testMatricesValues2() { + FieldLUDecomposition lu = FieldLUDecomposition.of(create(luData)); + FieldDenseMatrix lRef = create(new Fraction[][] { + { Fraction.of(1), Fraction.of(0), Fraction.of(0) }, + { Fraction.of(3), Fraction.of(1), Fraction.of(0) }, + { Fraction.of(1), Fraction.of(0), Fraction.of(1) } + }); + FieldDenseMatrix uRef = create(new Fraction[][] { + { Fraction.of(2), Fraction.of(3), Fraction.of(3) }, + { Fraction.of(0), Fraction.of(-3), Fraction.of(-1) }, + { Fraction.of(0), Fraction.of(0), Fraction.of(4) } + }); + FieldDenseMatrix pRef = create(new Fraction[][] { + { Fraction.of(1), Fraction.of(0), Fraction.of(0) }, + { Fraction.of(0), Fraction.of(0), Fraction.of(1) }, + { Fraction.of(0), Fraction.of(1), Fraction.of(0) } + }); + int[] pivotRef = { 0, 2, 1 }; + + // check values against known references + FieldDenseMatrix l = lu.getL(); + Assert.assertEquals(lRef, l); + FieldDenseMatrix u = lu.getU(); + Assert.assertEquals(uRef, u); + FieldDenseMatrix p = lu.getP(); + Assert.assertEquals(pRef, p); + int[] pivot = lu.getPivot(); + for (int i = 0; i < pivotRef.length; ++i) { + Assert.assertEquals(pivotRef[i], pivot[i]); + } + } +}