defmodule Matrix do @moduledoc """ *Matrix* is a linear algebra library for manipulating dense matrices. Its primary design goal is ease of use. It is desirable that the *Matrix* package interact with standard Elixir language constructs and other packages. The underlying storage mechanism is, therefore, Elixir lists. A secondary design consideration is for the module to be reasonably efficient in terms of both memory usage and computations. Unfortunately there is a trade off between memory efficiency and computational efficiency. Where these requirements conflict *Matrix* will use the more computationally efficient algorithm. Each matrix is represented as a "list of lists" whereby a 3x4 matrix is represented by a list of three items, with each item a list of 4 values. Constructors are provided for several common matrix configurations including zero filled, one filled, random filled the identity matrix, etc. ## Examples iex> Matrix.new(3, 4) [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]] iex> Matrix.ident(4) [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]] """ @vsn 1 @doc """ A list of values representing a matrix row. """ @type row :: [number] @type matrix :: [row] @comparison_epsilon 1.0e-12 @comparison_max_ulp 1 @doc """ Returns a new matrix of the specified size (number of rows and columns). All elements of the matrix are filled with the supplied value "val" (default 0). #### See also [ones/2](#ones/2), [rand/2](#rand/2), [zeros/2](#zeros/2) #### Examples iex> Matrix.new(3, 4) [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]] iex> Matrix.new(2, 3, -10) [[-10, -10, -10], [-10, -10, -10]] """ @spec new(integer, integer, number) :: matrix def new(rows, cols, val \\ 0) do for _r <- 1..rows, do: make_row(cols,val) end def make_row(0, _val), do: [] def make_row(n, val), do: [val] ++ make_row(n-1, val) @doc """ Returns a new matrix of the specified size (number of rows and columns) whose elements are sequential starting at 1 and increasing across the row. #### See also [new/3](#new/3), [ones/2](#ones/2), [rand/2](#rand/2), [zeros/2](#zeros/2) #### Examples iex> Matrix.seq(3,2) [[1, 2], [3, 4], [5, 6]] """ @spec seq(integer, integer) :: matrix def seq(rows, cols) do for r <- 1..rows, do: for c <- 1..cols, do: (r-1)*cols + c end @doc """ Returns a new matrix of the specified size (number of rows and columns). All elements of the matrix are filled with uniformly distributed random numbers between 0 and 1. #### Examples iex> _ = :random.seed(12345) iex> Matrix.rand(3,3) [[0.07797290969719865, 0.3944785128151924, 0.9781224924937147], [1.3985610037403617e-4, 0.5536761216397539, 0.35476183770551284], [0.7021763747372531, 0.5537966721193639, 0.1607491687700906]] #### See also [new/3](#new/3), [ones/2](#ones/2), [zeros/2](#zeros/2) """ @spec rand(integer, integer) :: matrix def rand(rows, cols) do for _r <- 1..rows, do: make_random_row(cols) end def make_random_row(0), do: [] def make_random_row(n), do: [:random.uniform] ++ make_random_row(n-1) @doc """ Returns a new matrix of the specified size (number of rows and columns). All elements of the matrix are filled with the zeros. #### See also [new/3](#new/3), [ones/2](#ones/2), [rand/2](#rand/2) #### Examples iex> Matrix.zeros(3, 4) [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]] """ @spec zeros(integer, integer) :: matrix def zeros(rows, cols), do: new(rows, cols, 0) @doc """ Returns a new matrix of the specified size (number of rows and columns). All elements of the matrix are filled with the ones. #### See also [new/3](#new/3), [rand/2](#rand/2), [zeros/2](#zeros/2) #### Examples iex> Matrix.ones(3, 4) [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]] """ @spec ones(integer, integer) :: matrix def ones(rows, cols), do: new(rows, cols, 1) @doc """ Returns a new square "diagonal" matrix whose elements are zero except for the diagonal. The diagonal elements will be composed of the supplied list #### See also [new/3](#new/3), [ones/2](#ones/2), [ident/1](#ident/1) #### Examples iex> Matrix.diag([1,2,3]) [[1, 0, 0], [0, 2, 0], [0, 0, 3]] """ @spec diag([number]) :: matrix def diag(d) do rows = length(d) Enum.zip( d, 0..rows-1 ) |> Enum.map(fn({v,s})-> row = [v]++Matrix.make_row(rows-1,0) rrotate(row, s) end) end def lrotate(list, 0), do: list def lrotate([head|list], number), do: lrotate(list ++ [head], number - 1) def rrotate(list, number), do: list |> Enum.reverse |> lrotate(number) |> Enum.reverse @doc """ Returns a new "identity" matrix of the specified size. The identity is defined as a square matrix with ones on the diagonal and zeros in all off-diagonal elements. Since the matrix is square only a single size parameter is required. #### See also [diag/1](#diag/1), [ones/2](#ones/2), [rand/2](#rand/2) #### Examples iex> Matrix.ident(3) [[1, 0, 0], [0, 1, 0], [0, 0, 1]] """ @spec ident(integer) :: matrix def ident(rows), do: diag(make_row(rows,1)) @doc """ Returns the size (dimensions) of the supplied matrix. The return value is a tuple of the dimensions of the matrix as {rows,cols}. #### See also [new/3](#new/3), [ones/2](#ones/2), [rand/2](#rand/2) #### Examples iex> Matrix.size( Matrix.new(3,4) ) {3, 4} """ @spec size(matrix) :: {integer,integer} def size(x) do rows = length(x) cols = length( List.first(x) ) {rows, cols} end @doc """ Returns a matrix that is a copy of the supplied matrix (x) with the specified element (row and column) set to the specified value (val). The row and column indices are zero-based. #### See also [elem/3](#elem/3) #### Examples iex> Matrix.set( Matrix.ident(3), 0,0, -1) [[-1, 0, 0], [0, 1, 0], [0, 0, 1]] """ @spec set(matrix, integer, integer, number) :: matrix def set(x, row, col, val) do row_vals = Enum.at(x,row) new_row = List.replace_at(row_vals,col,val) List.replace_at(x, row, new_row) end @doc """ Returns the value of the specified element (row and column) of the given matrix (x). The row and column indices are zero-based. #### See also [set/4](#set/4) #### Examples iex> Matrix.elem( Matrix.ident(3), 0,0 ) 1 """ @spec elem(matrix, integer, integer) :: number def elem(x, row, col) do Enum.at( Enum.at(x,row), col ) end @doc """ Returns a new matrix whose elements are the sum of the elements of the provided matrices. #### See also [sub/2](#sub/2), [emult/2](#emult/2) #### Examples iex> Matrix.add( Matrix.ident(3), Matrix.ident(3) ) [[2, 0, 0], [0, 2, 0], [0, 0, 2]] """ @spec add(matrix, matrix) :: matrix def add(x, y) do Enum.zip(x, y) |> Enum.map( fn({a,b})->add_rows(a,b) end ) end @doc """ Returns a new matrix whose elements are the difference (subtraction) of the elements of the provided matrices. #### See also [add/2](#add/2), [emult/2](#emult/2) #### Examples iex> Matrix.sub( Matrix.ident(3), Matrix.ones(3,3) ) [[0, -1, -1], [-1, 0, -1], [-1, -1, 0]] """ @spec sub(matrix, matrix) :: matrix def sub(x, y) do Enum.zip(x, y) |> Enum.map( fn({a,b})->subtract_rows(a,b) end ) end @doc """ Returns a new matrix whose elements are the element-by-element multiply of the elements of the provided matrices. Note that this is not the linear algebra matrix multiply. #### See also [add/2](#add/2), [sub/2](#sub/2) #### Examples iex> Matrix.emult( Matrix.new(3,3,2), Matrix.new(3,3,-2) ) [[-4, -4, -4], [-4, -4, -4], [-4, -4, -4]] """ @spec emult(matrix, matrix) :: matrix def emult(x, y) do Enum.zip(x, y) |> Enum.map( fn({a,b})->emult_rows(a,b) end ) end @doc """ Returns a new matrix which is the linear algebra matrix multiply of the provided matrices. #### See also [emult/2](#emult/2) #### Examples iex> Matrix.mult( Matrix.seq(2,2), Matrix.seq(2,2) ) [[7, 10], [15, 22]] """ @spec mult(matrix, matrix) :: matrix def mult(x, y) do trans_y = transpose(y) Enum.map(x, fn(row)-> Enum.map(trans_y, &dot_product(row, &1)) end) end defp dot_product(r1, _r2) when r1 == [], do: 0 defp dot_product(r1, r2) do [h1|t1] = r1 [h2|t2] = r2 (h1*h2) + dot_product(t1, t2) end @doc """ Returns a new matrix whose elements are the transpose of the supplied matrix. The transpose essentially swaps rows for columns - that is, the first row becomes the first column, the second row becomes the second column, etc. #### Examples iex> Matrix.transpose( Matrix.seq(3,2) ) [[1, 3, 5], [2, 4, 6]] """ @spec transpose(matrix) :: matrix def transpose(m) do swap_rows_cols(m) end defp swap_rows_cols( [h|_t] ) when h==[], do: [] defp swap_rows_cols(rows) do firsts = Enum.map(rows, fn(x) -> hd(x) end) # first element of each row rest = Enum.map(rows, fn(x) -> tl(x) end) # remaining elements of each row [firsts | swap_rows_cols(rest)] end @doc """ Returns a new matrix which is the (linear algebra) inverse of the supplied matrix. If the supplied matrix is "x" then, by definition, x * inv(x) = I where I is the identity matrix. This function uses a brute force Gaussian elimination so it is not expected to be terribly fast. #### Examples iex> x = Matrix.rand(5,5) iex> res = Matrix.mult( x, Matrix.inv(x) ) iex> Matrix.almost_equal(res,[[1,0,0],[0,1,0],[0,0,1]]) true """ @spec inv(matrix) :: matrix def inv(x) do {rows,_cols} = size(x) y = row_reduce( supplement(x,ident(rows)) ) {yl,yr} = desupplement(y) z = supplement( full_flip(yl), full_flip(yr) ) {_,zzr} = desupplement( row_reduce(z) ) full_flip(zzr) end @doc """ Returns a new matrix whose elements are identical to the supplied matrix x but with the supplied value appended to the beginning of each row. #### See also [postfix_row/2](#postfix_rows/2) #### Examples iex> Matrix.prefix_rows( Matrix.seq(2,2), 10 ) [[10, 1, 2], [10, 3, 4]] """ @spec prefix_rows(matrix, number) :: matrix def prefix_rows(x, val) do Enum.map(x, fn(r) -> [val]++r end) end @doc """ Returns a new matrix whose elements are identical to the supplied matrix x but with the supplied value appended to the end of each row. #### See also [prefix_rows/2](#prefix_rows/2) #### Examples iex> Matrix.postfix_rows( Matrix.seq(2,2), 10 ) [[1, 2, 10], [3, 4, 10]] """ @spec postfix_rows(matrix, number) :: matrix def postfix_rows(x, val) do Enum.map(x, fn(r) -> r++[val] end) end @doc """ Compares two matrices as being (approximately) equal. Since floating point numbers have slightly different representations and accuracies on different architectures it is generally not a good idea to compare them directly. Rather numbers are considered equal if they are within an "epsilon" of each other. *almost_equal* compares all elements of two matrices, returning true if all elements are within the provided epsilon. #### Examples iex> Matrix.almost_equal( [[1, 0], [0, 1]], [[1,0], [0,1+1.0e-12]] ) false iex> Matrix.almost_equal( [[1, 0], [0, 1]], [[1,0], [0,1+0.5e-12]] ) true """ @spec almost_equal(matrix, matrix) :: matrix def almost_equal(x, y) do Enum.zip(x,y) |> Enum.map(fn({r1,r2})->rows_almost_equal(r1, r2) end) |> Enum.all? end @doc """ Returns a new matrix whose elements are the elements of matrix x multiplied by the scale factor "s". #### Examples iex> Matrix.scale( Matrix.ident(3), 2 ) [[2,0,0], [0,2,0], [0,0,2]] iex> Matrix.scale( Matrix.ones(3,4), -2 ) [[-2, -2, -2, -2], [-2, -2, -2, -2], [-2, -2, -2, -2]] """ @spec scale(matrix, number) :: matrix def scale(x, s) do Enum.map(x, fn(r)->scale_row(r,s) end) end @doc """ Returns a string which is a "pretty" representation of the supplied matrix. """ @spec pretty_print(matrix, char_list, char_list) :: atom def pretty_print(m, fmt\\"%d", sep\\"") do str = m |> Enum.map(fn(r)->show_row(r,fmt,sep) <> "\n" end) |> Enum.join("") IO.puts(str) end @doc """ Returns the Kronecker tensor product of two matrices A and B. If A is an MxN and B is PxQ, then the returned matrix is an (M*P)x(N*Q) matrix formed by taking all possible products between the elements of A and the matrix B.
A = |1000| B = | 1 -1|
|0100| | -1 1|
|0010|
|0001|
then kron(A,B) = | 1 -1 0 0 0 0 0 0|
| -1 1 0 0 0 0 0 0|
| 0 0 1 -1 0 0 0 0|
| 0 0 -1 1 0 0 0 0|
| 0 0 0 0 1 -1 0 0|
| 0 0 0 0 -1 1 0 0|
| 0 0 0 0 0 0 1 -1|
| 0 0 0 0 0 0 -1 1|
"""
@spec kron(matrix, matrix) :: matrix
def kron([], _b), do: []
def kron([h|t], b) do
row_embed(h, b, [])++kron(t, b)
end
#################
# Private supporting functions
#################
#
# These functions apply a specific math operation to all the elements of the
# supplied rows. They are used by the math routine (e.g., add). They call the
# recursive element adding functions to continually append new elements to
# the row. Note that this was found to be faster than a simple list
# comprehension or Enum.map, at least using Erlang/OTP 18 and Elixir 1.1.1
#
defp add_rows(r1, r2) when r1 == [] or r2 == [], do: []
defp add_rows(r1, r2) do
[h1|t1] = r1
[h2|t2] = r2
[h1+h2] ++ add_rows(t1,t2)
end
defp subtract_rows(r1, r2) when r1 == [] or r2 == [], do: []
defp subtract_rows(r1, r2) do
[h1|t1] = r1
[h2|t2] = r2
[h1-h2] ++ subtract_rows(t1,t2)
end
defp emult_rows(r1, r2) when r1 == [] or r2 == [], do: []
defp emult_rows(r1, r2) do
[h1|t1] = r1
[h2|t2] = r2
[h1*h2] ++ emult_rows(t1,t2)
end
#
# Support function for matrix inverse. "Supplement" the supplied matrix (x)
# by appending the matrix y to the right of it. Generally used to make,
# for example,
# x = |1 2|
# |3 4|
# supplemented with the 2x2 identity matrix becomes
# x = |1 2 1 0|
# |3 4 0 1|
#
defp supplement([],y), do: y
defp supplement(x,y) do
Enum.zip(x,y)
|> Enum.map(fn({r1,r2}) -> r1++r2 end)
end
#
# Inverse of the "supplement" function. This breaks the matrix apart into
# a left and right part - returned as a tuple. For example for
# x = |1 2 1 0|
# |3 4 0 1|
# desupplement returns
# { |1 2| |1 0|
# |3 4| |0 1| }
#
defp desupplement(x) do
{_rows,cols} = size(x)
left = Enum.map(x, fn(r) -> elem(Enum.split(r,round(cols/2)),0) end)
right = Enum.map(x, fn(r) -> elem(Enum.split(r,round(cols/2)),1) end)
{left,right}
end
#
# Multiplies a row by a (scalar) constant.
#
defp scale_row(r, v) do
Enum.map(r, fn(x) -> x * v end)
end
#
# Uses elementary row operations to reduce the supplied matrix to row echelon
# form. This is the first step of matrix inversion using Gaussian elimination.
#
defp row_reduce([]), do: []
defp row_reduce(rows) do
firsts = Enum.map(rows, fn(x) -> hd(x) end) # first element of each row
s = hd(firsts)
y = if abs(s)<1.0e-10 do
scale_row(hd(rows), 1)
else
scale_row(hd(rows), 1/s)
end
first_rest = Enum.map(tl(rows), fn(x) -> hd(x) end)
z = Enum.zip(tl(rows),first_rest)
|> Enum.map(fn({r,v}) ->
if abs(v)<1.0e-10 do
tl(r)
else
tl(subtract_rows(scale_row(r,1/v),y))
end
end)
[y] ++ prefix_rows(row_reduce(z), 0)
end
#
# Used in the "inv" function. This function flips the supplied matrix top
# to bottom and left to right. It is used after the first reduction to
# reduced echelon form to allow for recursive back substitution to get the
# inverse.
#
defp full_flip(x) do
Enum.reverse( Enum.map( x, fn(r)->Enum.reverse(r) end ) )
end
#
# Embed the supplied matrix into a "row" matrix. Each instance of the embedded
# matrix is scaled by the element of r
#
defp row_embed([], _b, macc), do: macc
defp row_embed(r, b, macc) do
s = hd(r)
row_embed( tl(r), b, supplement(macc,scale(b,s)) )
end
#
# Pretty prints the values in a supplied row.
#
defp show_row(r, fmt, sep) do
str = r
|> Enum.map(fn(e)->ExPrintf.sprintf(fmt, [e]) end)
|> Enum.join(sep)
"|" <> str <> "|"
end
#
# The following functions are used for floating point comparison of matrices.
#
# Compares two rows as being (approximately) equal.
defp rows_almost_equal(r1, r2) do
x = Enum.zip(r1,r2)
|> Enum.map(fn({x,y})->vals_almost_equal(x, y) end)
Enum.all?(x)
end
# Compares two floats as being (approximately) equal.
defp vals_almost_equal(x, y) do
close_enough?(x, y, @comparison_epsilon, @comparison_max_ulp)
end
@doc """
Code borrowed from the ExMath library and duplicated here to reduce
dependencies. ExMath is copyright © 2015 Ookami Kenrou