Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No obvious way to concatenate vectors and matricies #446

Open
glfmn opened this issue Oct 15, 2018 · 11 comments
Open

No obvious way to concatenate vectors and matricies #446

glfmn opened this issue Oct 15, 2018 · 11 comments
Labels
enhancement good first issue Good first issue for newcomers. P-medium Medium priority

Comments

@glfmn
Copy link

glfmn commented Oct 15, 2018

When using a dynamic matrix, there is no clear way to extend a matrix not with copies of a single element, but with a vector (like with insert_row or insert_column) or with a matrix (counterpart to insert_rows or insert_columns). In fact judging by the names, I thought that they used a vector and matrix respectively and not a single value.

This isn't mentioned at all in the reference and I haven't found anything in the API. Is there any reason that concatenation isn't supported when insert_row and insert_column exist? My current solution is to create a vec of vectors and use the from_columns constructor.

@sebcrozet sebcrozet added enhancement good first issue Good first issue for newcomers. P-medium Medium priority labels Oct 16, 2018
@sebcrozet
Copy link
Member

This is not supported yet but could be added.
I agree the names can be misleading. Maybe we should rename them insert_row_from_element or something like this.

@glfmn
Copy link
Author

glfmn commented Oct 18, 2018

Considering that there are already from_element methods I think that's good, only thing is it would be a breaking change, right?

I think it would also be nice to have methods to extend a matrix with a matrix or vector at the end rather than inserting them.

For the record my workaround was to create a Vec of the columns and use the from_columns function to make the matrix for my dataset:

    // Stride the range of included sets in this dataset
    for s in sets {
        // Load the images from the set which correspond to the training set
        for i in range.clone() {
            if let Ok(img) = image::open(format!("{}/s{}/{}.pgm", path, s, i)) {
                bounds = img.dimensions();
                images.push(image_vector(img));
            } else {
                return Err(format!("Failed to open image s{}/{}.pgm", s, i));
            }
        }
    }

@blankenshipz
Copy link

I think that specifically for the DVector type there should be methods to extend the DVector by concatenating another DVector (this is similar to append on Vec in the stdlib) and a method to add a single element (which would be similair to push on Vec)

@sebcrozet What do you think about this? Should this be a new issue?

jswrenn added a commit to jswrenn/nalgebra that referenced this issue Nov 12, 2018
Extends a `Vector` with new rows populated from an iterator.

Inspired by dimforge#446 (comment)
@sebcrozet
Copy link
Member

@blankenshipz Yes this would be useful! As you suggested this should be on a new issue.

sebcrozet pushed a commit that referenced this issue Nov 18, 2018
Extends a `Vector` with new rows populated from an iterator.

Inspired by #446 (comment)
@pwnorbitals
Copy link

I would like to bump this issue : there is now Extend for DVector, but I'm not aware of any utility that could help with concatenating SVectors

@Andlon
Copy link
Collaborator

Andlon commented Jun 25, 2021

So at one point I actually hacked together a prototype for a fairly generic solution to concatenating matrices and vectors. The tricky part is to make it work properly with static dimensions. I solved this by introducing a new trait Block and wrapper types VCat and HCat which can be used to construct conceptual "matrix blocks" (without actually constructing any intermediate matrices), so that at the end we can obtain the correct dimensions for the output and finally populate the individual blocks of the input. I never had the time to work further on this, but perhaps it can serve as a starting point for a solution.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=221a1aab484d6432d1203c92fcefcae4

The example above shows how you can do things like:

    let a = Matrix2::new(0, 0, 0, 0);
    let b = Matrix2::new(1, 1, 1, 1);
    let c = Matrix4::new(2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2);
    
    let result: Matrix6x4<_> = bcat![a, b;
                                     c];
    println!("{}", result);

which outputs

  ┌         ┐
  │ 0 0 1 1 │
  │ 0 0 1 1 │
  │ 2 2 2 2 │
  │ 2 2 2 2 │
  │ 2 2 2 2 │
  │ 2 2 2 2 │
  └         ┘

I think it can be extended to support any number of desirable features.

(Note that the playground link might not work in the future if there are more breaking changes in nalgebra, since there is no way to pin dependency versions in the playground. But it should work with nalgebra version 0.27 I suppose, since I guess that's what's currently in the playground)

@Andlon
Copy link
Collaborator

Andlon commented Aug 27, 2021

Since another user asked for concatenation on Discord, I wanted to add some points about my previous suggestion. It is built around a Block trait like this:

trait Block<T> {
    type Rows: Dim;
    type Cols: Dim;
    
    fn shape(&self) -> (Self::Rows, Self::Cols);
    
    fn populate<S>(&self, output: &mut Matrix<T, Self::Rows, Self::Cols, S>)
    where 
        T: Scalar,
        S: StorageMut<T, Self::Rows, Self::Cols>;
}

Matrix types have a trivial Block impl. It is otherwise implemented by types corresponding to horizontal, vertical and diagonal concatenation (HCat and VCat in the linked prototype, I didn't have diagonal then):

struct Horizontal<X>(pub X);
struct Vertical<X>(pub X);
struct Diagonal<X>(pub X);

impl<T, B: Block<T>> Block<T> for Horizontal<(B,)>  { /* impl */ }
impl<T, B1: Block<T>, B2: Block<T>> Block<T> for Horizontal<(B1, B2)>  { /* impl */ }
// and so on for higher-arity tuples, plus a slice impl:
impl<'a, T, B: Block<T>> Block<T> for Horizontal<&'a [B]>  { /* impl */ }

// Analogous impls for `Vertical` and `Diagonal`

The macro bcat! then basically just wraps each "row" in the macro in Horizontal and wraps the whole thing in Vertical, so for example

bcat![a, b;
      c, d]

translates to allocating storage and populating the recursive block defined by

Vertical( (
  Horizontal((a, b)),
  Horizontal((c, d))
) )

These traits work across static and dynamic dimensions and mixes thereof by leveraging the support for dimension "algebra" already present innalgebra (see the linked prototype for details). Interestingly, the recursive nature of this API makes it much more powerful than just being able to handle "simple" concatenation like the above. Starting simple, we can do diagonal concatenation:

bcat![Diagonal((a, b))]

// equivalent in output to
bcat![        a, zero_1;
         zero_2,         b];
// with zero_1 and zero_2 appropriately sized matrices filled with zeros

But it doesn't stop there, we can mix and match these qualifiers almost arbitrarily. For example, say we want to construct the matrix

[ a, 0, b ]
[ 0, c, d ]
[ e, f, g ]

where 0 denotes a zero matrix of appropriate size. We could write this as:

bcat![
     Diagonal((a, c)), Vertical((b, d));
   Horizontal((e, f)),                g
]

The good news is that all of this happens with a single allocation: the output matrix is allocated once and each block is filled in recursively. Finally, we can also do all this with slices (so a dynamic number of matrices):

// Dynamic number of column entries
bcat![&[a, b];
      &[c, d]]

// Dynamic number of rows
bcat![Vertical(&[a, b, c])]

// Dynamic number of diagonal blocks
bcat![Diagonal(&[a, b, c])]

// Mix and match (matrices must be dimensionally compatible of course)
bcat![a, Diagonal(&[a, b, c]);
      d,              &[e, f]];

Undoubtedly there would be some details to work out, but at the surface, it seems this API would be remarkably powerful given its simplicity.

Unfortunately I don't foresee having time to look at this in the very near future, but I might be able to squeeze in a little bit of time soem time in the next few months? Alternatively I'd be happy to mentor someone who would like to give an implementation a try.

@sebcrozet
Copy link
Member

sebcrozet commented Aug 27, 2021

This looks like a great idea @Andlon. Could we perhaps avoid having to write the Diagonal/Vertical/Horizontal by simply adding zeros? Instead of writing:

bcat![
     Diagonal((a, c)), Vertical((b, d));
   Horizontal((e, f)),                g
]

could the macro directly understand this:

bcat![
    a, 0, b;
    0, c, d;
    e, f, g;
]

where the macro would automatically understand that each 0 literal should be replaced by the properly sized 0 submatrix?

@Andlon
Copy link
Collaborator

Andlon commented Aug 31, 2021

That's an interesting proposition @sebcrozet! I've been thinking about this over the weekend, but I have not yet reached a clear conclusion. I'll outline my thoughts below.

OK, so the first challenge is to distinguish 0 from a general expression. This cannot be done with a declarative macro as far as I know. But it's possible to do this with a procedural macro: using syn you can explicitly check for an ExprLit that corresponds to a single zero.

The second - and much more significant - challenge, is how to actually accomplish this. For example, it is tempting to think of something like a Zero type which the macro would just inject, i.e. equivalent to

bcat![   a, Zero, b,
      Zero,    c, d,
         e,    f, g]

Unfortunately, this would not work with the recursive Block-based solution I outlined earlier. The reason is that the Block has associated types Rows and Cols which determine the number of rows and columns. For Zero we wouldn't know what size to give it.

Next, we could imagine parametrizing Zero by its dimensions, i.e. Zero<Rows, Cols>. However, we face the same problem: the macro only has access to the (abstract) syntax, not information about the types themselves, so it cannot know what dimensions to give it.

The reason e.g. Diagonal((a, b)) works is that the sizes of the zero blocks are implicitly given by the block-diagonal expression.

I think something in this direction can be done, however, and we can use a similar structure as our current API proposal, although I think we might have to sacrifice some of our perfectly well-defined recursive structure to accomplish this. I don't have more time to go into details right now, but I'll think about this and hopefully get back to it later. However, we can outline the main subproblems that need to be solved:

  1. Determine the size of the output matrix. This is surprisingly tricky: Consider for example the matrix

    [ a, 0, b ]
    [ c, d, 0 ]
    [ 0, e, f ]
    

    Here the size of any single row or column is undecidable on its own: you need the context of the whole matrix in order to uniquel and correctly decide the size of the output matrix. This means that a simple recursive decision process as used in the initial prototype would not be sufficient.

  2. Populate the output matrix. This is trickier than in my previous proposal, as e.g. the row [a, 0, b, 0] does not by itself know the size of the zero matrix, so it wouldn't know where in the output matrix to populate the b matrix.

Food for thought...

@el-hult
Copy link

el-hult commented Apr 30, 2024

Just want to chime in. I'm playing with some LTI systems, where cascading systems is most easily done by matrix concatenation. I would hope to concatenate some SMatrix instences into larger SMatrices. But I cannot work out how to do that with DimSum etc.

Even implementing a simple function block_mat_2x2 that takes generic SMatrix instances as input and output seems too hard.

Having a cat! matrix would be amazing!

@Andlon
Copy link
Collaborator

Andlon commented Apr 30, 2024

For anyone following this issue, there is now a complete stack! macro in #1375 awaiting final review. Hopefully we can get it merged soon and include it in the next release!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement good first issue Good first issue for newcomers. P-medium Medium priority
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants