Setting or propagating orientation (rowwise or columnwise) of a Matrix
The default orientation of matrices is rowwise. This can be changed globally in SuiteSparse:GraphBLAS with gb.ss.config["format"] = "by_col", and a single matrix can be changed via A.ss.config["format"] = "by_col".
Using matrices with non-default orientation may result in surprising behavior. For example, would users expect elementwise operations to result in the orientation changing (such as B = (A + 1).new())? More illustrations below:
In [1]: import graphblas as gb
In [2]: A = gb.Matrix.from_csc([0, 2, 3, 3, 5], [1, 3, 7, 6, 20], [10, 20, 30, 40, 50])
In [3]: A.ss.format
Out[3]: 'csc'
In [4]: A.ss.orientation
Out[4]: 'columnwise'
In [5]: B = (A + 1).new()
In [6]: B.ss.orientation
Out[6]: 'rowwise'
In [7]: gb.ss.concat([[A, A]]).ss.orientation
Out[7]: 'rowwise'
In [8]: A[:4, :4].new().ss.orientation
Out[8]: 'rowwise'
Perhaps this isn't surprising, since a global default means just that: everything is predictable, and new objects by default will use the default orientation. If you want to control the orientation of the output, then this is possible by first creating the output object and setting its orientation.
Some questions:
- should we consider ever propagating the orientation of input matrices to the automatically created result matrix?
- should we consider a way to specify orientation (and other config?) when creating new objects?
- e.g.
B = (A + 1).new(order="col") - e.g.
B = gb.ss.concat([[A, A]], order="F") - e.g.
B = A[:4, :4].new(order="columnwise")
- e.g.
It's worth noting that the upcoming GraphBLAS 2.1 specification will introduce the ability to introspect a Matrix for its orientation.
I prefer .new(order="columnwise")
Hey thanks for bringing this up Erik, I think it would be a great addition.
I also would prefer .new(order="columwise") but I think all three options are equally suitable for this.
How difficult would it be to propagate the orientation of the input matrices to the result matrix? I feel like this is how it should work (intuitively) anyways. (as in, if someone cared enough about orientation to set a matrix to col, it should probably propagate)
Could you provide some pointers on how one could get started on adding this? I would love to give it a go.
Great, thanks @Transurgeon!
I think I would approach this as three separate tasks (and separate PRs):
- Add
order=Noneto.new()methods on matrix-like objects and also the constructors for which it makes sense- I would start by adding it to
TransposedMatrix.new, which will also need it added toMatrix.__new__. - Other matrix-like classes with
.new:MatrixInfixExpr,MatrixExpression,MatrixIndexExpr(and masks?)
- I would start by adding it to
- Add
matrix.orientationormatrix.orderor whatever attribute that can also be assigned the order (or None)- Getting and setting it today should only work if
backend == "suitesparse"by usingA.ss.orientation, otherwise raiseNotImplementedError(b/c we're waiting on the next version of the spec to be released)
- Getting and setting it today should only work if
- Propagate the orientation of the input matrices
- Again, I would begin small, such as
Transposed.new - It may be a little trickier to do this elsewhere, but it may be best to determine the orientation to propagate when e.g.
MatrixExpressionis created.
- Again, I would begin small, such as
Working on any of these would be a great learning experience. Please let us know how we can make contributing easier or the code easier to navigate. I'm happy to assist and teach, so please ping us/me if you have any questions or blockers.
Btw, our get_order utility function already handles all the variations of order=... that I shared.
-
{"C", "row", "rows", "rowwise"} -> "rowwise" -
{"F", "col", "cols", "column", "columns", "colwise", "columnwise"} -> "columnwise"
Hey @eriknw , thanks for your explanations.
If I understand correctly, the first PR is just there to add an optional argument order to the new() constructor for various Matrix objects. It won't cover any implementation or actually setting the order.
P.S could you give a primer on masks? In my understanding they are used to select a subset of the output (given a certain condition). How come we would need to add order to masks?
P.S.S I noticed that I am traversing the repo quite inefficiently (when searching for classes for example), I know about grep but was wondering if you had any efficiency related tips & tricks.
I would recommend implementing and testing anything that gets added, so maybe the first two items should be swapped or done simultaneously.
Quick tour of classes! These all have .new
In [1]: import graphblas as gb
In [2]: A = gb.Matrix(bool)
In [3]: type(A.T)
Out[3]: graphblas.core.matrix.TransposedMatrix
In [4]: type(A & A) # pass to monoid or binary operator gives MatrixExpression (see example below)
Out[4]: graphblas.core.infix.MatrixEwiseMultExpr
In [5]: type(A | A) # pass to monoid or binary operator gives MatrixExpression (see example below)
Out[5]: graphblas.core.infix.MatrixEwiseAddExpr
In [6]: type(A @ A) # pass to semiring operator gives MatrixExpression (see example below)
Out[6]: graphblas.core.infix.MatrixMatMulExpr
In [7]: type(A[:, :]) # used for assign and extract
Out[7]: graphblas.core.matrix.MatrixIndexExpr
Masks also have .new, which results in boolean matrices. We don't have e.g. MatrixMask yet, but maybe we should?
In [8]: type(A.S)
Out[8]: graphblas.core.mask.StructuralMask
In [9]: type(A.V)
Out[9]: graphblas.core.mask.ValueMask
In [10]: type(~A.S)
Out[10]: graphblas.core.mask.ComplementedStructuralMask
In [11]: type(~A.V)
Out[11]: graphblas.core.mask.ComplementedValueMask
MatrixExpression is the bread-and-butter of expressions--they are just waiting on an output to update or .new to be called:
In [13]: type(A.ewise_mult(A, 'lor'))
Out[13]: graphblas.core.matrix.MatrixExpression
In [14]: type(gb.binary.lor(A & A))
Out[14]: graphblas.core.matrix.MatrixExpression
In [15]: type(A.ewise_add(A, 'lor'))
Out[15]: graphblas.core.matrix.MatrixExpression
In [16]: type(gb.binary.lor(A | A))
Out[16]: graphblas.core.matrix.MatrixExpression
In [17]: type(gb.semiring.any_pair(A @ A))
Out[17]: graphblas.core.matrix.MatrixExpression
We have a couple more objects that do not have .new methods. These are used to update a matrix in-place. Note that when I call A() below, one would typically pass a mask and/or and accumulator such as A("+") or A(B.S).
In [18]: type(A())
Out[18]: graphblas.core.expr.Updater
In [19]: type(A[:, :]())
Out[19]: graphblas.core.expr.Assigner
In [20]: type(A()[:, :])
Out[20]: graphblas.core.expr.Assigner
Most implementation occurs in graphblas.core. To begin, you are likely to need to poke around in matrix, infix, and expr under graphblas.core.
I'll share more about masks soon.