immut tutorial
This tutorial shows how to work with immutable matrices and vectors: build them, update them without losing earlier versions, compute exact integer results such as powers and determinants, and use lazy matrices for structured data. The reasoning behind the package is in the immut design.
Quick start
moon add Luna-Flow/linear-algebra@0.5.0
///|
import {
"Luna-Flow/linear-algebra/immut",
}
///|
test "first immutable matrix" {
let a = @immut.Matrix::from_2d_array([[1, 2], [3, 4]])
let b = a.set(0, 0, 10)
inspect(a, content="|1, 2|\n|3, 4|")
inspect(b, content="|10, 2|\n|3, 4|")
inspect(a * b, content="|16, 10|\n|42, 22|")
}
set returns a new matrix; a keeps its value.
Everyday tasks
Keep a history of edits
Because every update returns a new value, a history is just an array of matrices that share most of their storage:
///|
test "undo by keeping old versions" {
let history = [@immut.Matrix::new(2, 2, 0)]
for step in 0..<3 {
let current = history[history.length() - 1]
history.push(current.set(step % 2, step / 2, step + 1))
}
inspect(history[3], content="|1, 3|\n|2, 0|")
inspect(history[1], content="|1, 0|\n|0, 0|")
}
Count paths with a matrix power
Entry of counts the walks of length from to in the graph with adjacency matrix . Integer powers are exact:
///|
test "walks in a triangle graph" {
let triangle = @immut.Matrix::from_2d_array([[0, 1, 1], [1, 0, 1], [1, 1, 0]])
let walks = triangle.pow(4).unwrap()
inspect(walks[0][0], content="6")
inspect(walks[0][1], content="5")
match triangle.pow(-1) {
Err(e) => inspect(e.is_negative_exponent(), content="true")
Ok(_) => fail("negative powers are rejected")
}
}
Exact determinants
determinant uses fraction-free elimination, so integer matrices get exact
results, and BigInt matrices are exact at any size. Scaling every entry by
scales the determinant by :
///|
test "exact determinant of an integer matrix" {
let m = @immut.Matrix::from_2d_array([
[2, 0, 1, 3, 1],
[1, 3, 2, 0, 4],
[0, 1, 4, 1, 2],
[3, 2, 0, 5, 1],
[1, 0, 2, 1, 3],
])
inspect(m.determinant().unwrap(), content="62")
let big = m.map(x => BigInt::from_int(x) * 1000000000000N)
inspect(
big.determinant().unwrap(),
content="62000000000000000000000000000000000000000000000000000000000000",
)
}
Build matrices from vectors
///|
test "outer products and diagonals" {
let u = @immut.Vector::from_array([1, 2, 3])
let ones = @immut.Vector::make(3, 1)
let rank_one = u.tensor_product(ones)
inspect(rank_one, content="|1, 1, 1|\n|2, 2, 2|\n|3, 3, 3|")
let d = u.scaled_matrix()
inspect(d * rank_one, content="|1, 1, 1|\n|4, 4, 4|\n|9, 9, 9|")
inspect(u.to_row_matrix() * u.to_col_matrix(), content="|14|")
}
Describe a structured matrix lazily
A MatrixFn stores a rule instead of entries. It is a good fit for matrices
defined by a formula, such as a band matrix, when you read only part of them:
///|
test "a lazy tridiagonal matrix" {
let n = 1000
let band = @immut.MatrixFn::make(n, n, (i, j) => {
if i == j {
2
} else if i - j == 1 || j - i == 1 {
-1
} else {
0
}
})
inspect(band[500][499], content="-1")
inspect(band[500][700], content="0")
let small = @immut.MatrixFn::make(3, 3, (i, j) => band[i][j])
inspect(small, content="|2, -1, 0|\n|-1, 2, -1|\n|0, -1, 2|")
inspect(small.determinant(), content="4")
}
No array is ever allocated.
Going further
Generic code. The concrete type does not implement the algebra traits.
Wrap it in @default.ImmutableDenseMatrix to pass it to functions bounded by
MatMulMatrix (see the backends/default tutorial).
Converting to mutable. For numerical work such as inverses,
decompositions or statistics, convert with the
container adapters and use mutable.
Your own scalar type. Any type implementing the luna-generic traits
works: a rational or modular-integer type with Num and Div gets exact
determinants through Bareiss elimination, and any Semiring gets pow.
Performance. Reads and single-entry updates cost . For a
long series of edits on a large matrix, a @mutable.Matrix working buffer is
faster; freeze it into an immut value at the end.
Common pitfalls
- Integer overflow.
Intarithmetic wraps.powis then exact modulo , which is rarely what you want; useInt64orBigIntfor large values. - Assigning through indexing.
m[r][c] = xdoes not exist here; usem.set(r, c, x)and keep the result. - Subtracting vectors.
Vectorhas no-operator; writeu + -v. - Reading lazy powers. Every read of a
MatrixFnpower recomputes the nested products. Materialize first withMatrix::make(r, c, (i, j) => f[i][j])if you read many entries. - Operators abort.
+,-,*abort on shape mismatch; usematmulwhen shapes come from input.
Next steps
- immut API for every method and its cost.
- immut design for value semantics and the Bareiss derivation.
- mutable tutorial for in-place numerical work.
- container tutorial for moving data between representations.