** ** Base matrix (n-dimensional array) class. The 'of' and 'shape' are constant for each Mat. ** ** TODO Support this technique for batched operations, if that turns out to matter for performance: ** ** Mat a = Mat.batch { ** b + c.trans * d ** } ** mixin Mat { ** ** A low-level tool when you want to specify the type and shape manually. ** ** Shape to Mat or 'Obj'? ** static Mat createOf(Type of, Int[] shape, Obj?[] values) { // TODO Base on array type. ObjMat.dupOrGen(of, shape, values) } ** ** Clears the link to underlying data, and allows the data to go back into a pool ** for reuse. Could make chunking through many large matrices faster. ** ** TODO Profile the effectiveness of pooling, if it's implemented. ** ** TODO Implement weak reference automated pooling on platforms that support it? ** Void dispose() { } ** ** Mat instead of Int[]. ** Void eachCoords(|Int[]| handler) { Int[] coords := List(Int#, shape.size) shape.size.times { coords.add(0) } size.times { if (it > 0) { coords[-1]++ // TODO Only go while == condition is true. (coords.size - 1 .. 0).each { if (coords[it] == shape[it]) { coords[it - 1]++ coords[it] = 0 } } } handler(coords) } } override Bool equals(Obj? that) { if (that isnot Mat) { return false } mat := that as Mat if (shape != mat.shape) { return false } if (of != mat.of) { // TODO Support of 'of' imperfect so far because of non-dynamic return types. // TODO Put this back in once we handle 'of' better. Maybe need explicit type literals. // return false } // Check the actual values. // TODO Break the value check out for optimizing in subclasses. This part is potentially slower than other parts of the test. result := true size.times |i| { if (get(i) != mat[i]) { result = false return } } // All clear. return result } static Mat eye(Int nrows, Int ncols := nrows) { // TODO FloatMat here instead of ObjMat. return gen(Float#, [nrows, ncols]) { it[0] == it[1] ? 1f : 0f } } ** ** TODO Change this to 'Obj' or 'Obj[]'? Should support n dims. Or lists of Mats, etc. ** static Mat from(Obj[][] rows) { // TODO Base on the array type. return ObjMat(rows) } ** ** TODO Allow (or require) Mats for shape and for coords. ** static Mat gen(Type of, Int[] shape, |Int[] coords -> Obj?| generator) { // TODO Delete this echo once typing is resolved!!! // TODO There was supposedly a fix for this. I need to look into it!! echo("Mat.gen: $generator") // TODO Check type and maybe even shape to determine class. return ObjMat.makeGen(of, shape, generator) } ** ** Pass in an 'Int[]' or 'Mat' for dimensional coords or an 'Int' for direct, undimensional array access. ** abstract Obj? get(Obj coords) ** ** May avoid boxing or arrays for fast 'Float' access. ** Will not convert. Only casts. ** ** Use c as -1 to imply r as an index into the full underlying data, ignoring dimensions. ** virtual Float getf(Int r, Int c := Int.minVal) { get(c == Int.minVal ? r : [r, c]) } ** ** May avoid boxing or arrays for fast 'Int' access. ** Will not convert. Only casts. ** ** Use c as Int.minVal to imply r as an index into the full underlying data, ignoring dimensions. ** virtual Int geti(Int r, Int c := Int.minVal) { get(c == Int.minVal ? r : [r, c]) } ** ** TODO Standardize this! For now, not guaranteed to be the same in the future. ** override Int hash() { toStr.hash } ** ** Convert the coords (Int[] or Mat or Int) to a raw index. ** Int index(Obj coords) { index := 0 if (coords is Int[]) { // TODO Method in Mat for sub2ind or whatever. // TODO Support negative indexes. coordsList := coords as Int[] coordsList.each |coord, dim| { if (dim > 0) { index *= shape[dim - 1] } index += coord } } else if (coords is Int) { index = coords } else { throw Err("coords not supported: $coords") } return index } ** ** Create a new matrix from this. ** The matrix passed in is the coordinates. ** TODO Change Int[] to Mat. ** Mat map(Type mappedOf, |Obj?, Int[] -> Obj?| mapper) { // TODO Use the return type to match List#map, once we can control return types. // TODO Delete this echo once typing is resolved!!! // TODO There was supposedly a fix for this. I need to look into it!! echo("Mat.map: $mapper") i := 0 return gen(mappedOf, shape) { mapper(this[i++], it) } } ** ** Create a new matrix from this. ** The matrix passed in is the coordinates. ** TODO Override in FloatMat for performance. ** TODO Change Int[] to Mat. ** virtual Mat mapf(|Float, Int[] -> Float| mapper) { // TODO Delete this echo once typing is resolved!!! // TODO There was supposedly a fix for this. I need to look into it!! echo("Mat.mapf: $mapper") return map(Float#, mapper) } ** ** Returns a new matrix, leaving this alone. ** Mat max(Int dim) { return zeros(1) } ** ** Returns a new matrix, leaving this alone. ** Mat mean(Int dim) { return zeros(1) } ** ** Returns a new matrix with the min values along dim. ** Mat min(Int dim) { reduce(dim, null) |a, b| {a->compare(b) <= 0 ? a : b} } ** ** Returns a new matrix, leaving this alone. ** Mat mult(Mat other) { // TODO Greater than 2 dims? if (ndims > 2) { throw Err("this.ndims > 2: $ndims") } if (other.ndims > 2) { throw Err("other.ndims > 2: ${other.ndims}") } if (ncols != other.nrows) { throw Err("mismatched inner sizes: $ncols vs. ${other.nrows}") } // TODO Deal with dims of size 0? result := gen(of, [nrows, other.ncols]) |coords| { sum := this[[coords[0], 0]]->mult(other[[0, coords[1]]]) (1 ..< ncols).each |i| { sum = sum->plus(this[[coords[0], i]]->mult(other[[i, coords[1]]])) } return sum } return result } Mat multEach(Mat other) { // TODO Broadcasting like numpy? i := 0 return map(of) { it->mult(other[i++]) } } ** ** Returns a new matrix, leaving this alone. ** Mat multFloat(Float float) { mapf{float * it} } Int ncols() { size(-1) } Int ndims() { // Or should this be how many dimensions have size > 1? No, unless we reverse order as in MATLAB. // Follow NumPy conventions here. // Is a 1x1 zero-dimensional? No. All unspecified sizes are implicitly 1. // Just trust the user to say how many dims they care about? Yes. // TODO Provide a method to collapse empty dimensions (to min of either 1 or 2)? shape.size } Int nrows() { size(-2) } ** ** Unlike List, actually require matching types on set! (Fan might start requiring soon too, though. It's TBD.) ** If we can enforce type, and maybe size, that would allow consistent optimization for various types and sizes. ** abstract Type of() Mat plus(Mat other) { i := 0 return map(of) { it->plus(other[i++]) } } ** ** Reduces the matrix along the given dimension. ** ** Reduces the Mat along one dimension perform item-wise operations. ** ** TODO Go back to supporting a reduced-matrix-wise form, too. ** ** TODO Should we support reducing multiple dimensions in one call? ** ** If init is null, then use the first item along dim. ** In such cases, the reducer will only be called if size(dim) >= 2. ** ** The first 'Mat' arg in the callback is the current item at the given index. ** The second is the next item to include in the reduction. ** The third is the current coordinates in the original matrix. ** ** Returning a new Mat each time could be wasteful without a Mat pool. ** Do we need modifiable Mats and just modify the current collapsed one? ** Mat reduce(Int dim, Obj? init, |Obj?, Obj?, Int[] -> Obj?| reducer) { newShape := shape.map |v, i| {i == dim ? 1 : v} newSize := sizeFor(newShape) if (size(dim) > 0) { // TODO If init === null, then we need to fill with the first element. // TODO If init === null, then we need size(dim) > 1 to bother calling back. // TODO Use the reduceMat form for implementing this one??? size(dim).times { // TODO Actually make any of this work. next := null result := reducer(init, next, Int[,]) } } return init } ** ** Generates a selection for this matrix of size ndims. ** For example, to create a selection of the first element of one dim, but all of the others, try: ** ** sel{it == dim ? 0 : Step.all) ** ** The result can then be used by methods such as 'get' or 'viewSlice'. ** Obj[] sel(|Int -> Obj| gen) { Step.mapTimes(Obj#, ndims, gen) } ** ** Returns the shape specified when creating the Mat as an immutable list. ** TODO Maybe as an immutable Mat in the future. ** ** TODO Support specifying the size of the shape vector. Could prepend 1s or remove leading 1s in many cases. ** abstract Int[] shape() ** ** If the dim is Int.minVal, then return the total number of elements. ** Using Int.minVal allows for avoiding object creation (vs. using null). ** virtual Int size(Int dim := Int.minVal) { dim == Int.minVal ? sizeFor(shape) : shape[dim] } static Int sizeFor(Int[] shape) { shape.reduce(1) |Int a, Int b -> Int| {return a * b} } ** ** TODO Better alignment of columns, ... ** override Str toStr() { // buf := StrBuf() // TODO Normalize newlines? // TODO Inline for single row? // TODO Only head and tail and size for big matrices? buf.add("Mat <| -> $of") eachCoords { if (it[-1] == 0) { if (it.size > 2 && it[-2] == 0) { buf.add("\npage ${it[0..-3]}") } buf.add("\n\t") } buf.add("${this[it]} ") } buf.add("\n|>\n") return buf.toStr } ** ** Modifies this. No way!!! ** Mat transpose() { return zeros(1) } ** ** Returns a new matrix, leaving this alone. ** Mat transposed() { return zeros(1) } ** ** Returns a view to this Mat sliced along each dimension according to sel. ** SliceMat viewSlice(Obj sel) { // SliceMat(this, sel) } static Mat zeros(Int nrows, Int ncols := nrows) { // TODO FloatMat here instead of ObjMat. return gen(Float#, [nrows, ncols]) {0f} } }