Implementing nalgebra matrix addition between custom arithmetic type and float

I am using nalgebra 0.31.4 and I successfully implemented a custom type called MyNum<T> which I can store in nalgebra vectors and matrices. My end goal is a bit more complicated, but the example below isolates the key issue I am having trouble with.

I want my custom type to be able to freely interact with other floats both as a scalar (e.g., my_num * 2, etc.) and when part of a nalgebra container, like a Vector2.

By implementing Add and AddAssign I can support adding nalgebra vectors of MyNum. I also implemented the traits necessary to add MyNum<f64> and f64 instances.

However, I am struggling to come up with a way to add, e.g., a Vector3<MyNum<f64>> to a Vector3<f64>>. I cannot figure out which trait I am missing.

Here is the code I have so far, which allows the operations above, but not mixing vectors of MyNum and plain doubles, as noted by the comment on top of the last line of code:

use nalgebra::{RealField, Vector2};
use std::ops::{Add, AddAssign};

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
struct MyNum<T> {
    a: T,
    b: T,
}

impl<T: RealField + Copy> Add for &MyNum<T> {
    type Output = MyNum<T>;

    fn add(self, rhs: Self) -> Self::Output {
        MyNum {
            a: self.a + rhs.a,
            b: self.b + rhs.b,
        }
    }
}

impl<T: RealField + Copy> Add for MyNum<T> {
    type Output = MyNum<T>;

    fn add(self, rhs: Self) -> Self::Output {
        MyNum {
            a: self.a + rhs.a,
            b: self.b + rhs.b,
        }
    }
}


// Note that we need this AddAssigns to support adding vectors containing MyNums, even if you don't explicitly AddAssign
// i.e., even if you just do '&v1 + &v2'.
impl<T: RealField + Copy> AddAssign for MyNum<T> {
    fn add_assign(&mut self, rhs: Self) {
        self.a += rhs.a;
        self.b += rhs.b;
    }
}

/// Adding a non-MyNum to MyNum.
impl<T: RealField + Copy> Add<T> for MyNum<T> {
    type Output = MyNum<T>;

    fn add(self, rhs: T) -> Self::Output {
        MyNum {
            a: self.a + rhs,
            b: self.b + rhs,
        }
    }
}
impl<T: RealField + Copy> Add<&T> for MyNum<T> {
    type Output = MyNum<T>;

    fn add(self, rhs: &T) -> Self::Output {
        MyNum {
            a: self.a + *rhs,
            b: self.b + *rhs,
        }
    }
}

impl<T: RealField + Copy> Add<T> for &MyNum<T> {
    type Output = MyNum<T>;

    fn add(self, rhs: T) -> Self::Output {
        MyNum {
            a: self.a + rhs,
            b: self.b + rhs,
        }
    }
}

impl<T: RealField + Copy> Add<&T> for &MyNum<T> {
    type Output = MyNum<T>;

    fn add(self, rhs: &T) -> Self::Output {
        MyNum {
            a: self.a + *rhs,
            b: self.b + *rhs,
        }
    }
}

impl<T: RealField + Copy> AddAssign<T> for MyNum<T> {
    fn add_assign(&mut self, rhs: T) {
        self.a += rhs;
        self.b += rhs;
    }
}
impl<T: RealField + Copy> AddAssign<T> for &mut MyNum<T> {
    fn add_assign(&mut self, rhs: T) {
        self.a += rhs;
        self.b += rhs;
    }
}
impl<T: RealField + Copy> AddAssign<&T> for MyNum<T> {
    fn add_assign(&mut self, rhs: &T) {
        self.a += *rhs;
        self.b += *rhs;
    }
}

impl<T: RealField + Copy> AddAssign<&T> for &mut MyNum<T> {
    fn add_assign(&mut self, rhs: &T) {
        self.a += *rhs;
        self.b += *rhs;
    }
}

fn main() {
    let my_num = MyNum { a: 0.0, b: 15.0 };
    println!("This works: {:?}", &my_num + 32.0f64);

    let vec_a = Vector2::<MyNum<f64>>::new(MyNum { a: 0.0, b: 0.0 }, MyNum { a: 0.0, b: 0.0 });
    let vec_b = Vector2::<MyNum<f64>>::new(MyNum { a: 0.0, b: 0.0 }, MyNum { a: 32.0, b: 64.0 });

    println!("vec_a + vec_b: {:?}", &vec_a + &vec_b);

    let vec_float = Vector2::<f64>::new(1.0, 2.0);
    // The line below does not compile
    println!("vec_float + a = {}", &vec_a + &vec_float);
}


Here is the error I get:

╰─➤  cargo run --release --bin my_num_vec_add                                                             101 â†ĩ
   Compiling fmad v0.1.0 (/Users/andrei/work/fmad)
error[E0277]: cannot add `&Matrix<f64, Const<3>, Const<1>, ArrayStorage<f64, 3, 1>>` to `&Matrix<MyNum<f64>, Const<3>, Const<1>, ArrayStorage<MyNum<f64>, 3, 1>>`
   --> src/bin/my_num_vec_add.rs:147:43
    |
147 |     println!("vec_float + a = {}", &vec_a + &vec_float);
    |                                           ^ no implementation for `&Matrix<MyNum<f64>, Const<3>, Const<1>, ArrayStorage<MyNum<f64>, 3, 1>> + &Matrix<f64, Const<3>, Const<1>, ArrayStorage<f64, 3, 1>>`
    |
    = help: the trait `Add<&Matrix<f64, Const<3>, Const<1>, ArrayStorage<f64, 3, 1>>>` is not implemented for `&Matrix<MyNum<f64>, Const<3>, Const<1>, ArrayStorage<MyNum<f64>, 3, 1>>`
    = help: the following other types implement trait `Add<Rhs>`:
              <&'a Matrix<T, R1, C1, SA> as Add<&'b Matrix<T, R2, C2, SB>>>
              <&'a Matrix<T, R1, C1, SA> as Add<Matrix<T, R2, C2, SB>>>
              <Matrix<T, R1, C1, SA> as Add<&'b Matrix<T, R2, C2, SB>>>
              <Matrix<T, R1, C1, SA> as Add<Matrix<T, R2, C2, SB>>>

For more information about this error, try `rustc --explain E0277`.

Thank you in advance for any pointers!

It looks to me like nalgebra only implements addition for Matrix[1] when the type in both matrices is the same.

You can look at the docs page for Matrix to see the impls, though it is... a bit of a mess due to how generic Matrix is.

I'm not sure if that's an oversight on the part of nalgebra or if there's a reason making those impls more generic wouldn't work


  1. The Vector types are just aliases for Matrix with some of the parameters filled in ↩ī¸Ž

2 Likes

This is the impl in question, where the T corresponds to the T in Vector2<T> (and is the same for both operands).

2 Likes

Thank you for the quick and thoughtful responses.

Since you are more experienced with Rust and nalgebra, do you think having allowing operands to have different Ts would make sense?

For example, one classic example would adding a Matrix<f64> and a Matrix<f32>---to me it makes sense to let the addition happen directly as opposed to first promoting the RHS to f64.

A similar line of reasoning goes for complex numbers; it should be IMHO OK to do the following:

    let c1 = Complex::new(10.0, 3.0);
    let c2 = Complex::new(20.0, 3.0);
    let complex_vec = Vector2::<Complex<f32>>::new(c1, c2);

    println!("sum: {:?}", &complex_vec + &vec_float);

but it currently doesn't compile since one matrix is Complex and the other is not.

I'm wondering if it's worth possibly contributing to the library to allow a T and U to be specified for matrix ops, as long as impl<T, U> Add<U> for T exists; basically if T and U can be used to call the functions below, shouldn't they be safe to be used in the corresponding matrices?

fn my_adder<T, U>(tt: T, uu: U) -> U
where
    T: Add<U, Output = U>,
{
    tt + uu
}

I'm not really experienced with nalgebra; I did look at the code briefly, which is macro heavy and uses unsafe, but didn't reach any conclusions.

You should probably ask the nalgebra maintainers more directly, or open a feature request perhaps.

1 Like

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.