Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add support for Complex number storage types. #287

Merged
merged 2 commits into from
May 16, 2022

Conversation

TobTobXX
Copy link
Contributor

@TobTobXX TobTobXX commented Mar 10, 2022

I have no idea what I'm doing.

Based on e2d5f20. Addresses #286.

Todo:

  • Code compiles
  • Code works (just some light testing in a simple project)
  • Tests

Discuss before merge:

  • Also support Complex<T>, or maybe only Complex<T> instead of Complex32, Complex64?
  • Are the features correct like this? I have never used features /shrug

Copy link
Owner

@iliekturtles iliekturtles left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! I did a very brief review and you're on the right track. I'll review in more detail tomorrow and see if I can assist with some of the trait implementations that are still missing.

Cargo.toml Outdated Show resolved Hide resolved
@TobTobXX
Copy link
Contributor Author

TobTobXX commented Mar 10, 2022

Oh, is it a problem if the types don't implement PartialOrd? As far as I can tell, num::complex::Complex<T> doesn't implement it (which does make some kind of mathematical sense). And as far as I can tell, num::Num doesn't automatically implement PartialOrd.

It seems to be a problem because uom::ConversionFactor<V> can only be implemented for PartialOrd types:

uom/src/lib.rs

Line 441 in fe7f0b3

lib::cmp::PartialOrd

error[E0277]: can't compare `Complex<f32>` with `Complex<f32>`
   --> src/lib.rs:664:10
    |
664 |     impl crate::ConversionFactor<V> for V {
    |          ^^^^^^^^^^^^^^^^^^^^^^^^^^ no implementation for `Complex<f32> < Complex<f32>` and `Complex<f32> > Complex<f32>`
    |
    = help: the trait `PartialOrd` is not implemented for `Complex<f32>`
note: required by a bound in `ConversionFactor`
   --> src/lib.rs:454:5
    |
453 | pub trait ConversionFactor<V>:
    |           ---------------- required by a bound in this
454 |     lib::cmp::PartialOrd
    |     ^^^^^^^^^^^^^^^^^^^^ required by this bound in `ConversionFactor`

Kinda crazy how some intuitive promises are broken by math.

Copy link
Owner

@iliekturtles iliekturtles left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the PartialOrd issue can be resolved by using Complex<T>'s underlying type as the ConversionFactor<V> type. See the two suggestions in this review for the changes that I believe are needed. I haven't attempted to compile so additional changes are likely needed.

src/storage_types.rs Show resolved Hide resolved
src/lib.rs Outdated Show resolved Hide resolved
@TobTobXX
Copy link
Contributor Author

It may have worked (I'm still getting errors, but I think unrelated ones, I'll work on them after). Just to check my thought process, could you confirm/correct this (applies to f64 too):

  1. uom needs to know how to convert Complex32 numbers (eg. between 12i+3 km to m)
  2. Desired behavior is just scaling (eg. 12i+3 km == 1e3 * (12i+3) m)
  3. Because having this scaling factor be a complex number has some complications (ie. prev. errors), we want the conversion value to be a f32 (conversion factors don't need an imaginary part anyway).
  4. I need to implement Conversion<Complex32> to define how conversions should work.
  5. The implementation ConversionFactor<Complex32> for f32 declares that Complex32 numbers can be converted with f32 numbers.
  6. Conversion<Complex32, T = f32>::conversion() should return the norm of the complex number, since it can be multiplied by this factor again to get the original number.
  7. ConversionFactor<Complex32>::value() for Self = f32 should return (self + 0i). Because 12i+3km == (1e3+0i) * (12i+3) m (so it is basically just glue to satisfy the type system).

Also, I have no idea what that Conversion::constant() function is doing with this evil 0.0 != -0.0 stuff. I'll just copy your suggestion... (Is it even needed if I overwrite the Conversion::conversion() fn? Or should I overwrite `Conversion::coefficient() instead?)

Thank you for your patience and help 🤗

@TobTobXX
Copy link
Contributor Author

It seems to work now btw :D

@iliekturtles
Copy link
Owner

Excellent! I started the build and will review in more detail later today.

Copy link
Owner

@iliekturtles iliekturtles left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests::Test needs to be implemented. See the relevant code for f32/f64 below. You'll want to use similar approx comparisons for the real part. I assume you'll want approx comparisons for the imaginary part.

uom/src/tests/mod.rs

Lines 99 to 142 in fe7f0b3

storage_types! {
types: Float;
use crate::num::Float;
// const EPSILON: V = 64.0 * V::epsilon(); //error[E0015]; calls in constants are limited...
const EPS_FACTOR: V = 0.5;
const ULPS: u32 = 3;
impl super::super::Test for V {
/// Assert that `lhs` and `rhs` are exactly equal.
fn assert_eq(lhs: &Self, rhs: &Self) {
match (lhs.is_nan(), rhs.is_nan()) {
(true, true) => {}
_ => { assert_eq!(lhs, rhs); }
}
}
/// Assert that `lhs` and `rhs` are approximately equal for floating point types or
/// exactly equal for other types.
fn assert_approx_eq(lhs: &Self, rhs: &Self) {
match (lhs.is_nan(), rhs.is_nan()) {
(true, true) => {}
_ => {
assert_ulps_eq!(lhs, rhs, epsilon = EPS_FACTOR * V::epsilon(),
max_ulps = ULPS);
}
}
}
/// Exactly compare `lhs` and `rhs` and return the result.
fn eq(lhs: &Self, rhs: &Self) -> bool {
(lhs.is_nan() && rhs.is_nan())
|| lhs == rhs
}
/// Approximately compare `lhs` and `rhs` for floating point types or exactly compare
/// for other types and return the result.
fn approx_eq(lhs: &Self, rhs: &Self) -> bool {
(lhs.is_nan() && rhs.is_nan())
|| ulps_eq!(lhs, rhs, epsilon = EPS_FACTOR * V::epsilon(), max_ulps = ULPS)
}
}
}

src/storage_types.rs Outdated Show resolved Hide resolved
@TobTobXX
Copy link
Contributor Author

I started some work. A lot of code duplication and still quite a few errors, but could you look over these points:

src/tests/system.rs Outdated Show resolved Hide resolved
src/tests/system.rs Outdated Show resolved Hide resolved
@iliekturtles
Copy link
Owner

Approved the build again so that you'll get test results. I'm hoping to have time tomorrow to do an in depth review.

@TobTobXX
Copy link
Contributor Author

TobTobXX commented Mar 15, 2022

Yeah no it'll still error out, but I don't know why.

I'm having this error (and 24 similar more):

error[E0277]: the trait bound `Complex<f32>: num_traits::Float` is not satisfied
   --> src/tests/system.rs:604:64
    |
604 |                         assert!(!Length::new::<meter>(complex).is_finite());
    |                                                                ^^^^^^^^^ the trait `num_traits::Float` is not implemented for `Complex<f32>`
    |
note: required by a bound in `tests::Quantity::<D, U, V>::is_finite`
   --> src/system.rs:625:20
    |
623 |               pub fn is_finite(self) -> bool
    |                      --------- required by a bound in this
624 |               where
625 |                   V: $crate::num::Float,
    |                      ^^^^^^^^^^^^^^^^^^ required by this bound in `tests::Quantity::<D, U, V>::is_finite`
    |
   ::: src/tests/mod.rs:55:1
    |
55  | / system! {
56  | |     quantities: Q {
57  | |         length: meter, L;
58  | |         mass: kilogram, M;
...   |
65  | |     }
66  | | }
    | |_- in this macro invocation
    = note: this error originates in the macro `system` (in Nightly builds, run with -Z macro-backtrace for more info)

referencing this.

... but is_finite() is implemented for Complex<T> where T: FloatCore and FloatCore is implemented for f32.

I have no idea what is going on here. If you can help, I'd greatly appreciate it. (Take your time :) )

@iliekturtles
Copy link
Owner

Looks like the issue is that Complex<T> implements the methods when T: FloatCore, but Complex<T> itself does not implement FloatCore (or Float). This may require a storage_types! call in system.rs or quantity.rs. I'll check in more detail during my review.

@iliekturtles
Copy link
Owner

Despite my best intentions I just didn't get to doing a deeper review. Hopefully this weekend or early next week.

@TobTobXX
Copy link
Contributor Author

no problem, I'm not in a hurry.

Copy link
Owner

@iliekturtles iliekturtles left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the review for the first four files. I haven't gotten through the remaining files and the the trait bound Complex: num_traits::Float is not satisfied error still exists. Will try to get through the rest tomorrow.

.github/workflows/ci-full-test-suite.yml Show resolved Hide resolved
src/lib.rs Outdated Show resolved Hide resolved
src/lib.rs Outdated Show resolved Hide resolved
src/lib.rs Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
src/lib.rs Outdated Show resolved Hide resolved
@TobTobXX
Copy link
Contributor Author

Will address the last review later. Rebased, will squash sometime later too...

Copy link
Owner

@iliekturtles iliekturtles left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the changes in this review and if you temporarily comment out all of the methods like is_normal that rely on Float/FloatCore the crate will compile and run tests! Some of the tests are failing and I haven't figured out what the solution for the Float/FloatCore is going to be yet.

I'm also noticing that a lot of the Complex storage_types! blocks are a copy-and-paste of the Float blocks with V replaced with VV. I don't want to block acceptance of this PR on reducing this duplicate code. Please post If you have any thoughts about the best way to reduce this duplication.

Add new features to extern crate approx; statement:

- #[cfg(all(test, any(feature = "f32", feature = "f64")))]
+ #[cfg(all(test, any(feature = "f32", feature = "f64", feature = "complex32", feature = "complex64")))]
  #[macro_use]
  extern crate approx;

Add to src/tests/asserts.rs:

storage_types! {
    types: Complex;

    use super::*;

    assert_impl_all!(Quantity<Q<Z0, Z0, Z0>, U<V>, V>:
        Clone, Copy, Debug, PartialEq, Send, Sync, Unpin);
    #[cfg(feature = "std")]
    assert_impl_all!(Quantity<Q<Z0, Z0, Z0>, U<V>, V>:
        RefUnwindSafe, UnwindSafe);
    assert_not_impl_any!(Quantity<Q<Z0, Z0, Z0>, U<V>, V>:
        Binary, Display, Eq, Hash, LowerExp, LowerHex, Octal, Ord, PartialOrd, UpperExp, UpperHex);
    assert_impl_all!(QuantityArguments<Q<Z0, Z0, Z0>, U<V>, V, meter>:
        Clone, Copy, Debug, Display, LowerExp, Send, Sync, Unpin, UpperExp);
    #[cfg(feature = "std")]
    assert_impl_all!(QuantityArguments<Q<Z0, Z0, Z0>, U<V>, V, meter>:
        RefUnwindSafe, UnwindSafe);
    assert_not_impl_any!(QuantityArguments<Q<Z0, Z0, Z0>, U<V>, V, meter>:
        Binary, Eq, Hash, LowerHex, Octal, Ord, PartialEq, PartialOrd, UpperHex);
}

src/lib.rs Outdated Show resolved Hide resolved
src/tests/mod.rs Outdated Show resolved Hide resolved
src/tests/mod.rs Outdated Show resolved Hide resolved
src/storage_types.rs Outdated Show resolved Hide resolved
src/storage_types.rs Outdated Show resolved Hide resolved
src/tests/mod.rs Outdated Show resolved Hide resolved
src/tests/mod.rs Outdated Show resolved Hide resolved
@TobTobXX
Copy link
Contributor Author

TobTobXX commented Apr 3, 2022

Ok, sorry I got a bit delayed on this, but all should be resolved now.

I commented out the tests where the weird issues with T: Float and T: FloatCore appear, and added a notice.

And about the duplication with VV instead of V: Nope, got no idea. I'm no macro god like you. 😂

@iliekturtles
Copy link
Owner

Excellent! I'll try to get to reviewing the latest changes this week.

.github/workflows/ci-full-test-suite.yml Outdated Show resolved Hide resolved
src/lib.rs Show resolved Hide resolved
src/lib.rs Outdated Show resolved Hide resolved
src/lib.rs Outdated Show resolved Hide resolved
src/lib.rs Outdated Show resolved Hide resolved
// Somehow Complex<f32|f64> don't implement these methods, even though they
// should, bc f32 / f64 implement Float and FloatCore:
// - https://docs.rs/num/latest/num/struct.Complex.html#impl-5
// - https://docs.rs/num/latest/num/struct.Complex.html#impl-4
//
// See discussion here:
// https://github.com/iliekturtles/uom/pull/287#pullrequestreview-922005933
/*
mod complex {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spent some time looking at this tonight and I believe what we'll need to do is explicitly implement these functions for f32, f64, Complex32, and Complex64 using the storage_types! macro. Complex<T> doesn't implement Float/FloatCore, but it does implement these methods when the underlying type, T, implements FloatCore.

storage_types! {
    types: Float, Complex;

    impl<...> Quantity<..., V /*V type alias defined by storage_types!*/> {
        fn is_infinite(self) -> bool { self.value.is_infinite() }
    }
}

// I don't think a separate storage_types! is necessary. The code should be able to be the same.
//storage_types! {
//    types: Complex;
//    
//    impl<...> Quantity<..., V> {
//        fn is_infinite(...) -> ... { ... }
//    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't completely understand why I should implement these methods on f32 and f64 too, num implements them for these two just fine, doesn't it?

As for doing it for the complexes... it's weird. Before the compiler was complaining that it didn't find an implementation for is_infinite et al., but now when I implement it with the following patch, it tells me it has duplicate implementation:

diff --git a/src/tests/system.rs b/src/tests/system.rs
index 1355f80..cad6de9 100644
--- a/src/tests/system.rs
+++ b/src/tests/system.rs
@@ -584,7 +584,6 @@ mod fixed {
 //
 // See discussion here:
 // https://github.com/iliekturtles/uom/pull/287#pullrequestreview-922005933
-/*
 mod complex {
     storage_types! {
         types: Complex;
@@ -593,6 +592,11 @@ mod complex {
 
         Q!(crate::tests, V);
 
+        impl<D, U> Quantity<D, U, V /*V type alias defined by storage_types!*/> {
+            fn is_infinite(self) -> bool { self.value.is_infinite() }
+            fn is_finite(self) -> bool { self.value.is_finite() }
+        }
+
         #[test]
         fn fp_categories() {
             let float_categories = vec![
@@ -724,7 +728,6 @@ mod complex {
         }
     }
 }
-*/
 
 mod non_complex {
     storage_types! {
error[E0592]: duplicate definitions with name `is_infinite`
    |
   ::: src/tests/system.rs:596:13
    |
596 |               fn is_infinite(self) -> bool { self.value.is_infinite() }
    |               ---------------------------- other definition for `is_infinite`
    |
   ::: src/tests/mod.rs:55:1
    |
55  | / system! {
56  | |     quantities: Q {
57  | |         length: meter, L;
58  | |         mass: kilogram, M;
...   |
65  | |     }
66  | | }
    | |_- in this macro invocation
   --> src/system.rs:613:13
    |
613 | /             pub fn is_infinite(self) -> bool
614 | |             where
615 | |                 V: $crate::num::Float,
    | |______________________________________^ duplicate definitions for `is_infinite`
    |
    = note: this error originates in the macro `system` (in Nightly builds, run with -Z macro-backtrace for more info)

Maybe I am also implementing it at the wrong location?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I see where the duplicates are and will take a look in the next few days!

@iliekturtles
Copy link
Owner

We're getting closer. I got all the tests compiling by moving the general function implementations where V: Float into explicit implementations for each concrete type V. There are now some test failures that I didn't get a chance to review. Could you review, and possibly apply, the patch below?

diff --git a/src/system.rs b/src/system.rs
index 6324553..016f714 100644
--- a/src/system.rs
+++ b/src/system.rs
@@ -596,47 +596,6 @@ macro_rules! system {
             U: Units<V> + ?Sized,
             V: $crate::num::Num + $crate::Conversion<V>,
         {
-            /// Returns `true` if this value is `NAN` and `false` otherwise.
-            #[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
-            #[inline(always)]
-            pub fn is_nan(self) -> bool
-            where
-                V: $crate::num::Float,
-            {
-                self.value.is_nan()
-            }
-
-            /// Returns `true` if this value is positive infinity or negative infinity and
-            /// `false` otherwise.
-            #[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
-            #[inline(always)]
-            pub fn is_infinite(self) -> bool
-            where
-                V: $crate::num::Float,
-            {
-                self.value.is_infinite()
-            }
-
-            /// Returns `true` if this number is neither infinite nor `NAN`.
-            #[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
-            #[inline(always)]
-            pub fn is_finite(self) -> bool
-            where
-                V: $crate::num::Float,
-            {
-                self.value.is_finite()
-            }
-
-            /// Returns `true` if the number is neither zero, infinite, subnormal, or `NAN`.
-            #[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
-            #[inline(always)]
-            pub fn is_normal(self) -> bool
-            where
-                V: $crate::num::Float,
-            {
-                self.value.is_normal()
-            }
-
             /// Returns the floating point category of the number. If only one property is
             /// going to be tested, it is generally faster to use the specific predicate
             /// instead.
@@ -649,43 +608,6 @@ macro_rules! system {
             }
 
             std! {
-            /// Takes the cubic root of a number.
-            ///
-            #[cfg_attr(all(feature = "si", feature = "f32"), doc = " ```rust")]
-            #[cfg_attr(not(all(feature = "si", feature = "f32")), doc = " ```rust,ignore")]
-            /// # use uom::si::f32::*;
-            /// # use uom::si::volume::cubic_meter;
-            /// let l: Length = Volume::new::<cubic_meter>(8.0).cbrt();
-            /// ```
-            ///
-            /// The input type must have dimensions divisible by three:
-            ///
-            #[cfg_attr(all(feature = "si", feature = "f32"), doc = " ```rust,compile_fail")]
-            #[cfg_attr(not(all(feature = "si", feature = "f32")), doc = " ```rust,ignore")]
-            /// # use uom::si::f32::*;
-            /// # use uom::si::area::square_meter;
-            /// // error[E0271]: type mismatch resolving ...
-            /// let r = Area::new::<square_meter>(8.0).cbrt();
-            /// ```
-            #[inline(always)]
-            pub fn cbrt(
-                self
-            ) -> Quantity<
-                $quantities<$($crate::typenum::PartialQuot<D::$symbol, $crate::typenum::P3>),+>,
-                U, V>
-            where
-                $(D::$symbol: $crate::lib::ops::PartialDiv<$crate::typenum::P3>,
-                <D::$symbol as $crate::lib::ops::PartialDiv<$crate::typenum::P3>>::Output: $crate::typenum::Integer,)+
-                D::Kind: $crate::marker::Div,
-                V: $crate::num::Float,
-            {
-                Quantity {
-                    dimension: $crate::lib::marker::PhantomData,
-                    units: $crate::lib::marker::PhantomData,
-                    value: self.value.cbrt(),
-                }
-            }
-
             autoconvert! {
             /// Calculates the length of the hypotenuse of a right-angle triangle given the legs.
             #[inline(always)]
@@ -769,39 +691,6 @@ macro_rules! system {
                 self.value.is_sign_negative()
             }
 
-            std! {
-            /// Fused multiply-add. Computes `(self * a) + b` with only one rounding error.
-            /// This produces a more accurate result with better performance than a separate
-            /// multiplication operation followed by an add.
-            ///
-            /// ## Generic Parameters
-            /// * `Da`: Dimension for parameter `a`.
-            /// * `Ua`: Base units for parameter `a`.
-            /// * `Ub`: Base units for parameter `b`.
-            #[inline(always)]
-            pub fn mul_add<Da, Ua, Ub>(
-                self,
-                a: Quantity<Da, Ua, V>,
-                b: Quantity<$quantities<$($crate::typenum::Sum<D::$symbol, Da::$symbol>),+>, Ub, V>,
-            ) -> Quantity<$quantities<$($crate::typenum::Sum<D::$symbol, Da::$symbol>),+>, U, V>
-            where
-                $(D::$symbol: $crate::lib::ops::Add<Da::$symbol>,
-                <D::$symbol as $crate::lib::ops::Add<Da::$symbol>>::Output: $crate::typenum::Integer,)+
-                D::Kind: $crate::marker::Mul,
-                V: $crate::num::Float,
-                Da: Dimension + ?Sized,
-                Da::Kind: $crate::marker::Mul,
-                Ua: Units<V> + ?Sized,
-                Ub: Units<V> + ?Sized,
-            {
-                // (self * a) + b
-                Quantity {
-                    dimension: $crate::lib::marker::PhantomData,
-                    units: $crate::lib::marker::PhantomData,
-                    value: self.value.mul_add(a.value, b.value),
-                }
-            }}
-
             /// Takes the reciprocal (inverse) of a number, `1/x`.
             ///
             #[cfg_attr(all(feature = "si", feature = "f32"), doc = " ```rust")]
@@ -827,76 +716,6 @@ macro_rules! system {
                 }
             }
 
-            std! {
-            /// Raises a quantity to an integer power.
-            ///
-            #[cfg_attr(all(feature = "si", feature = "f32"), doc = " ```rust")]
-            #[cfg_attr(not(all(feature = "si", feature = "f32")), doc = " ```rust,ignore")]
-            /// # use uom::si::f32::*;
-            /// # use uom::si::length::meter;
-            /// use uom::typenum::P2;
-            ///
-            /// let a: Area = Length::new::<meter>(3.0).powi(P2::new());
-            /// ```
-            ///
-            /// ## Generic Parameters
-            /// * `E`: `typenum::Integer` power.
-            #[inline(always)]
-            pub fn powi<E>(
-                self, _e: E
-            ) -> Quantity<$quantities<$($crate::typenum::Prod<D::$symbol, E>),+>, U, V>
-            where
-                $(D::$symbol: $crate::lib::ops::Mul<E>,
-                <D::$symbol as $crate::lib::ops::Mul<E>>::Output: $crate::typenum::Integer,)+
-                D::Kind: $crate::marker::Mul,
-                E: $crate::typenum::Integer,
-                V: $crate::num::Float,
-            {
-                Quantity {
-                    dimension: $crate::lib::marker::PhantomData,
-                    units: $crate::lib::marker::PhantomData,
-                    value: self.value.powi(E::to_i32()),
-                }
-            }
-
-            /// Takes the square root of a number. Returns `NAN` if `self` is a negative
-            /// number.
-            ///
-            #[cfg_attr(all(feature = "si", feature = "f32"), doc = " ```rust")]
-            #[cfg_attr(not(all(feature = "si", feature = "f32")), doc = " ```rust,ignore")]
-            /// # use uom::si::f32::*;
-            /// # use uom::si::area::square_meter;
-            /// let l: Length = Area::new::<square_meter>(4.0).sqrt();
-            /// ```
-            ///
-            /// The input type must have dimensions divisible by two:
-            ///
-            #[cfg_attr(all(feature = "si", feature = "f32"), doc = " ```rust,compile_fail")]
-            #[cfg_attr(not(all(feature = "si", feature = "f32")), doc = " ```rust,ignore")]
-            /// # use uom::si::f32::*;
-            /// # use uom::si::length::meter;
-            /// // error[E0271]: type mismatch resolving ...
-            /// let r = Length::new::<meter>(4.0).sqrt();
-            /// ```
-            #[inline(always)]
-            pub fn sqrt(
-                self
-            ) -> Quantity<
-                $quantities<$($crate::typenum::PartialQuot<D::$symbol, $crate::typenum::P2>),+>,
-                U, V>
-            where
-                $(D::$symbol: $crate::typenum::PartialDiv<$crate::typenum::P2>,
-                <D::$symbol as $crate::typenum::PartialDiv<$crate::typenum::P2>>::Output: $crate::typenum::Integer,)+
-                D::Kind: $crate::marker::Div,
-                V: $crate::num::Float,
-            {
-                Quantity {
-                    dimension: $crate::lib::marker::PhantomData,
-                    units: $crate::lib::marker::PhantomData,
-                    value: self.value.sqrt(),
-                }
-            }}
-
             /// Returns the maximum of the two quantities.
             #[inline(always)]
             pub fn max(self, other: Self) -> Self
@@ -924,6 +743,194 @@ macro_rules! system {
             }
         }
 
+        // Explicitly definte floating point methods for float and complex storage types.
+        // `Complex<T>` doesn't implement `Float`/`FloatCore`, but it does implement these methods
+        // when the underlying type, `T`, implements `FloatCore`.
+        mod float {
+            storage_types! {
+                types: Float, Complex;
+
+                use super::super::*;
+
+                impl<D, U> Quantity<D, U, V>
+                where
+                    D: Dimension + ?Sized,
+                    U: Units<V> + ?Sized,
+                {
+                    /// Returns `true` if this value is `NAN` and `false` otherwise.
+                    #[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
+                    #[inline(always)]
+                    pub fn is_nan(self) -> bool
+                    {
+                        self.value.is_nan()
+                    }
+
+                    /// Returns `true` if this value is positive infinity or negative infinity and
+                    /// `false` otherwise.
+                    #[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
+                    #[inline(always)]
+                    pub fn is_infinite(self) -> bool
+                    {
+                        self.value.is_infinite()
+                    }
+
+                    /// Returns `true` if this number is neither infinite nor `NAN`.
+                    #[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
+                    #[inline(always)]
+                    pub fn is_finite(self) -> bool
+                    {
+                        self.value.is_finite()
+                    }
+
+                    /// Returns `true` if the number is neither zero, infinite, subnormal, or `NAN`.
+                    #[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
+                    #[inline(always)]
+                    pub fn is_normal(self) -> bool
+                    {
+                        self.value.is_normal()
+                    }
+
+                    std! {
+                    /// Takes the cubic root of a number.
+                    ///
+                    #[cfg_attr(all(feature = "si", feature = "f32"), doc = " ```rust")]
+                    #[cfg_attr(not(all(feature = "si", feature = "f32")), doc = " ```rust,ignore")]
+                    /// # use uom::si::f32::*;
+                    /// # use uom::si::volume::cubic_meter;
+                    /// let l: Length = Volume::new::<cubic_meter>(8.0).cbrt();
+                    /// ```
+                    ///
+                    /// The input type must have dimensions divisible by three:
+                    ///
+                    #[cfg_attr(all(feature = "si", feature = "f32"), doc = " ```rust,compile_fail")]
+                    #[cfg_attr(not(all(feature = "si", feature = "f32")), doc = " ```rust,ignore")]
+                    /// # use uom::si::f32::*;
+                    /// # use uom::si::area::square_meter;
+                    /// // error[E0271]: type mismatch resolving ...
+                    /// let r = Area::new::<square_meter>(8.0).cbrt();
+                    /// ```
+                    #[inline(always)]
+                    pub fn cbrt(
+                        self
+                    ) -> Quantity<
+                        $quantities<$($crate::typenum::PartialQuot<D::$symbol, $crate::typenum::P3>),+>,
+                        U, V>
+                    where
+                        $(D::$symbol: $crate::lib::ops::PartialDiv<$crate::typenum::P3>,
+                        <D::$symbol as $crate::lib::ops::PartialDiv<$crate::typenum::P3>>::Output: $crate::typenum::Integer,)+
+                        D::Kind: $crate::marker::Div,
+                    {
+                        Quantity {
+                            dimension: $crate::lib::marker::PhantomData,
+                            units: $crate::lib::marker::PhantomData,
+                            value: self.value.cbrt(),
+                        }
+                    }
+
+                    /// Fused multiply-add. Computes `(self * a) + b` with only one rounding error.
+                    /// This produces a more accurate result with better performance than a separate
+                    /// multiplication operation followed by an add.
+                    ///
+                    /// ## Generic Parameters
+                    /// * `Da`: Dimension for parameter `a`.
+                    /// * `Ua`: Base units for parameter `a`.
+                    /// * `Ub`: Base units for parameter `b`.
+                    #[inline(always)]
+                    pub fn mul_add<Da, Ua, Ub>(
+                        self,
+                        a: Quantity<Da, Ua, V>,
+                        b: Quantity<$quantities<$($crate::typenum::Sum<D::$symbol, Da::$symbol>),+>, Ub, V>,
+                    ) -> Quantity<$quantities<$($crate::typenum::Sum<D::$symbol, Da::$symbol>),+>, U, V>
+                    where
+                        $(D::$symbol: $crate::lib::ops::Add<Da::$symbol>,
+                        <D::$symbol as $crate::lib::ops::Add<Da::$symbol>>::Output: $crate::typenum::Integer,)+
+                        D::Kind: $crate::marker::Mul,
+                        Da: Dimension + ?Sized,
+                        Da::Kind: $crate::marker::Mul,
+                        Ua: Units<V> + ?Sized,
+                        Ub: Units<V> + ?Sized,
+                    {
+                        #[allow(unused_imports)]
+                        use $crate::num_traits::MulAdd;
+
+                        // (self * a) + b
+                        Quantity {
+                            dimension: $crate::lib::marker::PhantomData,
+                            units: $crate::lib::marker::PhantomData,
+                            value: self.value.mul_add(a.value, b.value),
+                        }
+                    }
+
+                    /// Raises a quantity to an integer power.
+                    ///
+                    #[cfg_attr(all(feature = "si", feature = "f32"), doc = " ```rust")]
+                    #[cfg_attr(not(all(feature = "si", feature = "f32")), doc = " ```rust,ignore")]
+                    /// # use uom::si::f32::*;
+                    /// # use uom::si::length::meter;
+                    /// use uom::typenum::P2;
+                    ///
+                    /// let a: Area = Length::new::<meter>(3.0).powi(P2::new());
+                    /// ```
+                    ///
+                    /// ## Generic Parameters
+                    /// * `E`: `typenum::Integer` power.
+                    #[inline(always)]
+                    pub fn powi<E>(
+                        self, _e: E
+                    ) -> Quantity<$quantities<$($crate::typenum::Prod<D::$symbol, E>),+>, U, V>
+                    where
+                        $(D::$symbol: $crate::lib::ops::Mul<E>,
+                        <D::$symbol as $crate::lib::ops::Mul<E>>::Output: $crate::typenum::Integer,)+
+                        D::Kind: $crate::marker::Mul,
+                        E: $crate::typenum::Integer,
+                    {
+                        Quantity {
+                            dimension: $crate::lib::marker::PhantomData,
+                            units: $crate::lib::marker::PhantomData,
+                            value: self.value.powi(E::to_i32()),
+                        }
+                    }
+
+                    /// Takes the square root of a number. Returns `NAN` if `self` is a negative
+                    /// number.
+                    ///
+                    #[cfg_attr(all(feature = "si", feature = "f32"), doc = " ```rust")]
+                    #[cfg_attr(not(all(feature = "si", feature = "f32")), doc = " ```rust,ignore")]
+                    /// # use uom::si::f32::*;
+                    /// # use uom::si::area::square_meter;
+                    /// let l: Length = Area::new::<square_meter>(4.0).sqrt();
+                    /// ```
+                    ///
+                    /// The input type must have dimensions divisible by two:
+                    ///
+                    #[cfg_attr(all(feature = "si", feature = "f32"), doc = " ```rust,compile_fail")]
+                    #[cfg_attr(not(all(feature = "si", feature = "f32")), doc = " ```rust,ignore")]
+                    /// # use uom::si::f32::*;
+                    /// # use uom::si::length::meter;
+                    /// // error[E0271]: type mismatch resolving ...
+                    /// let r = Length::new::<meter>(4.0).sqrt();
+                    /// ```
+                    #[inline(always)]
+                    pub fn sqrt(
+                        self
+                    ) -> Quantity<
+                        $quantities<$($crate::typenum::PartialQuot<D::$symbol, $crate::typenum::P2>),+>,
+                        U, V>
+                    where
+                        $(D::$symbol: $crate::typenum::PartialDiv<$crate::typenum::P2>,
+                        <D::$symbol as $crate::typenum::PartialDiv<$crate::typenum::P2>>::Output: $crate::typenum::Integer,)+
+                        D::Kind: $crate::marker::Div,
+                    {
+                        Quantity {
+                            dimension: $crate::lib::marker::PhantomData,
+                            units: $crate::lib::marker::PhantomData,
+                            value: self.value.sqrt(),
+                        }
+                    }}
+                }
+            }
+        }
+
         impl<D, U, V> $crate::lib::clone::Clone for Quantity<D, U, V>
         where
             D: Dimension + ?Sized,
diff --git a/src/tests/system.rs b/src/tests/system.rs
index 1355f80..459c9c6 100644
--- a/src/tests/system.rs
+++ b/src/tests/system.rs
@@ -577,14 +577,6 @@ mod fixed {
     }
 }
 
-// Somehow Complex<f32|f64> don't implement these methods, even though they
-// should, bc f32 / f64 implement Float and FloatCore:
-//  - https://docs.rs/num/latest/num/struct.Complex.html#impl-5
-//  - https://docs.rs/num/latest/num/struct.Complex.html#impl-4
-//
-// See discussion here:
-// https://github.com/iliekturtles/uom/pull/287#pullrequestreview-922005933
-/*
 mod complex {
     storage_types! {
         types: Complex;
@@ -595,7 +587,7 @@ mod complex {
 
         #[test]
         fn fp_categories() {
-            let float_categories = vec![
+            let float_categories = [
                 VV::neg_infinity(),
                 VV::min_value(),
                 VV::zero(),
@@ -603,12 +595,13 @@ mod complex {
                 VV::infinity(),
                 VV::nan(),
             ];
-            for re in float_categories.clone() {
+
+            for re in float_categories {
                 for im in float_categories {
                     let complex = V::new(re, im);
 
                     // Infinities
-                    if re.is_infinite() || im.is_infinite() {
+                    if complex.is_infinite() {
                         assert!(!Length::new::<meter>(complex).is_finite());
                         assert!(Length::new::<meter>(complex).is_infinite());
                         assert!(!Length::new::<meter>(complex).is_normal());
@@ -616,7 +609,7 @@ mod complex {
                     }
 
                     // NaNs
-                    if re.is_nan() || im.is_nan() {
+                    if complex.is_nan() {
                         assert!(!Length::new::<meter>(complex).is_finite());
                         assert!(!Length::new::<meter>(complex).is_infinite());
                         assert!(!Length::new::<meter>(complex).is_normal());
@@ -624,8 +617,7 @@ mod complex {
                     }
 
                     // Finite and normal numbers
-                    if (!re.is_infinite() && !re.is_nan())
-                        && (!im.is_infinite() && !im.is_nan()) {
+                    if !complex.is_infinite() && !complex.is_nan() {
                         assert!(Length::new::<meter>(complex).is_finite());
                         assert!(!Length::new::<meter>(complex).is_infinite());
                         assert!(Length::new::<meter>(complex).is_normal());
@@ -638,26 +630,22 @@ mod complex {
         quickcheck! {
             #[allow(trivial_casts)]
             fn is_nan(v: A<V>) -> bool {
-                v.re.is_nan() || v.im.is_nan()
-                    == Length::new::<meter>(*v).is_nan()
+                v.is_nan() == Length::new::<meter>(*v).is_nan()
             }
 
             #[allow(trivial_casts)]
             fn is_infinite(v: A<V>) -> bool {
-                v.re.is_infinite() || v.im.is_infinite()
-                    == Length::new::<meter>(*v).is_infinite()
+                v.is_infinite() == Length::new::<meter>(*v).is_infinite()
             }
 
             #[allow(trivial_casts)]
             fn is_finite(v: A<V>) -> bool {
-                v.re.is_finite() && v.im.is_finite()
-                    == Length::new::<meter>(*v).is_finite()
+                v.is_finite() == Length::new::<meter>(*v).is_finite()
             }
 
             #[allow(trivial_casts)]
             fn is_normal(v: A<V>) -> bool {
-                v.re.is_normal() && v.im.is_normal()
-                    == Length::new::<meter>(*v).is_normal()
+                v.is_normal() == Length::new::<meter>(*v).is_normal()
             }
 
             #[cfg(feature = "std")]
@@ -672,16 +660,12 @@ mod complex {
                 Test::eq(&v.cbrt(), &l.value)
             }
 
-            #[cfg(feature = "std")]
-            #[allow(trivial_casts)]
-            fn hypot(l: A<V>, r: A<V>) -> bool {
-                Test::eq(&Length::new::<meter>(l.hypot(*r)),
-                    &Length::new::<meter>(*l).hypot(Length::new::<meter>(*r)))
-            }
-
             #[cfg(feature = "std")]
             #[allow(trivial_casts)]
             fn mul_add(s: A<V>, a: A<V>, b: A<V>) -> bool {
+                #[allow(unused_imports)]
+                use num_traits::MulAdd;
+
                 let r: Quantity<Q<P2, Z0, Z0>, U<V>, V> = Length::new::<meter>(*s).mul_add(
                     Length::new::<meter>(*a),
                     Quantity::<Q<P2, Z0, Z0>, U<V>, V> {
@@ -693,17 +677,6 @@ mod complex {
                 Test::eq(&s.mul_add(*a, *b), &r.value)
             }
 
-            #[allow(trivial_casts)]
-            fn recip(v: A<V>) -> bool {
-                let a: Quantity<Q<N1, Z0, Z0>, U<V>, V> = Quantity::<Q<P1, Z0, Z0>, U<V>, V> {
-                    dimension: PhantomData,
-                    units: PhantomData,
-                    value: *v,
-                }.recip();
-
-                Test::eq(&v.recip(), &a.value)
-            }
-
             #[cfg(feature = "std")]
             #[allow(trivial_casts)]
             fn powi(v: A<V>) -> bool {
@@ -724,7 +697,6 @@ mod complex {
         }
     }
 }
-*/
 
 mod non_complex {
     storage_types! {
diff --git a/src/unit.rs b/src/unit.rs
index c83f5c9..cf86fc7 100644
--- a/src/unit.rs
+++ b/src/unit.rs
@@ -255,15 +255,16 @@ macro_rules! unit {
                 type T = VV;
 
                 #[inline(always)]
-                #[allow(trivial_numeric_casts)]
+                #[allow(clippy::inconsistent_digit_grouping)]
                 fn coefficient() -> Self::T {
-                    unit!(@coefficient $($conversion),+) as Self::T
+                    unit!(@coefficient $($conversion),+)
                 }
 
                 #[inline(always)]
-                #[allow(unused_variables, trivial_numeric_casts)]
+                #[allow(unused_variables)]
+                #[allow(clippy::inconsistent_digit_grouping)]
                 fn constant(op: $crate::ConstantOp) -> Self::T {
-                    unit!(@constant op $($conversion),+) as Self::T
+                    unit!(@constant op $($conversion),+)
                 }
             }
 

@TobTobXX
Copy link
Contributor Author

TobTobXX commented May 1, 2022

I still didn't really get why it works in the float block now, but it does. At least it compiles, still get failures everywhere, but that's one step closer.

I'll take a look at it later...

@iliekturtles
Copy link
Owner

I still didn't really get why it works in the float block now, but it does. At least it compiles, still get failures everywhere, but that's one step closer.

The issue is that the original code implemented is_nan and all of the other floating point methods in an impl<..., V> Quantity<..., V> block where V: Float. Complex<T> doesn't implement Float so this original method didn't apply. Complex<T> does implement the methods outside the trait however. The solution is to remove the original method definitions in uom from the generic implementation and explicitly implement for each underlying storage type. e.g. impl<..., f32> Quantity<..., f32>, impl<..., f64> Quantity<..., f64>, impl<..., Complex32> Quantity<..., Complex32>, ... Manual monomorphization essentially.

I also reviewed the test failures. The issue is that Complex<T>'s div (and possibly mul?) implementation overflows with smaller numbers than regular f32/f64 would. See this example. If you run you can see that cf is 1.0000001e24 while c is inf. Replacing the regular div call with fdiv gives the same results.

Other underlying storage types have similar issues (#44) so I wont block inclusion of complex. . If you believe there are no other open issues, will you squash your commits into a single commit (or a few that each make specific changes). I'll do a final review and believe we can accept.

@TobTobXX
Copy link
Contributor Author

TobTobXX commented May 15, 2022

Sorry for the delay, finals prep claimed most of my time the last few weeks.

The issue is ...

Oh, I see.

The issue is that Complex's div implementation overflows with smaller numbers than regular f32/f64 would.

Other underlying storage types have similar issues (#44) so I wont block inclusion of complex.

Oh, yeah, would've never found that. Thank you! Well then, I guess we'll include them. Should I remove/comment out the tests to make the pipeline succeed?

Thank you a lot for your support on this PR!

@TobTobXX TobTobXX marked this pull request as ready for review May 15, 2022 11:36
@iliekturtles iliekturtles merged commit b7906fd into iliekturtles:master May 16, 2022
@iliekturtles
Copy link
Owner

Merged! Thank you so much for the PR and for your patience as I found time to work through the review. No need to comment out the tests as they aren't run by default. Currently I'm only requiring full test coverage for f32/f64 and will enable the tests as precision issues are fixed for other types.

I'll likely look to do a new release to crates.io in the next couple weeks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants