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

Implement muldiv assign for Ratio #364

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ approx = "0.5"
quickcheck = "1.0"
serde_json = "1.0"
static_assertions = "1.1"
paste = "1.0.9"

[features]
default = ["autoconvert", "f32", "f64", "si", "std"]
Expand Down
16 changes: 16 additions & 0 deletions src/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,22 @@ macro_rules! system {
}
}

impl<D, Ul, Ur, V> $crate::lib::ops::$MulDivAssignTrait<Quantity<DimensionOne, Ur, V>> for Quantity<D, Ul, V>
where
D: Dimension + ?Sized,
D::Kind: $crate::marker::$MulDivAssignTrait,
Ul: Units<V> + ?Sized,
Ur: Units<V> + ?Sized,
V: $crate::num::Num + $crate::Conversion<V>
+ $crate::lib::ops::$MulDivAssignTrait<V>,
{
#[inline(always)]
fn $muldivassign_fun(&mut self, rhs: Quantity<DimensionOne, Ur, V>) {
self.value $muldivassign_op rhs.value;
// change_base is needed for autoconvert! version
Copy link
Owner

Choose a reason for hiding this comment

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

autoconvert! and not_autoconvert! versions are needed. Scroll up a bit in the file and see $MulDivTrait for examples. The autoconvert! versions should use change_base::<Dr, Ul, Ur, V>(&rhs.value). The not_autoconvert! parameter should only have one U parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, I had hoped to test everything that needs to be implemented. But I guess that the not_autoconvert! version is only needed when the autoconvert feature is disabled, while the tests always run with the autoconvert feature enabled. So the tests simply do not test the not_autoconvert! versions at all. Is that right?

Ifs so, do you have any ideas about how to test the not_autoconvert! versions?

Copy link
Owner

Choose a reason for hiding this comment

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

The full test suite has test runs with and without autoconvert. Most of the tests do operations where both quantities have the same base units and run for both. Then there are some that are only run when autoconvert is enabled and ensure that operations with different base units work as expected.

Test cases:

- name: Test si
uses: actions-rs/cargo@v1
with:
command: test
args: --verbose --no-default-features --features "f32 si"
- name: Test all non-storage type features
uses: actions-rs/cargo@v1
with:
command: test
args: --verbose --no-default-features --features "autoconvert f32 si use_serde"

Tests:

#[cfg(feature = "autoconvert")]
quickcheck! {
#[allow(trivial_casts)]
fn add(l: A<V>, r: A<V>) -> TestResult {
let km: V = <kilometer as crate::Conversion<V>>::coefficient().value();
let f = (&*l + &*r) / &km;
let i = (&*l / &km) + (&*r / &km);
if !Test::approx_eq(&i, &f) {
return TestResult::discard();
}
TestResult::from_bool(
Test::approx_eq(&k::Length::new::<meter>(&*l + &*r),
&(k::Length::new::<meter>((*l).clone())
+ f::Length::new::<meter>((*r).clone()))))
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TLDR

Apoogies for being dense and verbose. It seems that I still don't understand what autoconvert does.

My understanding is that the autoconvert feature enables binary operations to act on different Units within the same Quantity. In other words, without autoconvert operations like something in metres + something in kilometres should not work.

The behaviour of the code appears to contradict my understanding.

Gory details

I've added an ac switch to the test! macro, so that

test!(ac ... etc)  // runs only when autoconvert feature enabled
test!(   ... etc)  // runs always

In the last commit I've pushed (entitiled: REVERT ME: run 'ac' tests even when autoconvert not enabled) I have commented out the feature gate, so that the ac tests run even if autoconvert is not enabled. At this point, I would expect the six tests labelled with ac in

https://github.com/jacg/uom/blob/eef466b82d0969b3063582108fcb9100d0563ba2/src/tests/impl_ops.rs#L135-L144

... which for some reason doesn't get expanded in the GH interface, so I'll paste the relevant lines by hand:

    test!(ac add                   m(1000.)   +     km(2.),     m(3000.)); // 1
    test!(ac sub                   m(8000.)   -     km(2.),     m(6000.)); // 1
    test!(   add                   m(   1.)   +      m(2.),        m(3.)); //  2
    test!(   sub                   m(   8.)   -      m(2.),        m(6.)); //  2
    test!(ac add_assign          [ m(1000.) ] +=    km(2.),       km(3.)); //   3
    test!(ac sub_assign          [ m(8000.) ] -=    km(2.),       km(6.)); //   3
    test!(   add_assign          [ m(   1.) ] +=     m(2.),        m(3.)); //    4
    test!(   sub_assign          [ m(   8.) ] -=     m(2.),        m(6.)); //    4
    test!(ac mul                   m(4000.)   *     km(2.),      km2(8.)); //     5
    test!(ac div                   m(6000.)   /     km(2.),    ratio(3.)); //     5

... so, I expect those six test marked with ac (just inside test!() either to FAIL or to FAIL TO COMPILE, if the autoconvert feature is not enabled. I expect this, because they all use different Units (belonging to the same Quantity) on their LHS/RHS. Specifically ac add uses m on the LHS and km on the RHS: these are both Lengths, but have different Units. According to my understanding this should only work if autoconvert is enabled.

And yet, they not only compile but they also run and pass, when autoconvert is disabled:

cargo test impl --no-default-features --features "f32 si"                                                                                                                                                                                                                                                                                                                                 ─╯
   Compiling uom v0.33.0 (/home/jacek/rust/uom)
    Finished test [unoptimized + debuginfo] target(s) in 3.92s
     Running unittests src/lib.rs (target/debug/deps/uom-2f2c30298b0ddf3b)

running 28 tests
test tests::impl_ops::vv::add ... ok
test tests::impl_ops::vv::add_assign ... ok
test tests::impl_ops::vv::div_assign_bare ... ok
test tests::impl_ops::vv::div ... ok
test tests::impl_ops::vv::add_assign_autoconvert ... ok
test tests::impl_ops::vv::div_assign_ratio_p ... ok
test tests::impl_ops::vv::add_autoconvert ... ok
test tests::impl_ops::vv::div_assign_ratio ... ok
test tests::impl_ops::vv::div_autoconvert ... ok
test tests::impl_ops::vv::div_bare_left ... ok
test tests::impl_ops::vv::div_bare_right ... ok
test tests::impl_ops::vv::div_ratio_left ... ok
test tests::impl_ops::vv::div_ratio_right ... ok
test tests::impl_ops::vv::div_ratio_right_p ... ok
test tests::impl_ops::vv::mul ... ok
test tests::impl_ops::vv::mul_assign_bare ... ok
test tests::impl_ops::vv::mul_assign_ratio ... ok
test tests::impl_ops::vv::mul_assign_ratio_p ... ok
test tests::impl_ops::vv::mul_autoconvert ... ok
test tests::impl_ops::vv::mul_bare_left ... ok
test tests::impl_ops::vv::mul_bare_right ... ok
test tests::impl_ops::vv::mul_ratio_left ... ok
test tests::impl_ops::vv::mul_ratio_right ... ok
test tests::impl_ops::vv::mul_ratio_right_p ... ok
test tests::impl_ops::vv::sub ... ok
test tests::impl_ops::vv::sub_assign ... ok
test tests::impl_ops::vv::sub_assign_autoconvert ... ok
test tests::impl_ops::vv::sub_autoconvert ... ok

test result: ok. 28 passed; 0 failed; 0 ignored; 0 measured; 174 filtered out; finished in 0.00s

Points to note:

  • --no-default-features
  • --features "f32 si" does NOT include autoconvert
  • The 6 tests whose names end in _autoconvert run and pass.

Where is my error / misunderstanding / brain fart?

Copy link
Owner

Choose a reason for hiding this comment

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

My understanding is that the autoconvert feature enables binary operations to act on different Units within the same Quantity. In other words, without autoconvert operations like something in meters + something in kilometers should not work.

autoconvert affects how quantities with different base units work. A uom Quantity doesn't store the original unit (e.g. meter, kilometer, ...) that was used. Instead the value is normalized into a base unit. Meter by default in the case of length. When talking about operations between quantities we can be more explicit by saying a length (the D generic parameter of quantity) stored as meters (the U generic parameter of quantity) inf32 (the V generic parameter of quantity).

let m = Length::new::<meter>(50.0); // Internally stored as 50.0 meters.
let km = Length::new::<kilometer>(50.0); // Internally stored as 50_000.0 meters.

You're never adding units together, always quantities. autoconvert affects how operations are defined such that when autoconvert is not enabled you can only operate on quantities with the same base units. e.g. length stored as meters can only operate with length stored as meters, but not length stored as kilometers. When autoconvert is enabled this limitation is lifted.

Compare https://github.com/iliekturtles/uom/blob/master/examples/si.rs and https://github.com/iliekturtles/uom/blob/master/examples/base.rs. si.rs will work regardless of the autoconvert setting because the same base units are used for all quantities. base.rs requires autoconvert to be enabled because a new set of base units (using centimeter, gram, second) is defined.

Copy link
Contributor Author

@jacg jacg Sep 4, 2022

Choose a reason for hiding this comment

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

Thanks for your patience and your clear explanations. Your description is consistent with my initial understanding of autoconvert. I'm not sure why, in the process of working on this, I convinced myself that it was something else. Apologies.

I have corrected my confusion, rebased, and force-pushed two commits:

  1. Add concise tests summarizing which binops work
  2. Implement MulDivAssign with Ratio on RHS

The first of these commits summarizes which arithmetic binary operations are implemented, both in the presence and absence of autoconvert. I have also added some tests for the comparison operators. For now, I'm only testing value operands; the idea is to add ref-ref, value-ref, and ref-value versions of these tests, for use in #307.

The second of these commits is the implementation that I pushed earlier, without any changes. Before this commit, these four tests :

test!(   mul_assign_ratio    [ m(4.) ] *=     one(2.),   m(8.)); //           c.f. AAA above
test!(   div_assign_ratio    [ m(6.) ] /=     one(2.),   m(3.)); //           c.f. BBB above
test!(ac mul_assign_ratio    [ m(4.) ] *= cgs_one(2.),   m(8.)); //           c.f. CCC above
test!(ac div_assign_ratio    [ m(6.) ] /= cgs_one(2.),   m(3.)); //           c.f. DDD above

fail to compile (which is why they are commented out in the first commit). After the second commit, all four of these tests pass (which is why they are uncommented in the commit).

The first two of these tests use the si system that uom provides out of the box on both the LHS and the RHS. As such they should work both with and without autoconvert. The second two tests differ from the first two in that they use a set of cgs units on the RHS; thus they should only work when autoconvert is enabled. All this is borne out in practice.

Returning to what you said right at the top of this conversation:

autoconvert! and not_autoconvert! versions are needed.

I was hoping that these tests would demonstrate that this is the case. Instead, they seem to be indicating that the single implementation I provided, covers both cases.

So ...

  • Have I misunderstood something again?
  • If not, why are the two versions needed? Perhaps because, even though the single implementation is a correct implementation covering both cases, it is unnecessarily inefficient in (at least) one of the cases?

I'll try to implement both versions when I find some more time, but I just wanted to check in and make sure that I'm getting back on the right path.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Perhaps I'm misunderstanding, but it seems to me that change_base applies a conversion factor which is appropriate for conversion between corresponding quantities in the two different sets of units, but in this case the RHS is Ratio while the LHS is anything but Ratio. In my specific test, this results in applying the conversion factor of 100 which converts between cm and m, but this is inappropriate, because the RHS is Ratio.

Also, is it possible to have a 'base unit' for Ratio which differs from 1? I see how ISQ can create sets of units with arbitrary base units for the 7 fundamental quantities, but I don't see how Ratio's base unit could be anything other than 1.

If the above points are approximately correct, then it would seem that separate autoconvert! and not_autoconvert! implementations are not necessary ... just like in the Quantity */= f32 case.

}
}

#[doc(hidden)]
mod $Mod {
storage_types! {
Expand Down
197 changes: 197 additions & 0 deletions src/tests/impl_ops.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
//! Tests summarizing which binary operations are implemented.
//!
//! This includes:
//!
//! + the operations implemented using the `impl_ops!` macro.
//! + the comparison operators: `==`, `<`, `>`, `<=` and `>=`.
//!
//! The purpose is not to test the implementations exhaustively, but rather to
//! give a clear overview of which ones are present or absent, depending on
//! factors like:
//!
//! + `autoconvert` being enabled or disabled
//! + whether the operands are values or refs

// Helper functions for constructing a variety of Quantities without excessive
// syntactic noise.
#[allow(unused_macros)]
mod concise {

// Bring the units used by the constructors into a limited scope
mod units {
pub use crate::si::{
area::square_meter,
frequency::hertz,
length::meter,
ratio::ratio,
time::second,
velocity::meter_per_second,
};
}

// Define a set of quantities with different base units, for testing of `autoconvert`
pub mod cgs {
ISQ!(crate::si, f32, (centimeter, gram, second, ampere, kelvin, mole, candela));
}

/// Generate a module containing concise constructors for Quantities based
/// on a given storage type, in order to allow writing pithy tests
macro_rules! constructors {
($storage_type:ident $($function:item)*) => {
#[allow(unused)]
pub mod $storage_type {
use crate::si::$storage_type as si;
use crate::tests::impl_ops::concise::cgs;
use crate::tests::impl_ops::concise::units::*;
type T = $storage_type;
$($function)*
}
};
}

/// Generate a function called NAME which returns QUANTITY by interpreting
/// its argument as UNIT
///
/// wrap!(NAME(QUANTITY) UNIT);
macro_rules! wrap {
($name:ident ($quantity:ty) $unit:ident ) => {
pub fn $name(x: T) -> $quantity {
<$quantity>::new::<$unit>(x)
}
};
}

// Use the above macros to generate terse Quantity constructors.
#[cfg(feature = "f32")]
constructors! { f32
wrap!( m( si::Length) meter);
wrap!( cgs_m(cgs::Length) meter);
wrap!( s( si::Time) second);
wrap!( one( si::Ratio) ratio);
wrap!(cgs_one(cgs::Ratio) ratio);
wrap!( m2( si::Area) square_meter);
wrap!( mps( si::Velocity) meter_per_second);
wrap!( hz(si::Frequency) hertz);
}

}

// The tests will come in 4 varieties: `vv`, `vr`, `rv` & `rr`. with `r` and `v`
// standing for 'value' and 'reference', describing the left and right operands
// of the operator being tested.

/// Generate a test that checks the result of a binary operation
///
/// There are two different kinds of tests:
///
/// 1. verify the result of a binary operation expression
/// 2. verify the side effect of an assignment operator on the left operand
///
/// Syntactically these look like
///
/// 1. test!(TEST_NAME LHS OPERATOR RHS, EXPECTED);
/// 2. test!(TEST_NAME \[ LHS \] OPERATOR RHS, EXPECTED);
///
/// In other words, the side-effect test is distinguished from the expression
/// test by wrapping the left operand (the one which will be mutated by the
/// operator) in square brackets.
///
/// Additionally, some tests should only be run when the `autoconvert` features
/// is enabled. These are identified by using the `ac` token at the start of the
/// input to `test!(...)`:
///
/// 1. Run test always: `test!( ...)`
/// 2. Run test only when `autoconvert` enabled: `test!(ac ...)`
///
/// Furthermore, the `ac` variant of `test!` appends `_autoconvert` to
/// TEST_NAME, in order to distinguish automatically between tests which require
/// autoconvert and those that don't.
#[allow(unused_macros)]
macro_rules! test {
// The first two arms (with `ac` as the first input token) forward the
// request to the last two arms, after injecting two changes:
// 1. add `#[cfg(feature = "autoconvert")]`
// 2. append `_autoconvert` to the test name
(ac $test_name:ident [$lhs:expr] $op:tt $rhs:expr, $expected:expr) => {
paste::paste! {
#[cfg(feature = "autoconvert")]
test!([<$test_name _ autoconvert>] [$lhs] $op $rhs, $expected);
}
};
(ac $test_name:ident $lhs:expr, $rhs:expr) => {
paste::paste! {
#[cfg(feature = "autoconvert")]
test!([<$test_name _ autoconvert>] $lhs, $rhs);
}
};
// ----------------------------------------------------------------
// test assignment operations: let mut l; l += r
($test_name:ident [$lhs:expr] $op:tt $rhs:expr, $expected:expr) => {
#[test]
fn $test_name() {
let mut x = $lhs;
x $op $rhs;
assert_eq!(x, $expected);
}
};
// test binary operator expressions: l + r
($test_name:ident $lhs:expr, $rhs:expr) => {
#[test]
fn $test_name() {
assert_eq!($lhs, $rhs);
}
};
}

// The number in the comment after each test indicates which implementation
// provides the functionality being tested, according to the table shown here:
// https://github.com/iliekturtles/uom/pull/307#issuecomment-1186208970
#[rustfmt::skip]
#[cfg(feature = "f32")]
mod vv {
use super::concise::f32::*;
//#[cfg(feature = "autoconvert")]

// NOTE: tests labelled with `ac` just inside `test!(` require the
// `autoconvert` feature to be enabled.

test!(ac add m(1.) + cgs_m(2.), m(3.)); // 1
test!(ac sub m(8.) - cgs_m(2.), m(6.)); // 1
test!( add m(1.) + m(2.), m(3.)); // 2
test!( sub m(8.) - m(2.), m(6.)); // 2
test!(ac add_assign [ m(1.) ] += cgs_m(2.), m(3.)); // 3
test!(ac sub_assign [ m(8.) ] -= cgs_m(2.), m(6.)); // 3
test!( add_assign [ m(1.) ] += m(2.), m(3.)); // 4
test!( sub_assign [ m(8.) ] -= m(2.), m(6.)); // 4
test!(ac mul m(4.) * cgs_m(2.), m2(8.)); // 5
test!(ac div m(6.) / cgs_m(2.), one(3.)); // 5
test!( mul m(4.) * m(2.), m2(8.)); // 6
test!( div m(6.) / s(2.), mps(3.)); // 6
test!( mul_ratio_left one(4.) * m(2.), m(8.)); // 6
test!( div_ratio_left one(6.) / s(2.), hz(3.)); // 6
test!( mul_ratio_right m(4.) * one(2.), m(8.)); // 6 c.f. AAA below
test!( div_ratio_right m(6.) / one(2.), m(3.)); // 6 c.f. BBB below
test!(ac mul_ratio_right m(4.) * cgs_one(2.), m(8.)); // 6 c.f. CCC below
test!(ac div_ratio_right m(6.) / cgs_one(2.), m(3.)); // 6 c.f. DDD below
test!( mul_assign_ratio [ m(4.) ] *= one(2.), m(8.)); // c.f. AAA above
test!( div_assign_ratio [ m(6.) ] /= one(2.), m(3.)); // c.f. BBB above
test!(ac mul_assign_ratio [ m(4.) ] *= cgs_one(2.), m(8.)); // c.f. CCC above
test!(ac div_assign_ratio [ m(6.) ] /= cgs_one(2.), m(3.)); // c.f. DDD above
test!( mul_bare_right m(4.) * 2. , m(8.)); // 7
test!( div_bare_right m(6.) / 2. , m(3.)); // 7
test!( mul_assign_bare [ m(2.) ] *= 3. , m(6.)); // 8
test!( div_assign_bare [ m(6.) ] /= 3. , m(2.)); // 8
test!( mul_bare_left 4. * m(2.), m(8.)); // 9
test!( div_bare_left 6. / s(2.), hz(3.)); // 9

test!( eq m(1.) == m(2.), false);
test!(ac eq m(1.) == cgs_m(2.), false);
test!( lt m(1.) < m(2.), true );
test!(ac lt m(1.) < cgs_m(2.), true );
test!( gt m(1.) > m(2.), false);
test!(ac gt m(1.) > cgs_m(2.), false);
test!( le m(1.) <= m(2.), true );
test!(ac le m(1.) <= cgs_m(2.), true );
test!( ge m(1.) >= m(2.), false);
test!(ac ge m(1.) >= cgs_m(2.), false);
}
1 change: 1 addition & 0 deletions src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ mod a_struct {
}

mod asserts;
mod impl_ops;
mod quantities;
mod quantity;
mod system;