Generic numeric code involving vectors and scalars, problem with types/traits

Hi, I'm new to Rust and in order to learn I decided to start implementing some numeric algorithms I'm familiar with, like Runge Kutta.

I implemented it for the scalar case using f64 types, worked as expected. Now I was trying to make the function use generic types so that it would work on e.g. vector-valued functions. This is where I got stuck...

use num_traits::Float;
use std::ops::{Add, Mul};

fn runge_kutta_4<V, S>(f: fn(S, V) -> V, y_curr: V, t_curr: S, step_size: S) -> V
where
    V: Add<Output = V> + Mul<Output = V>,
    S: Float,
{
    let half_step = 0.5 * step_size;
    let k1 = f(t_curr, y_curr);
    let k2 = f(t_curr + half_step, y_curr + half_step * k1);
    let k3 = f(t_curr + half_step, y_curr + half_step * k2);
    let k4 = f(t_curr + step_size, y_curr + step_size * k3);

    y_curr + step_size / 6. * (k1 + 2. * k2 + 2. * k3 + k4)
}

what I need to tell Rust is that the type V supports addition with other Vs and multiplication with elements of type S, and S can be multiplied and divided with elements of type f64 (or maybe cast all my constants to Float? but that would add a lot of clutter I think).

As of Now I think I'm telling it that V can be added to other Vs and multiplied with other Vs? (which is not what I need) But if I change Mul<Output = V> by Mul<Output = S> it doesn't work either.

How would I do this?

I edited your code on the playground until it compiled; here are the things I needed to change:

  • You need to explicity convert your literals into type S somehow
  • The Mul trait isn’t commutative, so V: Mul<S> has to be used as v * s instead of s * v
  • Arithmetic operators in Rust move their arguments, so you need to deal with that. Either:
    • Require V: Copy
    • Require V: Clone, and make explicit calls to clone() where required
    • Use references in your calculations (which needs a bound like for<‘a> &’a V: Mul<S, Output=V>)
  • Remove the final semicolon to return the value

use num_traits::Float;
use std::ops::{Add, Mul};

fn runge_kutta_4<V, S>(f: fn(S, V) -> V, y_curr: V, t_curr: S, step_size: S) -> V
where
    V: Add<Output = V> + Mul<S, Output = V> + Clone,
    S: Float + From<f64>,
{
    let s = <S as From<f64>>::from;
    let half_step = s(0.5) * step_size;
    let k1 = f(t_curr, y_curr.clone());
    let k2 = f(t_curr + half_step, y_curr.clone() + k1.clone() * half_step);
    let k3 = f(t_curr + half_step, y_curr.clone() + k2.clone() * half_step);
    let k4 = f(t_curr + step_size, y_curr.clone() + k3.clone() * step_size);

    y_curr + (k1 + k2 * s(2.) + k3 * s(2.) + k4) * (step_size / s(6.))
}
2 Likes

Here's what it took to get it working:

use num_traits::Float;
use std::ops::{Add, Mul};

fn runge_kutta_4<V, S>(f: fn(S, V) -> V, y_curr: V, t_curr: S, step_size: S) -> V
where
    V: Clone + Add<Output = V> + Mul<S, Output = V>,
    S: Float + From<f64> + Mul<Output = S>,
{
    let half_step = <S as From<_>>::from(0.5) * step_size;
    let k1 = f(t_curr, y_curr.clone());
    let k2 = f(t_curr + half_step, y_curr.clone() + k1.clone() * half_step);
    let k3 = f(t_curr + half_step, y_curr.clone() + k2.clone() * half_step);
    let k4 = f(t_curr + step_size, y_curr.clone() + k3.clone() * step_size);

    let two = <S as From<_>>::from(2.0);

    y_curr + (k1 + k2 * two + k3 * two + k4) * (step_size / <S as From<_>>::from(6.0))
}

There were quite a few problems:

  • As you noted, you need to specify that V * S is available, done by specifying the Rhs parameter to the Mul trait.
  • You were using S * V in a number of places, so I had to reverse the arguments. Multiplication is commutative, but Mul is not.
  • You can't use literals with generic types, so I had to explicitly require a conversion from f64 -> S. The <S as From<_>> construct is because one of the num_traits traits also defines a from method, so I had to disambiguate.
  • You re-used Vs in a number of places, so I had to require Clone.

Edit: You know, it would be good if the forum had a more obvious "hey, someone has posted another comment while you've been editing this one" signal. :stuck_out_tongue:

4 Likes

thank you two, I didn't realize Mul is not commutative but ofc makes sense mathematically.

The clone calls don't look aesthetic but I think I won't delve into references and lifetimes for now :smile: