Matrix Validation DSL

Koma provides definitions for a domain-specific language that you can use to validate matrices used as inputs to your functions in a consistent way. The rules throw detailed, human-readable exceptions using an appropriate Java exception class based on rules that look like this:

import koma.*
import koma.extensions.*
import koma.util.validation.*

fun mFunction(foo: Matrix<Double>, bar: Matrix<Double>, baz: Matrix<Double>) {
    validate {
        foo("foo") {  1  x 'N'; transposable }
        bar("bar") { 'N' x 'N'; symmetric }
        baz("baz") { 'N' x  1 ; max = 5 }
    }

    /* Your code here */
}

Some of the exceptions the above code could generate include:

java.lang.IndexOutOfBoundsException: Invalid matrix dimensions.

Matrix Required Actual 
====== ======== ====== 
foo       1xN     1x2
bar       NxN     2x2
baz       Nx1     2x3

baz must have the same number of rows as foo has columns
baz must have the same number of rows as bar has rows
baz must have the same number of rows as bar has columns
baz must have exactly 1 columns (has 3)

or

java.lang.IndexOutOfBoundsException: bar must be symmetric, but has dimensions 1x2

or

java.lang.IllegalArgumentException: baz[0, 0] > 5.0 (value was 15.0)

Annotated Syntax

fun myFunction(foo: Matrix<Double>, bar: Matrix<Double>, baz: Matrix<Double>) {
    validate { /*
        vvv ------------------------------------ Matrix to examine.
            vvvvv ------------------------------ Name to use in the exception.
                    vvvvvvvvvvvvvvvvvvvvvvvv --- Rules to check the matrix */
        foo("foo") { 1  x 'N'; transposable }
    }
}

Rules

Rules are regular Kotlin statements, which can be separated either by semicolons or newlines. The rules block will be evaluated with an instance of ValidaitonContext as its receiver.

Syntax Description
1 x 2 or dim(1, 2) Verify the matrix has 1 row and 2 columns. Values can be any expression that evaluates to Int. The latter syntax is provided as an alternative if the order-of-operations for infix functions does something weird.
1 x 'N' Verify the matrix has 1 row and any number of columns; compare the column count to other things that use the character 'N'
transposable The given dimensions can be in either order. So, a 1 x 3; transposable matrix can have either 1 row and 3 columns or 3 rows and 1 column.
symmetric Verify that the matrix is symmetric
max = 4.0 Specify a maximum allowable value for individual coefficients in the matrix. Can be any expression that evaluates to Double
min = 2.0 Specify a minimum allowable value for individual coefficients in the matrix. Can be any expression that evaluates to Double

Shorthand for a single Matrix

If you've written a function that only has a single Matrix argument, you can use this shorthand syntax instead of a full validate block.

fun myFunction(foo: Matrix<Double>) {
    foo.validate("foo") { 1 x 3; transposable }

    /* Your code here */
}

A Cautionary Example

If you have more than one matrix, you should avoid the shorthand syntax because dimensions variables will not "stick" as you might expect. For example:

fun myFunction(foo: Matrix<Double>, bar: Matrix<Double>) {
    foo.validate("foo") {  1  x 'N'; transposable }
    bar.validate("bar") { 'N' x 'N' } // <-- WILL NOT WORK


    /* Your code here */
}

In that example, it will validate that bar is square, but not that its dimensions correspond with the number of columns in foo as you might expect. This is because each validate block allocates a separate ValidationContext that performs some of its validations at the end of the block.

How it works

Behind the scenes, the validation code uses kotlin extension methods to enable a syntax inspired by Kotlin's "Type-safe builders" feature.

Each of the validation rules you define is calling an extension method on the ValidationContext class.

If you'd like to extend the validation syntax yourself, you can do so by adding more extension methods to ValidationContext. bounds.kt probably offers the best example to work from.