Chris@5: Chris@94: module yetilab.matrix.matrix; Chris@94: Chris@214: // A matrix is an array of vectors. Chris@94: Chris@101: // A matrix can be stored in either column-major (the default) or Chris@101: // row-major format. Storage order is an efficiency concern only: Chris@101: // every API function operating on matrix objects will return the same Chris@101: // result regardless of storage order. (The transpose function just Chris@101: // switches the row/column order without moving the elements.) Chris@18: Chris@214: //!!! check that we are not unnecessarily copying in the transform functions Chris@214: Chris@222: vec = load yetilab.vector.vector; Chris@222: bf = load yetilab.vector.blockfuncs; Chris@9: Chris@222: load yetilab.vector.vectortype; Chris@195: load yetilab.matrix.matrixtype; Chris@195: Chris@208: size m = Chris@208: case m of Chris@234: DenseRows r: Chris@208: major = length r; Chris@208: { Chris@208: rows = major, Chris@214: columns = if major > 0 then vec.length r[0] else 0 fi, Chris@208: }; Chris@234: DenseCols c: Chris@208: major = length c; Chris@208: { Chris@214: rows = if major > 0 then vec.length c[0] else 0 fi, Chris@208: columns = major, Chris@208: }; Chris@254: SparseRows { size, data }: size; Chris@254: SparseCols { size, data }: size; Chris@208: esac; Chris@208: Chris@208: width m = (size m).columns; Chris@208: height m = (size m).rows; Chris@208: Chris@249: nonZeroValues m = Chris@254: (nzDense d = Chris@242: sum Chris@242: (map do v: Chris@242: sum (map do n: if n == 0 then 0 else 1 fi done (vec.list v)) Chris@242: done d); Chris@254: nzSparse d = Chris@254: sum (map do k: Chris@254: length (keys d.data[k]) Chris@254: done (keys d.data)); Chris@242: case m of Chris@254: DenseRows d: nzDense d; Chris@254: DenseCols d: nzDense d; Chris@254: SparseRows d: nzSparse d; Chris@254: SparseCols d: nzSparse d; Chris@242: esac); Chris@242: Chris@249: density m = Chris@249: ({ rows, columns } = size m; Chris@249: cells = rows * columns; Chris@249: (nonZeroValues m) / cells); Chris@249: Chris@254: fromSlice n m { data } = Chris@254: if n in data and m in data[n] then data[n][m] else 0 fi; Chris@236: Chris@254: filledSlice sz n { data } = Chris@254: (slice = new double[sz]; Chris@254: if n in data then Chris@254: h = data[n]; Chris@254: for (keys h) do k: slice[k] := h[k] done; Chris@254: fi; Chris@254: vec.vector slice); Chris@236: Chris@208: getAt row col m = Chris@208: case m of Chris@234: DenseRows rows: r = rows[row]; vec.at col r; Chris@234: DenseCols cols: c = cols[col]; vec.at row c; Chris@254: SparseRows data: fromSlice row col data; Chris@254: SparseCols data: fromSlice col row data; Chris@208: esac; Chris@208: Chris@208: getColumn j m = Chris@208: case m of Chris@234: DenseCols cols: cols[j]; Chris@254: SparseCols data: filledSlice data.size.rows j data; Chris@236: _: vec.fromList (map do i: getAt i j m done [0..height m - 1]); Chris@208: esac; Chris@208: Chris@208: getRow i m = Chris@208: case m of Chris@234: DenseRows rows: rows[i]; Chris@254: SparseRows data: filledSlice data.size.columns i data; Chris@236: _: vec.fromList (map do j: getAt i j m done [0..width m - 1]); Chris@208: esac; Chris@208: Chris@208: isRowMajor? m = Chris@208: case m of Chris@234: DenseRows _: true; Chris@234: DenseCols _: false; Chris@254: SparseRows _: true; Chris@254: SparseCols _: false; Chris@234: esac; Chris@234: Chris@234: isSparse? m = Chris@234: case m of Chris@234: DenseRows _: false; Chris@234: DenseCols _: false; Chris@254: SparseRows _: true; Chris@254: SparseCols _: true; Chris@208: esac; Chris@94: Chris@244: newColumnMajorStorage { rows, columns } = Chris@97: if rows < 1 then array [] Chris@214: else array (map \(vec.zeros rows) [1..columns]) Chris@97: fi; Chris@94: Chris@98: zeroMatrix { rows, columns } = Chris@244: DenseCols (newColumnMajorStorage { rows, columns }); Chris@201: Chris@201: zeroMatrixWithTypeOf m { rows, columns } = Chris@208: if isRowMajor? m then Chris@244: DenseRows (newColumnMajorStorage { rows = columns, columns = rows }); Chris@201: else Chris@244: DenseCols (newColumnMajorStorage { rows, columns }); Chris@201: fi; Chris@5: Chris@214: zeroSizeMatrix () = zeroMatrix { rows = 0, columns = 0 }; Chris@214: Chris@98: generate f { rows, columns } = Chris@214: if rows < 1 or columns < 1 then zeroSizeMatrix () Chris@214: else Chris@214: m = array (map \(new double[rows]) [1..columns]); Chris@214: for [0..columns-1] do col: Chris@214: for [0..rows-1] do row: Chris@214: m[col][row] := f row col; Chris@214: done; Chris@5: done; Chris@234: DenseCols (array (map vec.vector m)) Chris@214: fi; Chris@5: Chris@235: enumerateSparse m = Chris@254: (enumerate { size, data } = Chris@240: concat Chris@240: (map do i: Chris@254: map do j: Chris@254: { i, j, v = data[i][j] } Chris@254: done (keys data[i]) Chris@254: done (keys data)); Chris@235: case m of Chris@254: SparseCols d: Chris@240: map do { i, j, v }: { i = j, j = i, v } done (enumerate d); Chris@254: SparseRows d: Chris@240: enumerate d; Chris@237: _: []; Chris@235: esac); Chris@235: Chris@254: makeSparse type size entries = Chris@254: (isRow = case type of RowMajor (): true; ColumnMajor (): false; esac; Chris@254: data = [:]; Chris@254: preprocess = Chris@254: if isRow then id Chris@254: else do { i, j, v }: { i = j, j = i, v } done Chris@238: fi; Chris@254: for (map preprocess entries) do { i, j, v }: Chris@254: if not (i in data) then data[i] := [:] fi; Chris@254: data[i][j] := v; Chris@254: done; Chris@254: if isRow then SparseRows else SparseCols fi { size, data }); Chris@238: Chris@243: toSparse m = Chris@238: if isSparse? m then m Chris@238: else Chris@238: { rows, columns } = size m; Chris@243: enumerate m ii jj = Chris@238: case ii of Chris@238: i::irest: Chris@238: case jj of Chris@238: j::rest: Chris@238: v = getAt i j m; Chris@243: if v != 0 then { i, j, v } :. \(enumerate m ii rest) Chris@243: else enumerate m ii rest Chris@238: fi; Chris@243: _: enumerate m irest [0..columns-1]; Chris@238: esac; Chris@238: _: []; Chris@238: esac; Chris@238: makeSparse Chris@244: if isRowMajor? m then RowMajor () else ColumnMajor () fi Chris@238: (size m) Chris@243: (enumerate m [0..rows-1] [0..columns-1]); Chris@238: fi; Chris@238: Chris@238: toDense m = Chris@238: if not (isSparse? m) then m Chris@238: elif isRowMajor? m then Chris@238: DenseRows (array (map do row: getRow row m done [0..height m - 1])); Chris@238: else Chris@238: DenseCols (array (map do col: getColumn col m done [0..width m - 1])); Chris@238: fi; Chris@235: Chris@20: constMatrix n = generate do row col: n done; Chris@20: randomMatrix = generate do row col: Math#random() done; Chris@5: identityMatrix = constMatrix 1; Chris@5: Chris@100: transposed m = Chris@208: case m of Chris@234: DenseRows d: DenseCols d; Chris@234: DenseCols d: DenseRows d; Chris@254: SparseRows { data, size }: SparseCols Chris@254: { data, size = { rows = size.columns, columns = size.rows } }; Chris@254: SparseCols { data, size }: SparseRows Chris@254: { data, size = { rows = size.columns, columns = size.rows } }; Chris@208: esac; Chris@100: Chris@254: sparseFlipped m = Chris@254: ({ tagger, data } = Chris@254: case m of Chris@254: SparseCols { data }: { tagger = SparseRows, data }; Chris@254: SparseRows { data }: { tagger = SparseCols, data }; Chris@254: _: failWith "sparseFlipped called for non-sparse matrix"; Chris@254: esac; Chris@254: data' = [:]; Chris@254: for (keys data) do i: Chris@254: for (keys data[i]) do j: Chris@254: if not (j in data') then data'[j] := [:] fi; Chris@254: data'[j][i] := data[i][j]; Chris@254: done Chris@254: done; Chris@254: tagger { size = size m, data = data' }); Chris@254: Chris@100: flipped m = Chris@235: if isSparse? m then Chris@254: sparseFlipped m Chris@100: else Chris@235: if isRowMajor? m then Chris@235: generate do row col: getAt row col m done (size m); Chris@235: else Chris@235: transposed Chris@235: (generate do row col: getAt col row m done Chris@235: { rows = (width m), columns = (height m) }); Chris@235: fi Chris@100: fi; Chris@100: Chris@161: toRowMajor m = Chris@208: if isRowMajor? m then m else flipped m fi; Chris@161: Chris@161: toColumnMajor m = Chris@208: if not isRowMajor? m then m else flipped m fi; Chris@161: Chris@238: equal'' comparator vecComparator m1 m2 = Chris@254: // Prerequisite: m1 and m2 have same size, sparse-p, and storage order Chris@241: (compareVecLists vv1 vv2 = all id (map2 vecComparator vv1 vv2); Chris@238: compareSparse d1 d2 = Chris@254: (data1 = d1.data; Chris@254: data2 = d2.data; Chris@254: keys data1 == keys data2 and Chris@254: all id Chris@254: (map do i: Chris@254: keys data1[i] == keys data2[i] and Chris@254: all id Chris@254: (map do j: Chris@254: comparator data1[i][j] data2[i][j] Chris@254: done (keys data1[i])) Chris@254: done (keys data1))); Chris@238: case m1 of Chris@238: DenseRows d1: Chris@238: case m2 of DenseRows d2: compareVecLists d1 d2; _: false; esac; Chris@238: DenseCols d1: Chris@238: case m2 of DenseCols d2: compareVecLists d1 d2; _: false; esac; Chris@254: SparseRows d1: Chris@254: case m2 of SparseRows d2: compareSparse d1 d2; _: false; esac; Chris@254: SparseCols d1: Chris@254: case m2 of SparseCols d2: compareSparse d1 d2; _: false; esac; Chris@238: esac); Chris@238: Chris@238: equal' comparator vecComparator m1 m2 = Chris@226: if size m1 != size m2 then Chris@226: false Chris@226: elif isRowMajor? m1 != isRowMajor? m2 then Chris@238: equal' comparator vecComparator (flipped m1) m2; Chris@238: elif isSparse? m1 != isSparse? m2 then Chris@238: if isSparse? m1 then Chris@243: equal' comparator vecComparator m1 (toSparse m2) Chris@238: else Chris@243: equal' comparator vecComparator (toSparse m1) m2 Chris@238: fi Chris@100: else Chris@238: equal'' comparator vecComparator m1 m2 Chris@100: fi; Chris@97: Chris@228: // Compare matrices using the given comparator for individual cells. Chris@228: // Note that matrices with different storage order but the same Chris@228: // contents are equal, although comparing them is slow. Chris@249: //!!! Document the fact that sparse matrices can only be equal if they Chris@249: // have the same set of non-zero cells (regardless of comparator used) Chris@228: equalUnder comparator = Chris@238: equal' comparator (vec.equalUnder comparator); Chris@228: Chris@228: equal = Chris@238: equal' (==) vec.equal; Chris@226: Chris@163: newMatrix type data = //!!! NB does not copy data Chris@238: (tagger = case type of RowMajor (): DenseRows; ColumnMajor (): DenseCols esac; Chris@214: if empty? data or vec.empty? (head data) Chris@201: then zeroSizeMatrix () Chris@208: else tagger (array data) Chris@96: fi); Chris@96: Chris@163: newRowVector data = //!!! NB does not copy data Chris@234: DenseRows (array [data]); Chris@96: Chris@163: newColumnVector data = //!!! NB does not copy data Chris@234: DenseCols (array [data]); Chris@8: Chris@195: scaled factor m = //!!! v inefficient Chris@208: generate do row col: factor * (getAt row col m) done (size m); Chris@98: Chris@243: thresholded threshold m = //!!! v inefficient; and should take a threshold function? Chris@243: generate do row col: Chris@243: v = getAt row col m; if (abs v) > threshold then v else 0 fi Chris@243: done (size m); Chris@243: Chris@98: sum' m1 m2 = Chris@208: if (size m1) != (size m2) Chris@208: then failWith "Matrices are not the same size: \(size m1), \(size m2)"; Chris@98: else Chris@208: generate do row col: getAt row col m1 + getAt row col m2 done (size m1); Chris@98: fi; Chris@98: Chris@229: difference m1 m2 = //!!! doc: m1 - m2, not m2 - m1 Chris@229: if (size m1) != (size m2) Chris@229: then failWith "Matrices are not the same size: \(size m1), \(size m2)"; Chris@229: else Chris@229: generate do row col: getAt row col m1 - getAt row col m2 done (size m1); Chris@229: fi; Chris@229: Chris@229: abs' m = Chris@229: generate do row col: abs (getAt row col m) done (size m); Chris@229: Chris@249: sparseProductLeft size m1 m2 = Chris@249: (e = enumerateSparse m1; Chris@249: data = array (map \(new double[size.rows]) [1..size.columns]); Chris@249: for [0..size.columns - 1] do j': Chris@249: c = getColumn j' m2; Chris@249: for e do { v, i, j }: Chris@249: data[j'][i] := data[j'][i] + v * (vec.at j c); Chris@249: done; Chris@249: done; Chris@249: DenseCols (array (map vec.vector (list data)))); Chris@249: Chris@249: sparseProductRight size m1 m2 = Chris@249: (e = enumerateSparse m2; Chris@249: data = array (map \(new double[size.columns]) [1..size.rows]); Chris@249: for [0..size.rows - 1] do i': Chris@249: r = getRow i' m1; Chris@249: for e do { v, i, j }: Chris@249: data[i'][j] := data[i'][j] + v * (vec.at i r); Chris@249: done; Chris@249: done; Chris@249: DenseRows (array (map vec.vector (list data)))); Chris@249: Chris@251: sparseProduct size m1 m2 = Chris@251: case m2 of Chris@254: SparseCols d: Chris@251: (e = enumerateSparse m1; Chris@254: out = [:]; Chris@254: for (keys d.data) do j': Chris@254: h = [:]; Chris@254: for e do { v, i, j }: Chris@254: if j in d.data[j'] then Chris@254: if not (i in h) then h[i] := 0 fi; Chris@254: h[i] := h[i] + v * d.data[j'][j]; Chris@254: fi; Chris@254: done; Chris@254: out[j'] := h; Chris@254: done; Chris@254: SparseCols { size, data = out }); Chris@254: SparseRows _: Chris@251: sparseProduct size m1 (flipped m2); Chris@251: _: failWith "sparseProduct called for non-sparse matrices"; Chris@251: esac; Chris@251: Chris@249: denseProduct size m1 m2 = Chris@249: (data = array (map \(new double[size.rows]) [1..size.columns]); Chris@249: for [0..size.rows - 1] do i: Chris@249: row = getRow i m1; Chris@249: for [0..size.columns - 1] do j: Chris@249: data[j][i] := bf.sum (bf.multiply row (getColumn j m2)); Chris@249: done; Chris@249: done; Chris@249: DenseCols (array (map vec.vector (list data)))); Chris@249: Chris@98: product m1 m2 = Chris@208: if (size m1).columns != (size m2).rows Chris@246: then failWith "Matrix dimensions incompatible: \(size m1), \(size m2) (\((size m1).columns) != \((size m2).rows))"; Chris@249: else Chris@249: size = { rows = (size m1).rows, columns = (size m2).columns }; Chris@249: if isSparse? m1 then Chris@251: if isSparse? m2 then Chris@251: sparseProduct size m1 m2 Chris@251: else Chris@251: sparseProductLeft size m1 m2 Chris@251: fi Chris@249: elif isSparse? m2 then Chris@249: sparseProductRight size m1 m2 Chris@249: else Chris@249: denseProduct size m1 m2 Chris@249: fi; Chris@98: fi; Chris@98: Chris@161: asRows m = Chris@208: map do i: getRow i m done [0 .. (height m) - 1]; Chris@161: Chris@161: asColumns m = Chris@208: map do i: getColumn i m done [0 .. (width m) - 1]; Chris@161: Chris@178: concatAgainstGrain tagger getter counter mm = Chris@208: (n = counter (size (head mm)); Chris@208: tagger (array Chris@177: (map do i: Chris@214: vec.concat (map (getter i) mm) Chris@208: done [0..n-1]))); Chris@177: Chris@178: concatWithGrain tagger getter counter mm = Chris@208: tagger (array Chris@177: (concat Chris@177: (map do m: Chris@208: n = counter (size m); Chris@208: map do i: getter i m done [0..n-1] Chris@208: done mm))); Chris@177: Chris@178: checkDimensionsFor direction first mm = Chris@178: (counter = if direction == Horizontal () then (.rows) else (.columns) fi; Chris@208: n = counter (size first); Chris@208: if not (all id (map do m: counter (size m) == n done mm)) then Chris@208: failWith "Matrix dimensions incompatible for concat (found \(map do m: counter (size m) done mm) not all of which are \(n))"; Chris@178: fi); Chris@178: Chris@187: concat direction mm = //!!! doc: storage order is taken from first matrix in sequence Chris@187: //!!! would this be better as separate concatHorizontal/concatVertical functions? Chris@190: case mm of Chris@190: first::rest: Chris@178: checkDimensionsFor direction first mm; Chris@208: row = isRowMajor? first; Chris@178: // horizontal, row-major: against grain with rows Chris@178: // horizontal, col-major: with grain with cols Chris@178: // vertical, row-major: with grain with rows Chris@178: // vertical, col-major: against grain with cols Chris@178: case direction of Chris@178: Horizontal (): Chris@234: if row then concatAgainstGrain DenseRows getRow (.rows) mm; Chris@234: else concatWithGrain DenseCols getColumn (.columns) mm; Chris@178: fi; Chris@178: Vertical (): Chris@234: if row then concatWithGrain DenseRows getRow (.rows) mm; Chris@234: else concatAgainstGrain DenseCols getColumn (.columns) mm; Chris@178: fi; Chris@178: esac; Chris@190: [single]: single; Chris@190: _: zeroSizeMatrix (); Chris@190: esac; Chris@177: Chris@240: //!!! inconsistent with std.slice which has start..end not start+count (see also vec.slice/rangeOf) Chris@187: rowSlice start count m = //!!! doc: storage order same as input Chris@208: if isRowMajor? m then Chris@234: DenseRows (array (map ((flip getRow) m) [start .. start + count - 1])) Chris@187: else Chris@234: DenseCols (array (map (vec.rangeOf start count) (asColumns m))) Chris@187: fi; Chris@187: Chris@187: columnSlice start count m = //!!! doc: storage order same as input Chris@208: if not isRowMajor? m then Chris@234: DenseCols (array (map ((flip getColumn) m) [start .. start + count - 1])) Chris@187: else Chris@234: DenseRows (array (map (vec.rangeOf start count) (asRows m))) Chris@187: fi; Chris@187: Chris@201: resizedTo newsize m = Chris@208: (if newsize == (size m) then Chris@201: m Chris@208: elif (height m) == 0 or (width m) == 0 then Chris@202: zeroMatrixWithTypeOf m newsize; Chris@201: else Chris@208: growrows = newsize.rows - (height m); Chris@208: growcols = newsize.columns - (width m); Chris@208: rowm = isRowMajor? m; Chris@201: resizedTo newsize Chris@201: if rowm and growrows < 0 then Chris@201: rowSlice 0 newsize.rows m Chris@201: elif (not rowm) and growcols < 0 then Chris@201: columnSlice 0 newsize.columns m Chris@201: elif growrows < 0 then Chris@201: rowSlice 0 newsize.rows m Chris@201: elif growcols < 0 then Chris@201: columnSlice 0 newsize.columns m Chris@201: else Chris@201: if growrows > 0 then Chris@201: concat (Vertical ()) Chris@208: [m, zeroMatrixWithTypeOf m ((size m) with { rows = growrows })] Chris@201: else Chris@201: concat (Horizontal ()) Chris@208: [m, zeroMatrixWithTypeOf m ((size m) with { columns = growcols })] Chris@201: fi Chris@201: fi Chris@202: fi); Chris@201: Chris@5: { Chris@208: size, Chris@208: width, Chris@208: height, Chris@246: density, Chris@249: nonZeroValues, Chris@208: getAt, Chris@208: getColumn, Chris@208: getRow, Chris@208: isRowMajor?, Chris@234: isSparse?, Chris@208: generate, Chris@208: constMatrix, Chris@208: randomMatrix, Chris@208: zeroMatrix, Chris@208: identityMatrix, Chris@208: zeroSizeMatrix, Chris@208: equal, Chris@226: equalUnder, Chris@208: transposed, Chris@208: flipped, Chris@208: toRowMajor, Chris@208: toColumnMajor, Chris@238: toSparse, Chris@238: toDense, Chris@208: scaled, Chris@243: thresholded, Chris@208: resizedTo, Chris@208: asRows, Chris@208: asColumns, Chris@208: sum = sum', Chris@229: difference, Chris@229: abs = abs', Chris@208: product, Chris@208: concat, Chris@208: rowSlice, Chris@208: columnSlice, Chris@208: newMatrix, Chris@208: newRowVector, Chris@208: newColumnVector, Chris@245: newSparseMatrix = makeSparse Chris@208: } Chris@208: as Chris@208: { Chris@208: //!!! check whether these are right to be .selector rather than just selector Chris@208: Chris@208: size is matrix -> { .rows is number, .columns is number }, Chris@208: width is matrix -> number, Chris@208: height is matrix -> number, Chris@246: density is matrix -> number, Chris@249: nonZeroValues is matrix -> number, Chris@208: getAt is number -> number -> matrix -> number, Chris@214: getColumn is number -> matrix -> vector, Chris@214: getRow is number -> matrix -> vector, Chris@208: isRowMajor? is matrix -> boolean, Chris@234: isSparse? is matrix -> boolean, Chris@195: generate is (number -> number -> number) -> { .rows is number, .columns is number } -> matrix, Chris@195: constMatrix is number -> { .rows is number, .columns is number } -> matrix, Chris@195: randomMatrix is { .rows is number, .columns is number } -> matrix, Chris@195: zeroMatrix is { .rows is number, .columns is number } -> matrix, Chris@195: identityMatrix is { .rows is number, .columns is number } -> matrix, Chris@195: zeroSizeMatrix is () -> matrix, Chris@195: equal is matrix -> matrix -> boolean, Chris@226: equalUnder is (number -> number -> boolean) -> matrix -> matrix -> boolean, Chris@195: transposed is matrix -> matrix, Chris@195: flipped is matrix -> matrix, Chris@195: toRowMajor is matrix -> matrix, Chris@195: toColumnMajor is matrix -> matrix, Chris@243: toSparse is matrix -> matrix, Chris@238: toDense is matrix -> matrix, Chris@195: scaled is number -> matrix -> matrix, Chris@243: thresholded is number -> matrix -> matrix, Chris@195: resizedTo is { .rows is number, .columns is number } -> matrix -> matrix, Chris@214: asRows is matrix -> list, Chris@214: asColumns is matrix -> list, Chris@208: sum is matrix -> matrix -> matrix, Chris@229: difference is matrix -> matrix -> matrix, Chris@229: abs is matrix -> matrix, Chris@195: product is matrix -> matrix -> matrix, Chris@195: concat is (Horizontal () | Vertical ()) -> list -> matrix, Chris@195: rowSlice is number -> number -> matrix -> matrix, Chris@195: columnSlice is number -> number -> matrix -> matrix, Chris@214: newMatrix is (ColumnMajor () | RowMajor ()) -> list -> matrix, Chris@214: newRowVector is vector -> matrix, Chris@214: newColumnVector is vector -> matrix, Chris@245: newSparseMatrix is (ColumnMajor () | RowMajor ()) -> { .rows is number, .columns is number } -> list<{ .i is number, .j is number, .v is number }> -> matrix Chris@5: } Chris@5: