python-graphblas icon indicating copy to clipboard operation
python-graphblas copied to clipboard

Setting or propagating orientation (rowwise or columnwise) of a Matrix

Open eriknw opened this issue 2 years ago • 5 comments

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")

It's worth noting that the upcoming GraphBLAS 2.1 specification will introduce the ability to introspect a Matrix for its orientation.

eriknw avatar Aug 21 '23 21:08 eriknw

I prefer .new(order="columnwise")

jim22k avatar Aug 23 '23 14:08 jim22k

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.

Transurgeon avatar Aug 23 '23 22:08 Transurgeon

Great, thanks @Transurgeon!

I think I would approach this as three separate tasks (and separate PRs):

  • Add order=None to .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 to Matrix.__new__.
    • Other matrix-like classes with .new: MatrixInfixExpr, MatrixExpression, MatrixIndexExpr (and masks?)
  • Add matrix.orientation or matrix.order or whatever attribute that can also be assigned the order (or None)
    • Getting and setting it today should only work if backend == "suitesparse" by using A.ss.orientation, otherwise raise NotImplementedError (b/c we're waiting on the next version of the spec to be released)
  • 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. MatrixExpression is created.

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"

eriknw avatar Aug 24 '23 05:08 eriknw

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.

Transurgeon avatar Aug 26 '23 00:08 Transurgeon

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.

eriknw avatar Aug 27 '23 00:08 eriknw