[nalgebra] Why use `OMatrix`/`OVector`?

It looks like the main difference between nalgebra's Matrix aliases is that SMatrix uses ArrayStorage<T, R, C>, DMatrix uses VecStorage<T, Dyn, Dyn>, and OMatrix uses Owned<T, R, C>.

My main issue is that I don't really understand the semantics of Owned<T, R, C>. I (thought) I understood what ownership is in Rust -- don't SMatrix and DMatrix also own their data? Wouldn't the only way to not own the data be by borrowing a Matrix with & or &mut?

This lack of understanding leads me to my main question: If your Matrix needs to change size, you could use DMatrix. If it doesn't, you could use SMatrix. Why use OMatrix at all?

Here's an example of something that uses OMatrix, btw: src/propagators/propagator.rs · master · nyx-space / nyx · GitLab

Also, do you pass OMatrix an Allocator because it allocates memory upon instantiation? Is that what makes it owned?

1 Like

Note how the generic parameters of SMatrix, DMatrix, and OMatrix are different.

What's a OMatrix<T, Const<1>, Const<1>>, say? It's a

Matrix<T, Const<1>, Const<1>, Owned<T, Const<1>, Const<2>>

What's that Owned thing on the end? It's another type alias:

<DefaultAllocator as Allocator<T, Const<1>, Const<1>>>::Buffer

What's that? Here's the implementation in question; and it's a

ArrayStorage<T, 1, 1>

Putting it all together, we have a

Matrix<T, Const<1>, Const<1>, ArrayStorage<T, 1, 1>>

And if you look at the definition of SMatrix, you'll then see that these are the same:

OMatrix<T, Const<1>, Const<1>>
SMatrix<T, 1, 1>

And similarly you could figure out that these are the same

OMatrix<T, Dyn, Dyn>
DMatrix<T>

But there is no shorthand for a mix of Dyn and Const<_> in OMatrix, say.


You pass it an Allocator so that the library can support custom allocators, which could store the data in custom storage types, say. I don't know that I would say that "makes it owned" per se; they're owned because they're not borrowing from some other matrix.

3 Likes

If you unwrap the type synonyms, you’ll find Owned<T, R, C> goes through the type synonyms, Owned<T, R, C> expands to <DefaultAllocator as Allocator<T, R, C>>::Buffer which is further defined by three trait implementations:

impl<T: Scalar, C: Dim> Allocator<T, Dyn, C> for DefaultAllocator {
    type Buffer = VecStorage<T, Dyn, C>
    […]
}

impl<T: Scalar, R: DimName> Allocator<T, R, Dyn> for DefaultAllocator {
    type Buffer = VecStorage<T, R, Dyn>
    […]
}

impl<T: Scalar, const R: usize, const C: usize> Allocator<T, Const<R>, Const<C>> for DefaultAllocator {
    type Buffer = ArrayStorage<T, R, C>
    […]
}

Note by the way that ArrayStorage’s R and C arguments are const generics, whereas Owned’s R and C are, as are the one of VecStorage, types implementing Dim. This difference transfers to SMatrix having const generic parameters.

So what is “owned”? It’s simply some smart choice of an “owned” representation avoiding heap allocations if possible. And it’s nothing new, it literally just choosing either of ArrayStorage or VecStorage.

As for when to use what, in terms of the Matrix types, it’s mostly just shorthands then. SMatrix<T, R, C> is OMatrix<T, Const<R>, Const<C>>. If you already have const generics, or concrete numbers, no need to type out the more verbose version. Similarly, DMatrix<T> is OMatrix<T, Dyn, Dyn>.

While VecStorage can be used with Const<…> dimensions, too, for reasonably small sizes you probably never want this. On the other hand, if your dimensions are known but large, e.g. perhaps instead of SMatrix<T, 10_000, 10_000>, you want to move over to Matrix<T, Const<10_000>, Const<10_1000>, VecStorage<T, Const<10_000>, Const<10_000>> instead, because anything else would blow up your stack :slight_smile: That's not actually correct, this wouldn't work, as VecStorage doesn't support both dimensions being Const, I didn't spot that earlier in the docs.


Note also that OMatrix is used in generic functions, e.g. the into_owned() function that works quite generically on almost any type of Matrix you’ll have – so that function, too, will be smart, and appropriately return array-based matrices for known sizes, and vec-based ones when (at least one of the) dimensions are unknown, aka Dyn. E.g. in code like

use nalgebra::{Matrix2, Matrix1x2};

fn f(x: Matrix2<u8>) {
    let r: Matrix1x2<u8> = x.row(0).into_owned();
}

even though into_owned() returns a OMatrix, it’s actually the same type as the synonym Matrix1x2 in this case, and it would be the same type as SMatrix<u8, 1, 2> as well.

2 Likes

Didn't spot the question on the first read, the answer is that - no - a Matrix can also be one of the MatrixView versions which don't own their data, using ViewStorage or ViewStorageMut.

2 Likes

Ah, I see now. Thanks to both of you for stepping through that with me!

One of the more important things I learned from this process was that SMatrix uses const generics whereas OMatrix instead uses my new friend typenum. Given the current state of const generics, that gives OMatrix a huge advantage.

As beautiful as const generics are in concept, I can't stop running into issues with them. While typenum introduces a bit of noise into the code, it's well worth it since I at least feel like I can at least reliably get it to work.

Thanks again!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.