[orx-math] Add matrix and sparse matrix implementations with Cholesky and QR decompositions

This commit is contained in:
Edwin Jakobs
2025-08-16 17:21:33 +02:00
parent e6997a968f
commit 1d81754a23
7 changed files with 1670 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
package matrix
import org.openrndr.extra.math.matrix.SparseMatrix
import org.openrndr.extra.math.matrix.qrDecomposition
import org.openrndr.extra.math.matrix.solveQR
import kotlin.test.assertEquals
import kotlin.math.abs
import kotlin.test.Test
class SparseMatrixQRTest {
@Test
fun testQRDecomposition() {
// Create a sparse matrix
// This will create the following matrix:
// 4.0, 3.0, 0.0
// 6.0, 3.0, 0.0
// 0.0, 1.0, 5.0
val values = doubleArrayOf(4.0, 3.0, 6.0, 3.0, 1.0, 5.0)
val columnIndices = intArrayOf(0, 1, 0, 1, 1, 2)
val rowPointers = intArrayOf(0, 2, 4, 6)
val sparseMatrix = SparseMatrix(3, 3, values, columnIndices, rowPointers)
// Perform QR decomposition
val (q, r) = qrDecomposition(sparseMatrix)
val originalDense = sparseMatrix.toDenseMatrix()
for (i in 0 until originalDense.rows) {
val row = (0 until originalDense.cols).map { j -> originalDense[i, j] }.joinToString()
}
val qDense = q.toDenseMatrix()
for (i in 0 until qDense.rows) {
val row = (0 until qDense.cols).map { j -> qDense[i, j] }.joinToString()
}
val rDense = r.toDenseMatrix()
for (i in 0 until rDense.rows) {
val row = (0 until rDense.cols).map { j -> rDense[i, j] }.joinToString()
}
// Verify Q is orthogonal (Q^T * Q = I)
val qTranspose = q.transpose()
val qTq = qTranspose * q
val identity = SparseMatrix.identity(q.rows)
val qTqDense = qTq.toDenseMatrix()
for (i in 0 until qTqDense.rows) {
val row = (0 until qTqDense.cols).map { j -> qTqDense[i, j] }.joinToString()
}
// Check that Q^T * Q is approximately identity
for (i in 0 until q.rows) {
for (j in 0 until q.cols) {
assertEquals(identity[i, j], qTq[i, j], 1e-10)
}
}
// Verify R is upper triangular
for (i in 0 until r.rows) {
for (j in 0 until r.cols) {
if (i > j) {
assertEquals(0.0, abs(r[i, j]), 1e-10)
}
}
}
// Verify A = Q * R
val qr = q * r
val qrDense = qr.toDenseMatrix()
for (i in 0 until qrDense.rows) {
val row = (0 until qrDense.cols).map { j -> qrDense[i, j] }.joinToString()
}
// Check that A = Q * R
for (i in 0 until sparseMatrix.rows) {
for (j in 0 until sparseMatrix.cols) {
assertEquals(sparseMatrix[i, j], qr[i, j], 1e-10)
}
}
}
@Test
fun testSolveWithQRDecomposition() {
// Create a sparse matrix A
// This will create the following matrix:
// 4.0, 3.0, 0.0
// 6.0, 3.0, 0.0
// 0.0, 1.0, 5.0
val valuesA = doubleArrayOf(4.0, 3.0, 6.0, 3.0, 1.0, 5.0)
val columnIndicesA = intArrayOf(0, 1, 0, 1, 1, 2)
val rowPointersA = intArrayOf(0, 2, 4, 6)
val matrixA = SparseMatrix(3, 3, valuesA, columnIndicesA, rowPointersA)
// Create a sparse matrix b (right-hand side)
// This will create the following vector:
// 1.0
// 2.0
// 3.0
val valuesB = doubleArrayOf(1.0, 2.0, 3.0)
val columnIndicesB = intArrayOf(0, 0, 0)
val rowPointersB = intArrayOf(0, 1, 2, 3)
val matrixB = SparseMatrix(3, 1, valuesB, columnIndicesB, rowPointersB)
// Perform QR decomposition
val qr = qrDecomposition(matrixA)
// Solve the system Ax = b
val x = solveQR(qr, matrixB)
// Verify A * x = b
val product = matrixA * x
// Check that A * x = b
for (i in 0 until matrixB.rows) {
for (j in 0 until matrixB.cols) {
assertEquals(matrixB[i, j], product[i, j], 1e-10)
}
}
}
}

View File

@@ -0,0 +1,270 @@
package matrix
import org.openrndr.extra.math.matrix.Matrix
import org.openrndr.extra.math.matrix.SparseMatrix
import org.openrndr.extra.math.matrix.checkIntegrity
import org.openrndr.extra.math.matrix.toSparseMatrix
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class SparseMatrixTest {
@Test
fun testCreateSparseMatrix() {
// Create a sparse matrix directly
val values = doubleArrayOf(1.0, 2.0, 3.0, 4.0)
val columnIndices = intArrayOf(0, 2, 1, 2)
val rowPointers = intArrayOf(0, 2, 3, 4)
val sparseMatrix = SparseMatrix(3, 3, values, columnIndices, rowPointers)
// Verify dimensions
assertEquals(3, sparseMatrix.rows)
assertEquals(3, sparseMatrix.cols)
// Verify values
assertEquals(1.0, sparseMatrix[0, 0])
assertEquals(0.0, sparseMatrix[0, 1])
assertEquals(2.0, sparseMatrix[0, 2])
assertEquals(0.0, sparseMatrix[1, 0])
assertEquals(3.0, sparseMatrix[1, 1])
assertEquals(0.0, sparseMatrix[1, 2])
assertEquals(0.0, sparseMatrix[2, 0])
assertEquals(0.0, sparseMatrix[2, 1])
assertEquals(4.0, sparseMatrix[2, 2])
}
@Test
fun testFromDenseMatrix() {
// Create a dense matrix
val denseMatrix = Matrix(3, 3)
denseMatrix[0, 0] = 1.0
denseMatrix[0, 2] = 2.0
denseMatrix[1, 1] = 3.0
denseMatrix[2, 2] = 4.0
// Convert to sparse matrix
val sparseMatrix = denseMatrix.toSparseMatrix()
// Verify dimensions
assertEquals(3, sparseMatrix.rows)
assertEquals(3, sparseMatrix.cols)
// Verify values
assertEquals(1.0, sparseMatrix[0, 0])
assertEquals(0.0, sparseMatrix[0, 1])
assertEquals(2.0, sparseMatrix[0, 2])
assertEquals(0.0, sparseMatrix[1, 0])
assertEquals(3.0, sparseMatrix[1, 1])
assertEquals(0.0, sparseMatrix[1, 2])
assertEquals(0.0, sparseMatrix[2, 0])
assertEquals(0.0, sparseMatrix[2, 1])
assertEquals(4.0, sparseMatrix[2, 2])
// Verify non-zero count
assertEquals(4, sparseMatrix.nonZeroCount())
}
@Test
fun testToDenseMatrix() {
// Create a sparse matrix
val values = doubleArrayOf(1.0, 2.0, 3.0, 4.0)
val columnIndices = intArrayOf(0, 2, 1, 2)
val rowPointers = intArrayOf(0, 2, 3, 4)
val sparseMatrix = SparseMatrix(3, 3, values, columnIndices, rowPointers)
// Convert to dense matrix
val denseMatrix = sparseMatrix.toDenseMatrix()
// Verify dimensions
assertEquals(3, denseMatrix.rows)
assertEquals(3, denseMatrix.cols)
// Verify values
assertEquals(1.0, denseMatrix[0, 0])
assertEquals(0.0, denseMatrix[0, 1])
assertEquals(2.0, denseMatrix[0, 2])
assertEquals(0.0, denseMatrix[1, 0])
assertEquals(3.0, denseMatrix[1, 1])
assertEquals(0.0, denseMatrix[1, 2])
assertEquals(0.0, denseMatrix[2, 0])
assertEquals(0.0, denseMatrix[2, 1])
assertEquals(4.0, denseMatrix[2, 2])
}
@Test
fun testMatrixMultiplication() {
// Create a sparse matrix
val values1 = doubleArrayOf(1.0, 2.0, 3.0)
val columnIndices1 = intArrayOf(0, 1, 0)
val rowPointers1 = intArrayOf(0, 2, 3)
val sparseMatrix1 = SparseMatrix(2, 2, values1, columnIndices1, rowPointers1)
// Create another sparse matrix
val values2 = doubleArrayOf(4.0, 5.0, 6.0)
val columnIndices2 = intArrayOf(0, 1, 1)
val rowPointers2 = intArrayOf(0, 2, 3)
val sparseMatrix2 = SparseMatrix(2, 2, values2, columnIndices2, rowPointers2)
// Multiply sparse matrices
val result = sparseMatrix1.times(sparseMatrix2)
result.checkIntegrity()
// Verify dimensions
assertEquals(2, result.rows)
assertEquals(2, result.cols)
// Verify values (1*4 + 2*0 = 4, 1*5 + 2*6 = 17, 3*4 + 0*0 = 12, 3*5 + 0*6 = 15)
assertEquals(4.0, result[0, 0])
assertEquals(17.0, result[0, 1])
assertEquals(12.0, result[1, 0])
assertEquals(15.0, result[1, 1])
}
@Test
fun testScalarMultiplication() {
// Create a sparse matrix
val values = doubleArrayOf(1.0, 2.0, 3.0, 4.0)
val columnIndices = intArrayOf(0, 2, 1, 2)
val rowPointers = intArrayOf(0, 2, 3, 4)
val sparseMatrix = SparseMatrix(3, 3, values, columnIndices, rowPointers)
// Multiply by scalar
val result = sparseMatrix * 2.0
// Verify dimensions
assertEquals(3, result.rows)
assertEquals(3, result.cols)
// Verify values
assertEquals(2.0, result[0, 0])
assertEquals(0.0, result[0, 1])
assertEquals(4.0, result[0, 2])
assertEquals(0.0, result[1, 0])
assertEquals(6.0, result[1, 1])
assertEquals(0.0, result[1, 2])
assertEquals(0.0, result[2, 0])
assertEquals(0.0, result[2, 1])
assertEquals(8.0, result[2, 2])
}
@Test
fun testAddition() {
// Create a sparse matrix
val values1 = doubleArrayOf(1.0, 2.0, 3.0)
val columnIndices1 = intArrayOf(0, 2, 1)
val rowPointers1 = intArrayOf(0, 2, 3)
val sparseMatrix1 = SparseMatrix(2, 3, values1, columnIndices1, rowPointers1)
// Create another sparse matrix
val values2 = doubleArrayOf(4.0, 5.0, 6.0)
val columnIndices2 = intArrayOf(0, 1, 2)
val rowPointers2 = intArrayOf(0, 2, 3)
val sparseMatrix2 = SparseMatrix(2, 3, values2, columnIndices2, rowPointers2)
// Add sparse matrices
val result = sparseMatrix1 + sparseMatrix2
result.checkIntegrity()
// Verify dimensions
assertEquals(2, result.rows)
assertEquals(3, result.cols)
// Verify values
assertEquals(5.0, result[0, 0])
assertEquals(5.0, result[0, 1])
assertEquals(2.0, result[0, 2])
assertEquals(0.0, result[1, 0])
assertEquals(3.0, result[1, 1])
assertEquals(6.0, result[1, 2])
}
@Test
fun testSubtraction() {
// Create a sparse matrix
val values1 = doubleArrayOf(5.0, 7.0, 9.0)
val columnIndices1 = intArrayOf(0, 2, 1)
val rowPointers1 = intArrayOf(0, 2, 3)
val sparseMatrix1 = SparseMatrix(2, 3, values1, columnIndices1, rowPointers1)
// Create another sparse matrix
val values2 = doubleArrayOf(1.0, 2.0, 3.0)
val columnIndices2 = intArrayOf(0, 1, 2)
val rowPointers2 = intArrayOf(0, 2, 3)
val sparseMatrix2 = SparseMatrix(2, 3, values2, columnIndices2, rowPointers2)
// Subtract sparse matrices
val result = sparseMatrix1 - sparseMatrix2
result.checkIntegrity()
// Verify dimensions
assertEquals(2, result.rows)
assertEquals(3, result.cols)
// Verify values
assertEquals(4.0, result[0, 0])
assertEquals(-2.0, result[0, 1])
assertEquals(7.0, result[0, 2])
assertEquals(0.0, result[1, 0])
assertEquals(9.0, result[1, 1])
assertEquals(-3.0, result[1, 2])
}
@Test
fun testTranspose() {
// Create a sparse matrix
val values = doubleArrayOf(1.0, 2.0, 3.0, 4.0)
val columnIndices = intArrayOf(0, 2, 1, 2)
val rowPointers = intArrayOf(0, 2, 3, 4)
val sparseMatrix = SparseMatrix(3, 3, values, columnIndices, rowPointers)
sparseMatrix.checkIntegrity()
// Transpose the matrix
val transposed = sparseMatrix.transpose()
// Verify dimensions
assertEquals(3, transposed.rows)
assertEquals(3, transposed.cols)
// Verify values
assertEquals(1.0, transposed[0, 0])
assertEquals(0.0, transposed[0, 1])
assertEquals(0.0, transposed[0, 2])
assertEquals(0.0, transposed[1, 0])
assertEquals(3.0, transposed[1, 1])
assertEquals(0.0, transposed[1, 2])
assertEquals(2.0, transposed[2, 0])
assertEquals(0.0, transposed[2, 1])
assertEquals(4.0, transposed[2, 2])
}
@Test
fun testIsSymmetric() {
// Create a symmetric sparse matrix
// For a symmetric matrix, if matrix[i,j] = value, then matrix[j,i] = value
// In CSR format, we need to ensure both positions have the same value
// This will create the following symmetric matrix:
// 1.0, 2.0, 3.0
// 2.0, 4.0, 5.0
// 3.0, 5.0, 6.0
val values = doubleArrayOf(1.0, 2.0, 3.0, 2.0, 4.0, 5.0, 3.0, 5.0, 6.0)
val columnIndices = intArrayOf(0, 1, 2, 0, 1, 2, 0, 1, 2)
val rowPointers = intArrayOf(0, 3, 6)
val symmetricMatrix = SparseMatrix(3, 3, values, columnIndices, rowPointers)
// Verify it's symmetric
assertTrue(symmetricMatrix.isSymmetric())
// Create a non-symmetric sparse matrix
val values2 = doubleArrayOf(1.0, 2.0, 3.0, 4.0)
val columnIndices2 = intArrayOf(0, 2, 1, 2)
val rowPointers2 = intArrayOf(0, 2, 3, 4)
val nonSymmetricMatrix = SparseMatrix(3, 3, values2, columnIndices2, rowPointers2)
// Verify it's not symmetric
assertFalse(nonSymmetricMatrix.isSymmetric())
}
}