All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.io.nacular.doodle.utils.Matrix.kt Maven / Gradle / Ivy

@file:Suppress("NestedLambdaShadowedImplicitParameter", "ReplaceSingleLineLet")

package io.nacular.doodle.utils


/**
 * An NxM [Matrix](https://en.wikipedia.org/wiki/Matrix_(mathematics)).
 */
public interface Matrix {
    /** The number of rows in the matrix. */
    public val numRows: Int

    /** The number of columns in the matrix. */
    public val numColumns: Int

    /** A value within the Matrix at [row, col]. */
    public operator fun get(row: Int, col: Int): T
}

/**
 * A square (NxN) [Matrix].
 *
 * @constructor creates a new matrix from numbers
 * @param values must be a square 2d list of list of values
 */
public open class SquareMatrix internal constructor(values: List>): MatrixImpl(values) {
    /**
     * `true` if this matrix is equal to the [Identity Matrix](https://en.wikipedia.org/wiki/Matrix_(mathematics)#Identity_matrix):
     *
     * ```
     *                                 |1 0 ... 0|
     *                |1 0|            |0 1 ... 0|
     * I1 = |1|, I2 = |0 1|, ..., In = |: : ... :|
     *                                 |0 0 ... 1|
     * ```
     */
    public var isIdentity: Boolean = true
        private set

    init {
        require(numRows == numColumns) { "row and column count must be equal" }

        for (row in values.indices) {
            if (isIdentity) {
                run loop@{
                    values[row].forEachIndexed { index, value ->
                        isIdentity = when {
                            value.toDouble() != 1.0 && index == row -> false
                            value.toDouble() != 0.0 && index != row -> false
                            else                       -> true
                        }

                        if (!isIdentity) { return@loop }
                    }
                }
            }
        }
    }

    /**
     * The inverse of this matrix if it is [invertible](https://en.wikipedia.org/wiki/Invertible_matrix).
     */
    public val inverse: SquareMatrix? by lazy {
        when {
            isIdentity         -> this.map { it.toDouble() } // TODO: This shouldn't require any copy
            determinant == 0.0 -> null
            else               -> {
                val cofactors = values.mapIndexed { r, values ->
                    values.mapIndexed { c, _ ->
                        determinant(r, c) * when {
                            (r + c).isOdd -> -1
                            else          ->  1
                        }
                    }
                }.let { squareMatrixOf(numRows) { col, row -> it[row][col] } }

                val transposed = cofactors.transpose()

                (transposed * (1.0 / determinant))
            }
        }
    }

    private val determinant: Double by lazy {
        when (numRows) {
            1    -> this[0,0].toDouble()
            2    -> this[0,0].toDouble() * this[1,1].toDouble() - this[0,1].toDouble() * this[1,0].toDouble()
            else -> {
                var sign   = 1
                var result = 0.0

                for (col in 0 until numColumns) {
                    result += sign * this[0, col].toDouble() * determinant(0, col)
                    sign *= -1
                }

                result
            }
        }
    }

    private fun determinant(row: Int, col: Int): Double {
        val subData = values.filterIndexed { r, _ -> r != row }.map { it.filterIndexed { c, _ -> c != col } }

        return SquareMatrix(subData).determinant
    }
}

/**
 * A matrix used to represent a 2D Affine Transformation. It is of the form
 *
 * ```
 *
 * |scaleX shearX translateX|
 * |shearY scaleY translateY|
 * |0      0      1         |
 *
 * ```
 *
 * @constructor creates a new instance
 * @param scaleX component of the matrix
 * @param shearX component of the matrix
 * @param translateX component of the matrix
 * @param shearY component of the matrix
 * @param scaleY component of the matrix
 * @param translateY component of the matrix
 */
public class AffineMatrix3D(
        scaleX    : Double,
        shearX    : Double,
        translateX: Double,
        shearY    : Double,
        scaleY    : Double,
        translateY: Double): SquareMatrix(listOf(
            listOf(scaleX, shearX, translateX),
            listOf(shearY, scaleY, translateY),
            listOf(   0.0,    0.0,        1.0)))

/**
 * Creates an NxN [SquareMatrix], where N == [size].
 *
 * @param size of N
 * @param init operation to get each value at [row, col]
 */
public fun  squareMatrixOf(size: Int, init: (row: Int, col: Int) -> T): SquareMatrix = SquareMatrix(List(size) { row -> List(size) { col -> init(col, row) } })

/**
 * Creates an NxM [Matrix], where N == [rows] and M == [cols].
 *
 * @param rows of the matrix
 * @param cols of the matrix
 * @param init operation to get each value at [row, col]
 */
public fun  matrixOf(rows: Int, cols: Int, init: (Int, Int) -> T): Matrix = when {
    rows != cols -> MatrixImpl(List(rows) { row -> List(cols) { col -> init(col, row) } })
    else         -> squareMatrixOf(rows, init)
}

/**
 * Gives the [transposition](https://en.wikipedia.org/wiki/Transpose) of a [SquareMatrix].
 */
public fun  SquareMatrix.transpose(): SquareMatrix {
    val values = MutableList(numRows){ MutableList(numColumns) { null } }

    for (row in 0 until numRows) {
        for (col in 0 until numColumns) {
            values[row][col] = this[col, row]
        }
    }

    return SquareMatrix(values.map { it.mapNotNull { it } })
}

/**
 * [Matrix multiplication](https://en.wikipedia.org/wiki/Matrix_multiplication) of two [Matrix]es.
 */
public operator fun Matrix.times(other: Matrix): Matrix {
    require (other.numRows == numColumns) { "matrix column and row counts do not match" }

    val values = MutableList(numRows) { MutableList(other.numColumns) { 0.0 } }

    for (r1 in 0 until numRows) {
        for (c2 in 0 until other.numColumns) {
            values[r1][c2] = (0 until other.numRows).sumByDouble { this[r1, it] * other[it, c2] }
        }
    }

    return MatrixImpl(values)
}

/**
 * @see times
 */
public operator fun Matrix.times(other: SquareMatrix): Matrix {
    if (other.isIdentity) {
        return this
    }

    return this * (other as Matrix)
}

/**
 * Left [Scalar multiplication](https://en.wikipedia.org/wiki/Scalar_multiplication) of a [SquareMatrix].
 *
 * ```
 *     |A11 A12 ... A1n|   |λA11 λA12 ... λA1n|
 * λ * |A21 A12 ... A21| = |λA21 λA12 ... λA21|
 *     | :   :  ...  : |   | :     :  ...  :  |
 *     |An1 An2 ... Ann|   |λAn1 λAn2 ... λAnn|
 * ```
 */
public operator fun  SquareMatrix.times(value: Number): SquareMatrix = map { it.toDouble() * value.toDouble() }

/**
 * Right [Scalar multiplication](https://en.wikipedia.org/wiki/Scalar_multiplication) of a [SquareMatrix].
 *
 * ```
 * |A11 A12 ... A1n|       |A11λ A12λ ... A1nλ|
 * |A21 A12 ... A21| * λ = |A21λ A12λ ... A21λ|
 * | :   :  ...  : |       | :     :  ...  :  |
 * |An1 An2 ... Ann|       |An1λ An2λ ... Annλ|
 * ```
 */
public operator fun  T.times(value: SquareMatrix): SquareMatrix = value.map { toDouble() * it.toDouble() }

/**
 * [Matrix multiplication](https://en.wikipedia.org/wiki/Matrix_multiplication) of two [SquareMatrix]es.
 */
public operator fun SquareMatrix.times(other: SquareMatrix): SquareMatrix {
    if (other.isIdentity) {
        return this
    }

    if (this.isIdentity) {
        return other
    }

    val values = MutableList(numRows) { MutableList(other.numColumns) { 0.0 } }

    for (c2 in 0 until other.numColumns) {
        for (r1 in 0 until numRows) {
            val sum = (0 until other.numRows).sumByDouble { this[r1, it] * other[it, c2] }

            values[r1][c2] = sum
        }
    }

    return SquareMatrix(values)
}

/**
 * @see times
 */
public operator fun AffineMatrix3D.times(value: Number): AffineMatrix3D = value.toDouble().let {
    AffineMatrix3D(
            this[0, 0] * it, this[0, 1] * it, this[0, 2] * it,
            this[1, 0] * it, this[1, 1] * it, this[1, 2] * it)
}

/**
 * @see times
 */
public operator fun AffineMatrix3D.times(other: AffineMatrix3D): AffineMatrix3D {
    if (other.isIdentity) {
        return this
    }

    if (this.isIdentity) {
        return other
    }

    val values = mutableListOf()//MutableList(numRows - 1) { MutableList(other.numColumns) { 0.0 } }

    for (r1 in 0 until numRows - 1) {
        for (c2 in 0 until other.numColumns) {
            values += (0 until other.numRows).sumByDouble { this[r1, it] * other[it, c2] }
        }
    }

    return AffineMatrix3D(values[0], values[1], values[2],
                          values[3], values[4], values[5])
}

/**
 * Creates a new [SquareMatrix] whose members are the [transform] of those in this one.
 *
 * @param transform operation to map elements
 */
public fun  SquareMatrix.map(transform: (T) -> R): SquareMatrix = SquareMatrix(values.map { it.map { transform(it) } })

/**
 * Like [map], but with [col, row] given for each item during tranformation.
 */
public fun  SquareMatrix.mapIndexed(transform: (col: Int, row: Int, T) -> R): SquareMatrix = SquareMatrix(values.mapIndexed { row, rows -> rows.mapIndexed { col, value -> transform(col, row, value) } })


/**
 * An (NxM) [Matrix].
 *
 * @constructor creates a new matrix from numbers
 * @param values must be a square 2d list of list of values
 */
public open class MatrixImpl internal constructor(internal val values: List>): Matrix {

    final override val numRows   : Int = values.size
    final override val numColumns: Int = if (numRows > 0) values[0].size else 0

    init {
        require(numColumns > 0) { "empty Matrices are invalid" }

        for (row in values.indices) {
            require(values[row].size == numColumns) { "all rows must have the same length" }
        }
    }

    override operator fun get(row: Int, col: Int): T = values[row][col]

    override fun toString(): String {
        val result = StringBuilder()

        for (r in 0 until numRows) {
            result.append((if (r > 0) "\n" else "") + "|")

            for (c in 0 until numColumns) {
                result.append((if (c > 0) " " else "") + values[r][c])
            }

            result.append("|")
        }

        return result.toString()
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Matrix<*>) return false

        if (numRows    != other.numRows   ) return false
        if (numColumns != other.numColumns) return false

        for (row in 0 until numRows) {
            for (col in 0 until numColumns) {
                if (this[row, col].toDouble() != other[row, col].toDouble()) {
                    return false
                }
            }
        }

        return true
    }

    override fun hashCode(): Int = values.hashCode()
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy