AtheMathmo / rulinalg

A linear algebra library written in Rust

Home Page:https://crates.io/crates/rulinalg

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Conversion functions between element index and dimensional index

opened this issue · comments

I didn't spot a function like this in the docs, but is this right?

// Gets the pair index from a matrix. Assumes row-major (but works with either).
fn indices_from_pos(row_stride: usize, ind: usize) -> [usize; 2] {
  let col_ind = ind % row_stride;
  let row_ind = ind / row_stride;
  [row_ind, col_ind]
}

Also, would a pair of methods like this be helpful to have implemented on Matrix?

I can't help but feel that if users need to use this, it indicates that the API is lacking in some aspect, so maybe we should look into these potential deficiencies. Do you have an example when you would want to map between flat data indices and (row, col) indices?

I agree with @Andlon . Currently we accept the row and column index in all functions and produce the flat data index ourself. I'm not sure of a situation when a user would want to do the opposite.

Actually there is one case where I needed something similar but it is very niche. To do some arithmetic strength reduction I had some helper functions that did the above without the mod and divide operators (in some special cases).

Functions that use the Cartesian product of elements of a matrix produce an MNxMN matrix. An example is the method of moments, which involves calculating the distance between each element and all others in the same matrix. In this case, it's convenient to iterate over .data(), but if you need to refer to another matrix that has related dimensions MxN, (say, a tensor containing other characteristics about the same point), you need to do a couple of things to relate the indices back.

Here's the math: https://bright-star.gitlab.io/electro/method-of-moments/

I'm afraid that I'm still not convinced (or maybe I just don't get it). Is it possible for you to give a (preferably) small example?

There is actually an example in a PR right now of computing a distance matrix.

And also, if you need to treat two different matrices would it not be easier to zip iterators going over both matrices?

Sorry if I'm misunderstanding!

I can see that this might be convenient in that context, @bright-star.

That said, I think this is conflating two very similar but different problems:

  • Indexing into the internal data structure of a matrix (which "coincidentally" happens to be laid out flat in the way you want to do)
  • Mapping indices between two related, but different vector spaces

Due to separation of concerns, it's usually a good idea to not mix these two concepts. In practice, choosing the vector spaces so that it aligns with the row/col alignment of the data structure might give better performance in some cases, but this is probably rarely the bottleneck of your program, and it could perhaps be considered a case of premature optimization. And if it is actually a bottleneck, a function to map indices can still be written outside of the library.

By the way, I really like your write-ups on electrostatics. I haven't read them in detail, and it's certainly not within my area of expertise, but it looks like a useful resource!

@Andlon is right. It just happened that the row-major flat form was what I needed:

fn row_pairwise_dists(&self) -> Vec<Vec<T>> {
    self.grid
        .space
        .data()
        .iter()
        .map(|coord_arr: &[T; 2]| {
          self.grid.space.data().iter().map(|&other_coord_arr| {
            pairwise_dist(coord_arr, other_coord_arr)
          }).collect::<Vec<T>>()
        }).collect::<Vec<Vec<T>>>()
  }

Later:

let moment_vec: Vec<T> = distances
      .iter()
      .flat_map(move |dist_row| {
        dist_row.iter().enumerate().map(move |(el_ind, &dist)| {

          if dist != NumCast::from(0).unwrap() {
            // Since we were iterating over the MxN elements of the matrix, we
            // need to convert back to the usual dimensional indices for sanity.

            // Check to see if we're in the area of values matching the top
            // plate grid
            if el_ind < top_grid_size {
              let current_pos = indices_from_pos(self.top_grid.space.row_stride(), el_ind);
              
              self.top_grid.space_area(current_pos) / two_cast_tau / dist
            } else {
              let current_pos = indices_from_pos(self.bottom_grid.space.row_stride(),
                                                 el_ind - top_grid_size);
              
              self.bottom_grid.space_area(current_pos) / two_cast_tau / dist
            }            

          } else {
            // This should only occur in the diagonal case. It's only a
            // placeholder until the diagonal correction down below.
            NumCast::from(0).unwrap()
          }        
        })
      }).collect();

I'm afraid I'm not following this discussion very well...

It seems this issue is at least somewhat resolved? Personally I think that even if this addition would add some utility is very niche and even the implementation of such a function varies on the use case - for example you might need a couple hundred lines of code for efficient indexing....

Am I ok to close this?

@AtheMathmo I'm fine either way. I just wanted to let you know that people will want to do this if they compute a function in the product space of a matrix.