Skip to content

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

Open

Description

@eriknw

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.

Activity

  1. added
    enhancementImprove existing functionality or make things work better
    discussionDiscussing a topic with no specific actions yet
    on Aug 21, 2023
  2. jim22k commented on Aug 23, 2023

    @jim22k
    Member

    I prefer .new(order="columnwise")

  3. Transurgeon commented on Aug 23, 2023

    @Transurgeon
    Contributor

    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.

  4. eriknw commented on Aug 24, 2023

    @eriknw
    MemberAuthor

    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"
  5. Transurgeon commented on Aug 26, 2023

    @Transurgeon
    Contributor

    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.

  6. eriknw commented on Aug 27, 2023

    @eriknw
    MemberAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    discussionDiscussing a topic with no specific actions yetenhancementImprove existing functionality or make things work better

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions