# Copyright 2023 Mattia Giambirtone & All Contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from std/strformat import `&` import std/random randomize() type MatrixOrder* = enum RowMajor, ColumnMajor Matrix*[T] = ref object ## A matrix object data: ref seq[T] # Nim seqs are value types, so this is needed to avoid copies on assignment shape*: tuple[rows, cols: int] order*: MatrixOrder MatrixView*[T] = ref object ## A zero-copy view into a matrix m: Matrix[T] # The matrix that owns the row we point to row: int # The row in the matrix to which we point to # Simple one-line helpers func len*[T](self: Matrix[T]): int {.inline.} = self.data[].len() func len*[T](self: MatrixView[T]): int {.inline.} = self.shape.cols func raw*[T](self: Matrix[T]): ref seq[T] {.inline.} = self.data proc getSize*(shape: tuple[rows, cols: int]): int = ## Helper to get the size required for the ## underlying data array for a matrix of the ## given shape if shape.rows == 0: return shape.cols return shape.cols * shape.rows proc shape*[T](self: MatrixView[T]): tuple[rows, cols: int] = return (0, self.m.shape.cols) proc newMatrix*[T](data: seq[T]): Matrix[T] = ## Initializes a new matrix from a given ## 1D sequence new(result) new(result.data) result.data[] = data result.shape = (rows: 0, cols: len(data)) result.order = RowMajor proc newMatrix*[T](data: seq[seq[T]], order: MatrixOrder = RowMajor): Matrix[T] = ## Initializes a new matrix from a given ## 2D sequence new(result) new(result.data) var temp: seq[T] = @[] result.shape = (rows: len(data), cols: len(data[0])) result.data[] = newSeqOfCap[T](result.shape.getSize()) result.order = order for sub in data: if len(sub) != result.shape.cols: raise newException(ValueError, "invalid shape of input data (mismatching column)") for j in sub: temp.add(j) if order == RowMajor: for j in temp: result.data[].add(j) else: var idx = 0 var col = 0 while col < result.shape.cols: result.data[].add(temp[idx]) idx += result.shape.cols if idx > temp.high(): inc(col) idx = col proc newMatrixFromSeq*[T](data: seq[T], shape: tuple[rows, cols: int], order: MatrixOrder = RowMajor): Matrix[T] = ## Creates a new matrix of the given shape from a flat ## sequence new(result) new(result.data) result.data[] = data result.shape = shape result.order = order proc zeros*[T: int | float](shape: tuple[rows, cols: int], order: MatrixOrder = RowMajor): Matrix[T] = ## Creates a new matrix of the given shape ## filled with zeros new(result) new(result.data) result.data[] = @[] let size = shape.getSize() result.shape = shape when T is int: for _ in 0.. added: result.data[].delete(0) result.shape.rows = 0 result.shape.cols = added result.order = RowMajor proc sum*[T](self: MatrixView[T]): T = ## Returns the sum of all elements ## in the matrix view var i = 0 while i < self.shape.cols: result += self[i] inc(i) proc copy*[T](self: Matrix[T]): Matrix[T] = ## Creates a new copy of the given matrix ## (copies the underlying data!) new(result) new(result.data) result.data[] = self.data[] result.shape = self.shape result.order = self.order proc dup*[T](self: Matrix[T]): Matrix[T] = ## Creates a new shallow copy of the given ## matrix, without copying the underlying ## data new(result) result.data = self.data result.shape = self.shape result.order = self.order proc copy*[T](self: MatrixView[T]): Matrix[T] = ## Creates a new copy of the given matrix ## view. Only the data of the chosen row is ## copied new(result) new(result.data) for e in self: result.data[].add(e) result.shape = self.shape proc dup*[T](self: MatrixView[T]): MatrixView[T] = ## Creates a new shallow copy of the given ## matrix view, without copying the underlying ## data new(result) result.m = self.m result.shape = self.shape result.row = self.row # matrix/scalar operations # Wrappers because builtins are not # procvars func add*[T](a, b: T): T = a + b func sub*[T](a, b: T): T = a - b func mul*[T](a, b: T): T = a * b func divide*[T](a, b: T): T = a / b func neg*[T](a: T): T = -a # Warning: These *all* perform copies of the underlying matrix! proc `+`*[T](a: Matrix[T], b: T): Matrix[T] = a.copy().apply(add, b, axis= -1) proc `+`*[T](a: T, b: Matrix[T]): Matrix[T] = b.copy().apply(add, a, axis= -1) proc `-`*[T](a: Matrix[T], b: T): Matrix[T] = a.copy().apply(sub, b, axis= -1) proc `-`*[T](a: T, b: Matrix[T]): Matrix[T] = b.copy().apply(sub, a, axis= -1) proc `-`*[T](a: Matrix[T]): Matrix[T] = a.copy().apply(neg, a, axis= -1) proc `*`*[T](a: Matrix[T], b: T): Matrix[T] = a.copy().apply(mul, b, axis = -1) proc `*`*[T](a: T, b: Matrix[T]): Matrix[T] = b.copy().apply(mul, a, axis= -1) proc `/`*[T](a: Matrix[T], b: T): Matrix[T] = a.copy().apply(divide, b, axis= -1) proc `/`*[T](a: T, b: Matrix[T]): Matrix[T] = b.copy().apply(divide, a, axis= -1) proc `+`*[T](a: MatrixView[T], b: T): Matrix[T] = a.copy().apply(add, b, axis= -1) proc `+`*[T](a: T, b: MatrixView[T]): Matrix[T] = b.copy().apply(add, a, axis= -1) proc `-`*[T](a: MatrixView[T], b: T): Matrix[T] = a.copy().apply(sub, b, axis= -1) proc `-`*[T](a: T, b: MatrixView[T]): Matrix[T] = b.copy().apply(sub, a, axis= -1) proc `-`*[T](a: MatrixView[T]): Matrix[T] = a.copy().apply(neg, a, axis= -1) proc `*`*[T](a: MatrixView[T], b: T): Matrix[T] = a.copy().apply(mul, b, axis = -1) proc `*`*[T](a: T, b: MatrixView[T]): Matrix[T] = b.copy().apply(mul, a, axis= -1) proc `/`*[T](a: MatrixView[T], b: T): Matrix[T] = a.copy().apply(divide, b, axis= -1) proc `/`*[T](a: T, b: MatrixView[T]): Matrix[T] = b.copy().apply(divide, a, axis= -1) # matrix/matrix operations. They produce a new matrix with the # result of the operation proc `+`*[T](a, b: MatrixView[T]): Matrix[T] = ## Performs the vector sum of the ## given matrix views and returns a new ## vector with the result when not defined(release): if a.shape.cols != b.shape.cols: # Basically if their length is different raise newException(ValueError, &"incompatible argument shapes for addition") new(result) new(result.data) result.shape = a.shape result.order = RowMajor result.data[] = newSeqOfCap[T](result.shape.getSize()) for i in 0.. 0 and b.shape.rows > 0 and a.shape != b.shape: raise newException(ValueError, &"incompatible argument shapes for addition") elif (a.shape.rows == 0 or b.shape.rows == 0) and a.shape.cols != b.shape.cols: raise newException(ValueError, &"incompatible argument shapes for addition") if a.shape.rows == 0 and b.shape.rows == 0: return a[0] + b[0] if a.shape.rows == 0: result = zeros[T](b.shape) for i, row in b: for j, e in (row + a[0])[0]: result[i, j] = e elif b.shape.rows == 0: result = zeros[T](a.shape) for i, row in a: for j, e in (row + b[0])[0]: result[i, j] = e else: result = zeros[T](a.shape) for i, row in a: for j, e in (b[i] + row)[0]: result[i, j] = e proc `-`*[T](a, b: MatrixView[T]): Matrix[T] = ## Performs the vector difference of the ## given matrix views and returns a new ## vector with the result when not defined(release): if a.shape.cols != b.shape.cols: # Basically if their length is different raise newException(ValueError, &"incompatible argument shapes for addition") new(result) new(result.data) result.shape = a.shape result.order = RowMajor result.data[] = newSeqOfCap[T](result.shape.getSize()) for i in 0.. 0 and b.shape.rows > 0 and a.shape != b.shape: raise newException(ValueError, &"incompatible argument shapes for addition") elif (a.shape.rows == 0 or b.shape.rows == 0) and a.shape.cols != b.shape.cols: raise newException(ValueError, &"incompatible argument shapes for addition") if a.shape.rows == 0 and b.shape.rows == 0: return a[0] + b[0] new(result) new(result.data) result.data[] = newSeqOfCap[T](result.shape.getSize()) result.shape = a.shape result.order = RowMajor if result.shape.rows > 1: for row in 0.. 0 and b.shape.rows > 0 and a.shape != b.shape: raise newException(ValueError, &"incompatible argument shapes for multiplication") elif (a.shape.rows == 0 or b.shape.rows == 0) and a.shape.cols != b.shape.cols: raise newException(ValueError, &"incompatible argument shapes for multiplication") if a.shape.rows == 0 and b.shape.rows == 0: return a[0] * b[0] if a.shape.rows == 0: result = zeros[T](b.shape) for i, row in b: for j, e in (row * a[0])[0]: result[i, j] = e elif b.shape.rows == 0: result = zeros[T](a.shape) for i, row in a: for j, e in (row * b[0])[0]: result[i, j] = e else: result = zeros[T](a.shape) for i, row in a: for j, e in (b[i] * row)[0]: result[i, j] = e # Comparison operators. They produce a new matrix of the same # shape as the input(s) and containing boolean values (the result of # the comparison element-wise). Useful for use in where() # matrix/scalar comparisons proc `==`*[T](a: Matrix[T], b: T): Matrix[bool] = new(result) new(result.data) result.shape = a.shape result.data[] = newSeqOfCap[bool](result.shape.getSize()) for e in a.data[]: result.data[].add(e == b) proc `<`*[T](a: Matrix[T], b: T): Matrix[bool] = new(result) new(result.data) result.shape = a.shape result.data[] = newSeqOfCap[bool](result.shape.getSize()) for e in a.data[]: result.data[].add(e < b) proc `>`*[T](a: Matrix[T], b: T): Matrix[bool] = new(result) new(result.data) result.shape = a.shape result.data[] = newSeqOfCap[bool](result.shape.getSize()) for e in a.data[]: result.data[].add(e > b) proc `<=`*[T](a: Matrix[T], b: T): Matrix[bool] = new(result) new(result.data) result.shape = a.shape result.data[] = newSeqOfCap[bool](result.shape.getSize()) for e in a.data[]: result.data[].add(e <= b) proc `>=`*[T](a: Matrix[T], b: T): Matrix[bool] = new(result) new(result.data) result.shape = a.shape result.data[] = newSeqOfCap[bool](result.shape.getSize()) for e in a.data[]: result.data[].add(e >= b) proc `==`*[T](a: MatrixView[T], b: MatrixView[T]): Matrix[bool] = when not defined(release): if a.len() != b.len(): raise newException(ValueError, "invalid shapes for comparison") new(result) new(result.data) result.shape = a.shape result.order = RowMajor result.data[] = newSeqOfCap[bool](result.shape.getSize()) var col = 0 while col < result.shape.cols: result.data[].add(a[col] == b[col]) inc(col) proc `==`*[T](a: Matrix[T], b: MatrixView[T]): Matrix[bool] = when not defined(release): if a.shape.cols != b.len() or a.shape.rows > 0: raise newException(ValueError, "invalid shapes for comparison") return a[0] == b proc diag*[T](a: Matrix[T], k: int = 0): Matrix[T] = ## Returns the kth diagonal of ## the given matrix if a is 2-D ## or a 2-D matrix with a on its ## kth diagonal if it is 1-D if a.shape.rows > 0: if k >= a.shape.cols: return newMatrix[T](@[]) var current = k.ind2sub(a.shape) var res = newSeqOfCap[T](a.shape.getSize()) while current.row < a.shape.rows and current.col < a.shape.cols: res.add(a.data[a.getIndex(current.row, current.col)]) inc(current.row) inc(current.col) result = newMatrix(res) else: let size = len(a) + k result = zeros[T]((size, size)) var current = k.ind2sub(a.shape) for e in a[0]: result[current.row, current.col] = e inc(current.row) inc(current.col) proc diagflat*[T](a: Matrix[T], k: int = 0): Matrix[T] = ## Create a 2-D array with the flattened ## input as a diagonal result = a.flatten().diag(k) proc fliplr*[T](self: Matrix[T]): Matrix[T] = ## Flips each row in the matrix left ## to right. A copy is returned new(result) result.shape = self.shape result.order = self.order new(result.data) result.data[] = newSeqOfCap[T](self.shape.getSize()) for row in self: for i in countdown(row.len() - 1, 0, 1): result.data[].add(row[i]) proc `==`*[T](a, b: Matrix[T]): Matrix[bool] = when not defined(release): if a.shape != b.shape: raise newException(ValueError, "can't compare matrices of different shapes") new(result) new(result.data) result.shape = a.shape result.order = RowMajor result.data[] = newSeqOfCap[bool](result.shape.getSize()) if a.shape.rows == 0: result = a[0] == b[0] for r in 0..`*[T](a, b: Matrix[T]): Matrix[bool] = when not defined(release): if a.shape != b.shape: raise newException(ValueError, "can't compare matrices of different shapes") new(result) new(result.data) result.shape = a.shape result.order = RowMajor result.data[] = newSeqOfCap[bool](result.shape.getSize()) if a.shape.rows == 0: result = a[0] > b[0] for r in 0.. b[r, c]) proc `>=`*[T](a, b: Matrix[T]): Matrix[bool] = when not defined(release): if a.shape != b.shape: raise newException(ValueError, "can't compare matrices of different shapes") new(result) new(result.data) result.shape = a.shape result.order = RowMajor result.data[] = newSeqOfCap[bool](result.shape.getSize()) if a.shape.rows == 0: result = a[0] >= b[0] for r in 0..= b[r, c]) proc `<=`*[T](a, b: Matrix[T]): Matrix[bool] = when not defined(release): if a.shape != b.shape: raise newException(ValueError, "can't compare matrices of different shapes") new(result) new(result.data) result.shape = a.shape result.order = RowMajor result.data[] = newSeqOfCap[bool](result.shape.getSize()) if a.shape.rows == 0: result = a[0] <= b[0] for r in 0.. a proc `*`*[T](a: Matrix[T], b: MatrixView[T]): Matrix[T] = b * a proc `==`*[T](a: T, b: Matrix[T]): Matrix[bool] = b == a proc `==`*[T](a: MatrixView[T], b: Matrix[T]): Matrix[bool] = b == a proc toRowMajor*[T](self: Matrix[T], copy: bool = true): Matrix[T] = ## Converts a column-major matrix to a ## row-major one. Returns a copy unless ## copy equals false if self.order == RowMajor: return self if copy: result = self.copy() else: result = self result.order = RowMajor for row in self: for element in row: self.data[].add(element) proc toColumnMajor*[T](self: Matrix[T], copy: bool = true): Matrix[T] = ## Converts a row-major matrix to a ## column-major one if self.order == ColumnMajor: return self if copy: result = self.copy() else: result = self self.order = ColumnMajor let orig = self.data[] self.data[] = @[] var idx = 0 var col = 0 while col < self.shape.cols: self.data[].add(orig[idx]) idx += self.shape.cols if idx > orig.high(): inc(col) idx = col result = self # Matrices and matrix views are iterable! iterator items*[T](self: Matrix[T]): MatrixView[T] = if self.len() > 0: for row in 0.. 0: for column in 0.. 0: return $(self[0]) result &= "[" for i, row in self: result &= "[" for j, e in row: result &= $e if j < self.shape.cols - 1: result &= ", " if i < self.shape.rows - 1: result &= "], \n" result &= " " else: result &= "]" result &= "]" proc dot*[T](self, other: Matrix[T]): Matrix[T] = ## Computes the dot product of the two ## input matrices if self.shape.rows > 1 and other.shape.rows > 1: when not defined(release): if self.shape.rows != other.shape.cols: raise newException(ValueError, &"incompatible argument shapes for dot product") result = zeros[T]((self.shape.rows, other.shape.cols)) var other = other.transpose() for i in 0.. 1: when not defined(release): if self.shape.cols != other.shape.cols: raise newException(ValueError, &"incompatible argument shapes for dot product") result = zeros[T]((0, self.shape.rows)) for i in 0.. 1: return other.transpose().dot(self) else: return self * other proc dot*[T](self: MatrixView[T], other: Matrix[T]): Matrix[T] = ## Computes the dot product of the two ## input matrices when not defined(release): if self.shape.cols != other.shape.cols: raise newException(ValueError, &"incompatible argument shapes for dot product") result = zeros[T]((0, self.shape.rows)) for i in 0.. 10, y) works as expected proc where*[T](self: Matrix[T], cond: Matrix[bool], other: Matrix[T]): Matrix[T] {.inline.} = cond.where(self, other) proc where*[T](self: Matrix[T], cond: Matrix[bool], other: T): Matrix[T] {.inline.} = cond.where(self, other) proc max*[T](self: Matrix[T]): T = ## Returns the largest element ## into the matrix var m: T = self[0, 0] for row in self: for element in row: if m < element: m = element return m proc argmax*[T](self: Matrix[T]): int = ## Returns the index of largest element ## into the matrix var m: T = self[0, 0] var row = 0 col = 0 while row < self.shape.rows: while col < self.shape.cols: if self[row, col] > m: m = self[row, col] if self.shape.rows == 0: while col < self.shape.cols: if self[0, col] > m: m = self[0, col] inc(col) return self.getIndex(row, col) proc contains*[T](self: Matrix[T], e: T): bool = ## Returns whether the matrix contains ## the element e for row in self: for element in row: if element == e: return true return false proc count*[T](self: Matrix[T], e: T): int = ## Returns the number of occurrences ## of e in self for row in self: for k in row: if k == e: inc(result) proc replace*[T](self: Matrix[T], other: Matrix[T], copy: bool = false) = ## Replaces the data in self with the data from ## other (a copy is not performed unless copy equals ## true) if copy: self.data[] = other.data[] else: self.data = other.data self.order = other.order self.shape = other.shape when isMainModule: import math proc pow(a, b: int): int = return a ^ b var m = newMatrix[int](@[@[1, 2, 3], @[4, 5, 6]]) var k = m.transpose() doAssert k[2, 1] == m[1, 2], "transpose mismatch" doAssert all(m.transpose() == k), "transpose mismatch" doAssert k.sum() == m.sum(), "element sum mismatch" doAssert all(k.sum(axis=1) == m.sum(axis=0)), "sum over axis mismatch" doAssert all(k.sum(axis=0) == m.sum(axis=1)), "sum over axis mismatch" var y = newMatrix[int](@[1, 2, 3, 4]) doAssert y.sum() == 10, "element sum mismatch" doAssert (y + y).sum() == 20, "matrix sum mismatch" doAssert all(m + m == m * 2), "m + m != m * 2" var z = newMatrix[int](@[1, 2, 3]) doAssert (m * z).sum() == 46, "matrix multiplication mismatch" doAssert all(z * z == z.apply(pow, 2, axis = -1, copy=true)), "matrix multiplication mismatch" var x = newMatrix[int](@[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) doAssert (x < 5).where(x, x * 10).sum() == 360, "where mismatch" doAssert all((x < 5).where(x, x * 10) == x.where(x < 5, x * 10)), "where mismatch" doAssert x.max() == 9, "max mismatch" doAssert x.argmax() == 10, "argmax mismatch" doAssert all(newMatrix[int](@[12, 23]).dot(newMatrix[int](@[@[11, 22], @[33, 44]])) == newMatrix[int](@[891, 1276])), "dot product mismatch" doAssert all(newMatrix[int](@[@[1, 2, 3], @[2, 3, 4]]).dot(newMatrix[int](@[1, 2, 3])) == newMatrix[int](@[14, 20])), "dot product mismatch" doAssert all(m.diag() == newMatrix[int](@[1, 5])), "diagonal mismatch" doAssert all(m.diag(1) == newMatrix[int](@[2, 6])), "diagonal mismatch" doAssert all(m.diag(2) == newMatrix[int](@[3])), "diagonal mismatch" doAssert m.diag(3).len() == 0, "diagonal mismatch" var j = m.fliplr() doAssert all(j.diag() == newMatrix[int](@[3, 5])), "diagonal mismatch" doAssert all(j.diag(1) == newMatrix[int](@[2, 4])), "diagonal mismatch" doAssert all(j.diag(2) == newMatrix[int](@[1])), "diagonal mismatch" doAssert j.diag(3).len() == 0, "diagonal mismatch" var o = newMatrix[int](@[1, 2, 3]) doAssert all(o.diag() == newMatrix[int](@[@[1, 0, 0], @[0, 2, 0], @[0, 0, 3]])), "diagonal mismatch" var n = newMatrix[int](@[@[1, 2], @[3, 4]]) doAssert all(n.diagflat() == newMatrix[int](@[@[1, 0, 0, 0], @[0, 2, 0, 0], @[0, 0, 3, 0], @[0, 0, 0, 4]])), "diagflat mismatch" doAssert (newMatrix[int](@[1, 2, 3]) + newMatrix[int](@[@[1, 2], @[3, 4], @[5, 6]]).transpose()).sum() == 33, "matrix sum mismatch" doAssert (newMatrix[int](@[@[1, 2], @[3, 4], @[5, 6]]).transpose() + newMatrix[int](@[1, 2, 3])).sum() == 33, "matrix sum mismatch"