diff --git a/kmath-commons/src/main/kotlin/scientifik/kmath/commons/CommonsMatrix.kt b/kmath-commons/src/main/kotlin/scientifik/kmath/commons/CommonsMatrix.kt deleted file mode 100644 index a4a9a67ae..000000000 --- a/kmath-commons/src/main/kotlin/scientifik/kmath/commons/CommonsMatrix.kt +++ /dev/null @@ -1,3 +0,0 @@ -package scientifik.kmath.commons - -//val solver: DecompositionSolver \ No newline at end of file diff --git a/kmath-core/src/commonMain/kotlin/scientifik/kmath/linear/LUDecomposition.kt b/kmath-core/src/commonMain/kotlin/scientifik/kmath/linear/LUDecomposition.kt index 6d329809f..e51d574f8 100644 --- a/kmath-core/src/commonMain/kotlin/scientifik/kmath/linear/LUDecomposition.kt +++ b/kmath-core/src/commonMain/kotlin/scientifik/kmath/linear/LUDecomposition.kt @@ -1,44 +1,12 @@ package scientifik.kmath.linear -import scientifik.kmath.operations.Field import scientifik.kmath.structures.MutableNDArray import scientifik.kmath.structures.NDArray import scientifik.kmath.structures.NDArrays +import kotlin.math.absoluteValue /** - * 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 (with unit - * diagonal terms), U is upper triangular and P is a permutation matrix. All - * matrices are mm. - * - * As shown by the presence of the P matrix, this decomposition is - * implemented using partial pivoting. - * - * This class is based on the class with similar name from the - * [JAMA](http://math.nist.gov/javanumerics/jama/) library. - * - * * a [getP][.getP] method has been added, - * * the `det` method has been renamed as [ getDeterminant][.getDeterminant], - * * the `getDoublePivot` method has been removed (but the int based - * [getPivot][.getPivot] method has been kept), - * * the `solve` and `isNonSingular` methods have been replaced - * by a [getSolver][.getSolver] method and the equivalent methods - * provided by the returned [DecompositionSolver]. - * - * - * @see [MathWorld](http://mathworld.wolfram.com/LUDecomposition.html) - * - * @see [Wikipedia](http://en.wikipedia.org/wiki/LU_decomposition) - * - * @since 2.0 (changed to concrete class in 3.0) - * - * @param matrix The matrix to decompose. - * @param singularityThreshold threshold (based on partial row norm) - * under which a matrix is considered singular - * @throws NonSquareMatrixException if matrix is not square - + * Implementation copier from Apache common-maths */ abstract class LUDecomposition>(val matrix: Matrix) { @@ -51,7 +19,7 @@ abstract class LUDecomposition>(val matrix: Matrix) { private var even: Boolean = false init { - val pair = matrix.context.field.calculateLU() + val pair = calculateLU() lu = pair.first pivot = pair.second } @@ -134,72 +102,76 @@ abstract class LUDecomposition>(val matrix: Matrix) { abstract fun isSingular(value: T): Boolean - private fun Field.calculateLU(): Pair, IntArray> { + private fun abs(value: T) = if (value > matrix.context.field.zero) value else with(matrix.context.field) { -value } + + private fun calculateLU(): Pair, IntArray> { if (matrix.rows != matrix.columns) { error("LU decomposition supports only square matrices") } - fun T.abs() = if (this > zero) this else -this - val m = matrix.columns val pivot = IntArray(matrix.rows) //TODO fix performance val lu: MutableNDArray = NDArrays.createMutable(matrix.context.field, listOf(matrix.rows, matrix.columns)) { index -> matrix[index[0], index[1]] } - // Initialize permutation array and parity - for (row in 0 until m) { - pivot[row] = row - } - even = true - // Loop over columns - for (col in 0 until m) { - - // upper - for (row in 0 until col) { - var sum = lu[row, col] - for (i in 0 until row) { - sum -= lu[row, i] * lu[i, col] - } - lu[row, col] = sum + with(matrix.context.field) { + // Initialize permutation array and parity + for (row in 0 until m) { + pivot[row] = row } + even = true - // lower - val max = (col until m).maxBy { row -> - var sum = lu[row, col] - for (i in 0 until col) { - sum -= lu[row, i] * lu[i, col] + // Loop over columns + for (col in 0 until m) { + + // upper + for (row in 0 until col) { + var sum = lu[row, col] + for (i in 0 until row) { + sum -= lu[row, i] * lu[i, col] + } + lu[row, col] = sum } - //luRow[col] = sum - lu[row, col] = sum - sum.abs() - } ?: col + // lower + val max = (col until m).maxBy { row -> + var sum = lu[row, col] + for (i in 0 until col) { + sum -= lu[row, i] * lu[i, col] + } + //luRow[col] = sum + lu[row, col] = sum - // Singularity check - if (isSingular(lu[max, col].abs())) { - error("Singular matrix") - } + abs(sum) + } ?: col - // Pivot if necessary - if (max != col) { - //var tmp = zero - //val luMax = lu[max] - //val luCol = lu[col] - for (i in 0 until m) { - lu[max, i] = lu[col, i] - lu[col, i] = lu[max, i] + // Singularity check + if (isSingular(lu[max, col])) { + error("Singular matrix") } - val temp = pivot[max] - pivot[max] = pivot[col] - pivot[col] = temp - even = !even - } - // Divide the lower elements by the "winning" diagonal elt. - val luDiag = lu[col, col] - for (row in col + 1 until m) { - lu[row, col] /= luDiag + // Pivot if necessary + if (max != col) { + //var tmp = zero + //val luMax = lu[max] + //val luCol = lu[col] + for (i in 0 until m) { + lu[max, i] = lu[col, i] + lu[col, i] = lu[max, i] + } + val temp = pivot[max] + pivot[max] = pivot[col] + pivot[col] = temp + even = !even + } + + // Divide the lower elements by the "winning" diagonal elt. + val luDiag = lu[col, col] + for (row in col + 1 until m) { + lu[row, col] = lu[row, col] / luDiag +// lu[row, col] /= luDiag + } } } return Pair(lu, pivot) @@ -214,21 +186,22 @@ abstract class LUDecomposition>(val matrix: Matrix) { return pivot.copyOf() } +} + +class RealLUDecomposition(matrix: Matrix, private val singularityThreshold: Double = DEFAULT_TOO_SMALL) : LUDecomposition(matrix) { + override fun isSingular(value: Double): Boolean { + return value.absoluteValue < singularityThreshold + } + companion object { /** Default bound to determine effective singularity in LU decomposition. */ private const val DEFAULT_TOO_SMALL = 1e-11 } } -class RealLUDecomposition(matrix: Matrix, private val singularityThreshold: Double = 1e-11) : LUDecomposition(matrix) { - override fun isSingular(value: Double): Boolean { - return value < singularityThreshold - } -} - /** Specialized solver. */ -class RealLUSolver : LinearSolver { +object RealLUSolver : LinearSolver { // // /** {@inheritDoc} */ diff --git a/kmath-core/src/commonMain/kotlin/scientifik/kmath/linear/LinearAlgrebra.kt b/kmath-core/src/commonMain/kotlin/scientifik/kmath/linear/LinearAlgrebra.kt index 07295e0aa..e7345163f 100644 --- a/kmath-core/src/commonMain/kotlin/scientifik/kmath/linear/LinearAlgrebra.kt +++ b/kmath-core/src/commonMain/kotlin/scientifik/kmath/linear/LinearAlgrebra.kt @@ -14,31 +14,31 @@ import scientifik.kmath.structures.realNDFieldFactory * @param T type of individual element of the vector or matrix * @param V the type of vector space element */ -abstract class LinearSpace>(val rows: Int, val columns: Int, val field: Field) : Space { +abstract class MatrixSpace(val rows: Int, val columns: Int, val field: Field) : Space> { /** * Produce the element of this space */ - abstract fun produce(initializer: (Int, Int) -> T): V + abstract fun produce(initializer: (Int, Int) -> T): Matrix /** - * Produce new linear space with given dimensions. The space produced could be raised from cache since [LinearSpace] does not have mutable elements + * Produce new matrix space with given dimensions. The space produced could be raised from cache since [MatrixSpace] does not have mutable elements */ - abstract fun produceSpace(rows: Int, columns: Int): LinearSpace + abstract fun produceSpace(rows: Int, columns: Int): MatrixSpace - override val zero: V by lazy { + override val zero: Matrix by lazy { produce { _, _ -> field.zero } } - val one: V by lazy { - produce { i, j -> if (i == j) field.one else field.zero } - } +// val one: Matrix by lazy { +// produce { i, j -> if (i == j) field.one else field.zero } +// } - override fun add(a: V, b: V): V { + override fun add(a: Matrix, b: Matrix): Matrix { return produce { i, j -> with(field) { a[i, j] + b[i, j] } } } - override fun multiply(a: V, k: Double): V { + override fun multiply(a: Matrix, k: Double): Matrix { //TODO it is possible to implement scalable linear elements which normed values and adjustable scale to save memory and processing poser return produce { i, j -> with(field) { a[i, j] * k } } } @@ -46,29 +46,23 @@ abstract class LinearSpace>(val rows: Int, val columns: I /** * Dot product. Throws exception on dimension mismatch */ - fun multiply(a: V, b: V): V { + fun multiply(a: Matrix, b: Matrix): Matrix { if (a.rows != b.columns) { //TODO replace by specific exception error("Dimension mismatch in linear structure dot product: [${a.rows},${a.columns}]*[${b.rows},${b.columns}]") } return produceSpace(a.rows, b.columns).produce { i, j -> - (0..a.columns).asSequence().map { k -> field.multiply(a[i, k], b[k, j]) }.reduce { first, second -> field.add(first, second) } + (0 until a.columns).asSequence().map { k -> field.multiply(a[i, k], b[k, j]) }.reduce { first, second -> field.add(first, second) } } } - - infix fun V.dot(b: V): V = multiply(this, b) } -/** - * A specialized [LinearSpace] which works with vectors - */ -abstract class VectorSpace>(size: Int, field: Field) : LinearSpace(size, 1, field) +infix fun Matrix.dot(b: Matrix): Matrix = this.context.multiply(this, b) /** * A matrix-like structure */ -interface Matrix { - val context: LinearSpace> +interface Matrix : SpaceElement, MatrixSpace> { /** * Number of rows */ @@ -83,34 +77,58 @@ interface Matrix { */ operator fun get(i: Int, j: Int): T + override val self: Matrix + get() = this + fun transpose(): Matrix { return object : Matrix { - override val context: LinearSpace> = this@Matrix.context + override val context: MatrixSpace = this@Matrix.context override val rows: Int = this@Matrix.columns override val columns: Int = this@Matrix.rows override fun get(i: Int, j: Int): T = this@Matrix[j, i] } } + + companion object { + fun one(rows: Int, columns: Int, field: Field): Matrix { + return matrix(rows, columns, field) { i, j -> if (i == j) field.one else field.zero } + } + } } -interface Vector : Matrix { - override val context: VectorSpace> - override val columns: Int - get() = 1 - operator fun get(i: Int) = get(i, 0) +/** + * A linear space for vectors + */ +abstract class VectorSpace(val size: Int, val field: Field) : Space> { + + abstract fun produce(initializer: (Int) -> T): Vector + + override val zero: Vector by lazy { produce { field.zero } } + + override fun add(a: Vector, b: Vector): Vector = produce { with(field) { a[it] + b[it] } } + + override fun multiply(a: Vector, k: Double): Vector = produce { with(field) { a[it] * k } } +} + + +interface Vector : SpaceElement, VectorSpace> { + val size: Int + get() = context.size + + operator fun get(i: Int): T } /** * NDArray-based implementation of vector space. By default uses slow [SimpleNDField], but could be overridden with custom [NDField] factory. */ -class ArraySpace( +class ArrayMatrixSpace( rows: Int, columns: Int, field: Field, val ndFactory: NDFieldFactory = createFactory(field) -) : LinearSpace>(rows, columns, field) { +) : MatrixSpace(rows, columns, field) { val ndField by lazy { ndFactory(listOf(rows, columns)) @@ -118,8 +136,8 @@ class ArraySpace( override fun produce(initializer: (Int, Int) -> T): Matrix = ArrayMatrix(this, initializer) - override fun produceSpace(rows: Int, columns: Int): ArraySpace { - return ArraySpace(rows, columns, field, ndFactory) + override fun produceSpace(rows: Int, columns: Int): ArrayMatrixSpace { + return ArrayMatrixSpace(rows, columns, field, ndFactory) } } @@ -127,26 +145,20 @@ class ArrayVectorSpace( size: Int, field: Field, val ndFactory: NDFieldFactory = createFactory(field) -) : VectorSpace>(size, field) { +) : VectorSpace(size, field) { val ndField by lazy { ndFactory(listOf(size)) } - override fun produce(initializer: (Int, Int) -> T): Vector = produceVector { i -> initializer(i, 0) } - - fun produceVector(initializer: (Int) -> T): Vector = ArrayVector(this, initializer) - - override fun produceSpace(rows: Int, columns: Int): LinearSpace> { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } + override fun produce(initializer: (Int) -> T): Vector = ArrayVector(this, initializer) } /** - * Member of [ArraySpace] which wraps 2-D array + * Member of [ArrayMatrixSpace] which wraps 2-D array */ -class ArrayMatrix internal constructor(override val context: ArraySpace, val array: NDArray) : Matrix, SpaceElement, ArraySpace> { +class ArrayMatrix internal constructor(override val context: ArrayMatrixSpace, val array: NDArray) : Matrix { - constructor(context: ArraySpace, initializer: (Int, Int) -> T) : this(context, context.ndField.produce { list -> initializer(list[0], list[1]) }) + constructor(context: ArrayMatrixSpace, initializer: (Int, Int) -> T) : this(context, context.ndField.produce { list -> initializer(list[0], list[1]) }) override val rows: Int get() = context.rows @@ -160,32 +172,21 @@ class ArrayMatrix internal constructor(override val context: ArraySpace } -class ArrayVector internal constructor(override val context: ArrayVectorSpace, val array: NDArray) : Vector, SpaceElement, ArrayVectorSpace> { +class ArrayVector internal constructor(override val context: ArrayVectorSpace, val array: NDArray) : Vector { constructor(context: ArrayVectorSpace, initializer: (Int) -> T) : this(context, context.ndField.produce { list -> initializer(list[0]) }) init { - if (context.columns != 1) { - error("Vector must have single column") - } - if (context.rows != array.shape[0]) { + if (context.size != array.shape[0]) { error("Array dimension mismatch") } } - //private val array = context.ndField.produce { list -> initializer(list[0]) } - - - override val rows: Int get() = context.rows - - override val columns: Int = 1 - - override fun get(i: Int, j: Int): T { + override fun get(i: Int): T { return array[i] } override val self: ArrayVector get() = this - } /** @@ -193,8 +194,8 @@ class ArrayVector internal constructor(override val context: ArrayVecto */ interface LinearSolver { fun solve(a: Matrix, b: Matrix): Matrix - fun solve(a: Matrix, b: Vector): Vector = solve(a, b as Matrix).toVector() - fun inverse(a: Matrix): Matrix = solve(a, a.context.one) + fun solve(a: Matrix, b: Vector): Vector = solve(a, b.toMatrix()).toVector() + fun inverse(a: Matrix): Matrix = solve(a, Matrix.one(a.rows, a.columns, a.context.field)) } /** @@ -220,13 +221,13 @@ fun DoubleArray.asVector() = realVector(this.size) { this[it] } * Create [ArrayMatrix] with custom field */ fun matrix(rows: Int, columns: Int, field: Field, initializer: (Int, Int) -> T) = - ArrayMatrix(ArraySpace(rows, columns, field), initializer) + ArrayMatrix(ArrayMatrixSpace(rows, columns, field), initializer) /** * Create [ArrayMatrix] of doubles. */ fun realMatrix(rows: Int, columns: Int, initializer: (Int, Int) -> Double) = - ArrayMatrix(ArraySpace(rows, columns, DoubleField, realNDFieldFactory), initializer) + ArrayMatrix(ArrayMatrixSpace(rows, columns, DoubleField, realNDFieldFactory), initializer) /** @@ -234,17 +235,28 @@ fun realMatrix(rows: Int, columns: Int, initializer: (Int, Int) -> Double) = */ fun Matrix.toVector(): Vector { return when { - this is Vector -> return this this.columns == 1 -> { - if (this is ArrayMatrix) { - //Reuse existing underlying array - ArrayVector(ArrayVectorSpace(rows, context.field, context.ndFactory), array) - } else { - //Generic vector - vector(rows, context.field) { get(it, 0) } - } +// if (this is ArrayMatrix) { +// //Reuse existing underlying array +// ArrayVector(ArrayVectorSpace(rows, context.field, context.ndFactory), array) +// } else { +// //Generic vector +// vector(rows, context.field) { get(it, 0) } +// } + vector(rows, context.field) { get(it, 0) } } else -> error("Can't convert matrix with more than one column to vector") } } +fun Vector.toMatrix(): Matrix { +// return if (this is ArrayVector) { +// //Reuse existing underlying array +// ArrayMatrix(ArrayMatrixSpace(size, 1, context.field, context.ndFactory), array) +// } else { +// //Generic vector +// matrix(size, 1, context.field) { i, j -> get(i) } +// } + return matrix(size, 1, context.field) { i, j -> get(i) } +} + diff --git a/kmath-core/src/commonMain/kotlin/scientifik/kmath/operations/Algebra.kt b/kmath-core/src/commonMain/kotlin/scientifik/kmath/operations/Algebra.kt index 9938f4856..9a56a4e91 100644 --- a/kmath-core/src/commonMain/kotlin/scientifik/kmath/operations/Algebra.kt +++ b/kmath-core/src/commonMain/kotlin/scientifik/kmath/operations/Algebra.kt @@ -7,6 +7,11 @@ package scientifik.kmath.operations * @param S the type of mathematical context for this element */ interface MathElement { + /** + * Self value. Needed for static type checking. + */ + val self: T + /** * The context this element belongs to */ @@ -54,12 +59,6 @@ interface Space { * @param S the type of space */ interface SpaceElement> : MathElement { - - /** - * Self value. Needed for static type checking. Needed to avoid type erasure on JVM. - */ - val self: T - operator fun plus(b: T): T = context.add(self, b) operator fun minus(b: T): T = context.add(self, context.multiply(b, -1.0)) operator fun times(k: Number): T = context.multiply(self, k.toDouble()) diff --git a/kmath-core/src/commonTest/kotlin/scientifik/kmath/structures/ArrayMatrixTest.kt b/kmath-core/src/commonTest/kotlin/scientifik/kmath/linear/ArrayMatrixTest.kt similarity index 50% rename from kmath-core/src/commonTest/kotlin/scientifik/kmath/structures/ArrayMatrixTest.kt rename to kmath-core/src/commonTest/kotlin/scientifik/kmath/linear/ArrayMatrixTest.kt index 77a436626..ecc2ca28c 100644 --- a/kmath-core/src/commonTest/kotlin/scientifik/kmath/structures/ArrayMatrixTest.kt +++ b/kmath-core/src/commonTest/kotlin/scientifik/kmath/linear/ArrayMatrixTest.kt @@ -1,6 +1,5 @@ -package scientifik.kmath.structures +package scientifik.kmath.linear -import scientifik.kmath.linear.realVector import kotlin.test.Test import kotlin.test.assertEquals @@ -11,17 +10,25 @@ class ArrayMatrixTest { val vector1 = realVector(5) { it.toDouble() } val vector2 = realVector(5) { 5 - it.toDouble() } val sum = vector1 + vector2 - assertEquals(5.0, sum[2, 0]) + assertEquals(5.0, sum[2]) } + @Test + fun testVectorToMatrix() { + val vector = realVector(5) { it.toDouble() } + val matrix = vector.toMatrix() + assertEquals(4.0, matrix[4, 0]) + } + + @Test fun testDot() { val vector1 = realVector(5) { it.toDouble() } val vector2 = realVector(5) { 5 - it.toDouble() } - val product = with(vector1.context) { - vector1 dot (vector2.transpose()) - } + val product = vector1.toMatrix() dot (vector2.toMatrix().transpose()) - assertEquals(10.0, product[1, 0]) + + assertEquals(5.0, product[1, 0]) + assertEquals(6.0, product[2, 2]) } } \ No newline at end of file diff --git a/kmath-core/src/commonTest/kotlin/scientifik/kmath/linear/RealLUSolverTest.kt b/kmath-core/src/commonTest/kotlin/scientifik/kmath/linear/RealLUSolverTest.kt new file mode 100644 index 000000000..d2237b80a --- /dev/null +++ b/kmath-core/src/commonTest/kotlin/scientifik/kmath/linear/RealLUSolverTest.kt @@ -0,0 +1,14 @@ +package scientifik.kmath.linear + +import scientifik.kmath.operations.DoubleField +import kotlin.test.Test +import kotlin.test.assertEquals + +class RealLUSolverTest { + @Test + fun testInvert() { + val matrix = Matrix.one(2, 2, DoubleField) + val inverted = RealLUSolver.inverse(matrix) + assertEquals(1.0, inverted[0, 0]) + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 988a94843..03760221a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,5 +10,4 @@ enableFeaturePreview('GRADLE_METADATA') rootProject.name = 'kmath' include ':kmath-core' -include ':kmath-commons'