rust-num / num-traits

Numeric traits for generic mathematics in Rust
Apache License 2.0
719 stars 133 forks source link

Two::two() and OneHalf::one_half() #330

Closed sunsided closed 3 months ago

sunsided commented 3 months ago

This adds the traits Two, ConstTwo, as well as OneHalf and ConstOneHalf respectively. It's like One, just twice as good ... or half.


This may seem overkill at first glance, but I have found myself re-implementing

let two = T::one() + T::one();
let one_half = T::one() / two;

repeatedly when implementing digital filters that are either floating- or fixed-point. The need arises for example when differentiating nonlinear equations in order to provide their Laplacians for linear approximation. While a square can be implemented trivially (a * a), the differentiation of it (2 * a) causes the issue.


I provided OneHalf/ConstOneHalf since they are dual to Two/ConstTwo. They could be derived, but that would require an additional Div<T> trait bound (whereas Mul<T> is already required by One). Given that multiplication is generally faster than division, having it at hand is useful.

cuviper commented 3 months ago

I hate to invoke a fallacy, but this really does seem like a slippery slope. We're simply not going to add traits for every possibly-interesting numeric value, but at least 0 and 1 have status as additive and multiplicative identities. You can also consider From for more complex constants.

While a square can be implemented trivially (a * a), the differentiation of it (2 * a) causes the issue.

If you accept a * a, why not use a + a for the latter? Are you really operating with generic Mul and not Add?

sunsided commented 3 months ago

Yeah, precisely why I said overkill. :) It's the same discussion in rust-lang about FRAC_ constants. In my case, it looks like

let one = T::one();
let two = one + one;
let x = vec.x * (one - two * (q2 * q2 + q3 * q3))
    + vec.y * two * (q1 * q2 - q0 * q3)
    + vec.z * two * (q1 * q3 + q0 * q2);

// and 
let y = vec.x * (q1 * q2 + q0 * q3)
    + vec.y * (one - two * (q1 * q1 + q3 * q3))
    + vec.z * two * (q2 * q3 - q0 * q1);

// and 
let z = vec.x * (q1 * q3 - q0 * q2)
    + vec.y * two * (q2 * q3 + q0 * q1)
    + vec.z * (one - two * (q1 * q1 + q2 * q2));

then in the next it's

let one = T::one();
let two = one + one;
let sinp = two * (w * y - z * x);
// TODO: If sin >= 1.0 || sin <= -1.0, clamp to +/- pi/2 instead
sinp.arcsin()

and then rinse and repeat. PI is a common factor, and you see the division by two in the note as well. It's not like there is no solution, it's just getting repetitive and I figured others may have encountered similar situations.

Personally I don't mind having these constants as much, but - as I said - I completely understand it's not exactly a popular standpoint. :)

If you accept a * a, why not use a + a for the latter?

As for that, I totally do accept it. That said, depending on the actual number type the two will potentially invoke a computation that always can be avoided, whereas the square cannot be constant in general. I'm not talking impl Two for f32 where the compiler easily figures it out, it's more along the lines of FixedPointQ8_24 where we're now shuffling bytes around with every operation. Contrived example, simply to show the point.

cuviper commented 3 months ago

Ok, I'm going to take the stance that this isn't really needed. Thanks anyway!