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

Specifying ad-hoc Quantities and functions generic over Quantities #295

Closed
jacg opened this issue Mar 29, 2022 · 14 comments
Closed

Specifying ad-hoc Quantities and functions generic over Quantities #295

jacg opened this issue Mar 29, 2022 · 14 comments

Comments

@jacg
Copy link
Contributor

jacg commented Mar 29, 2022

Consider this toy function

fn invert(x: Length) -> PerLength {
    1.0 / x
}
  1. Is there a convenient way of defining ad-hoc Quantitys such as PerLength? Can I do better than this
    use uom::si::{ISQ, SI, Quantity};
    use uom::typenum::{Z0, N1};
    
    type PerLength = Quantity<ISQ<N1, Z0, Z0, Z0, Z0, Z0, Z0>, SI<f32>, f32>;
    ?
  2. How can this function be made generic over the input (and output) Quantity?
    Naively, I would like to write
    fn invert<T>(x: T) -> Per<T> {
        1.0 / x
    }
    • Per<T> can be expressed with typenum gymnastics, such as used in the return type of sqrt.
    • fn invert<T>(x: T) will probably look more like fn invert<D, U, V>(x: Quantity<D, U, V>), but when I try to add the necessary bounds on D, U and V I always end up getting stuck.
@jacg
Copy link
Contributor Author

jacg commented Mar 30, 2022

Another related problem arises in the context of #289:

Do you see use for affine/vector quantities beyond length? Are there cases in your simulation where Point and Vector are not sufficient?

I have Vector and Point types, each containing 3 Lengths. Parts of the code are expressed in terms of normalized units, relative to some unit cell. Those parts require Vector and Point types containing 3 Ratios each.

I kinda anticipated this, and wanted to make Vector and Point generic over the contained Quantity, but got lost in the implementation, and went with the hard-wired Length to start with. But now the motivation for the generic versions is much stronger.

Ideally I'd like to have Vector<T> and Point<T> such that

  • Vector<Length> + Vector<Length> -> Vector<Length>
  • Vector<Length> / Vector<Length> -> Vector<Ratio>
  • Point<Length> + Vector<Length> -> Point<Length>
  • Point<Ratio> + Vector<Ratio> -> Point<Ratio>

Is it possible to implement these in a generic way?

@iliekturtles
Copy link
Owner

iliekturtles commented Mar 30, 2022

  1. This is the best way right now. With a bit of type magic you can make this more reusable. I didn't go digging to see if the PerQuantity type parameter can accept a quantity (e.g. Length) because of time constraints.
type InvertDimension<D> = ISQ<
    <uom::typenum::Z0 as uom::lib::ops::Sub<<D as Dimension>::L>>::Output,
    <uom::typenum::Z0 as uom::lib::ops::Sub<<D as Dimension>::M>>::Output,
    <uom::typenum::Z0 as uom::lib::ops::Sub<<D as Dimension>::T>>::Output,
    <uom::typenum::Z0 as uom::lib::ops::Sub<<D as Dimension>::I>>::Output,
    <uom::typenum::Z0 as uom::lib::ops::Sub<<D as Dimension>::Th>>::Output,
    <uom::typenum::Z0 as uom::lib::ops::Sub<<D as Dimension>::N>>::Output,
    <uom::typenum::Z0 as uom::lib::ops::Sub<<D as Dimension>::J>>::Output>;

type PerQuantity<D> = Quantity<InvertDimension<D>, SI<f32>, f32>;

let l1 = Length::new::<meter>(15.0);
let i1: PerQuantity<uom::si::length::Dimension> = 1.0_f32 / l1;

2.a You can make this a generic function for any two types, Q and V, where V can be divided by Q like follows:

fn invert2<Q, V>(q: Q) -> <V as std::ops::Div<Q>>::Output
where
    V: uom::num::One + std::ops::Div<Q>
{
    V::one() / q
}

2.b This can be expanded to be Quantity specific as follows:

fn invert3<D, U, V>(q: Quantity<D, U, V>) -> <V as std::ops::Div<Quantity<D, U, V>>>::Output
where
    D: Dimension + ?Sized,
    U: Units<V> + ?Sized,
    V: uom::num::Num + uom::Conversion<V> + std::ops::Div<Quantity<D, U, V>>
{
    V::one() / q
}
  1. uom already implements Quantity1 + Quantity1 == Quantity1 and Quantity1 / Quantity1 == Ratio. All of your desired Vector<T> and Point<T> operators can be similar to invert2 where you just use a T parameter with the appropriate bounds. Or you can make methods similar to invert3 where you expand T into D, U, and V parameters. I think the choice should just depend on how much you want to tie to uom. If you have started implementation and have specific errors I can take a look for you.

@iliekturtles
Copy link
Owner

To add to this when I first started working on uom I attempted to implement the relevant operator traits on Dimension. I believe I didn't follow through because it didn't save the need to write out each of Dimension's associated types when writing trait bounds.

Adding them back in could save end-users from having to write their own InversionDimension<D> or other similar type aliases to calculate a new dimension.

@jacg
Copy link
Contributor Author

jacg commented Mar 30, 2022

Thank you so much! This is very useful.

type PerQuantity = Quantity<InvertDimension<uom::si::length::Dimension>, SI<f32>, f32>;

I think you meant:

type PerQuantity<D> = Quantity<InvertDimension<D>, SI<f32>, f32>;

If you have started implementation and have specific errors I can take a look for you.

What you've given me already should get me over quite a few hurdles. Might take you up on it further down the line.

@iliekturtles
Copy link
Owner

I think you meant:

Yep, too many changes in the test project and I missed copying over to my reply.

@jacg
Copy link
Contributor Author

jacg commented Mar 30, 2022

too many changes in the test project and I missed copying over to my reply.

Probably worth editing it, for the sake of future readers.


As it stands, PerQuantity requires to be given a Dimension as type argument. Would it be possible to make it accept a Quantity?That is to say

PerQuantity<uom::si::f32::Length>

instead of

PerQuantity<uom::si::length::Dimension>

I'm also struggling with your invert2

  •   let l: Length = todo!();
      let i = invert2(l)
    complains that type annotations are needed for i.
  •   type PerLength = Quantity<ISQ<N1, Z0, Z0, Z0, Z0, Z0, Z0>, SI<f32>, f32>;
      let i: PerLength = invert2(l);
    complains thus
    error[E0283]: type annotations needed
      --> geometry/src/bin/playground.rs:56:30
       |
    56 |         let i:  PerLength  = invert2(l); // ERROR: cannot infer type parameter V
       |                              ^^^^^^^ cannot infer type for type parameter `V` declared on the function `invert2`
       |
       = note: cannot satisfy `_: One`
    note: required by a bound in `invert2`
      --> geometry/src/bin/playground.rs:23:8
       |
    21 | fn invert2<Q, V>(q: Q) -> <V as std::ops::Div<Q>>::Output
       |    ------- required by a bound in this
    22 | where
    23 |     V: uom::num::One + std::ops::Div<Q>
       |        ^^^^^^^^^^^^^ required by this bound in `invert2`
    help: consider specifying the type arguments in the function call
       |
    56 |         let i:  PerLength  = invert2::<Q, V>(l); // ERROR: cannot infer type parameter V
       |                                     ++++++++
    

I would prefer to use invert2 rather than invert3 as not being tied to uom would make it more general, but if that turns out to be too much hassle, then invert3 seems to do the job just fine.

@iliekturtles
Copy link
Owner

While looking at the latest question I realized a recip method is already defined when V: Float. If your code meets that constraint you can just use recip instead of defining a new invert method.

InvertDimension<D> can also be made slightly less verbose and probably more easily understood by using Neg instead of Sub:

type InvertDimension<D> = ISQ<
    <<D as Dimension>::L as uom::lib::ops::Neg>::Output,
    <<D as Dimension>::M as uom::lib::ops::Neg>::Output,
    <<D as Dimension>::T as uom::lib::ops::Neg>::Output,
    <<D as Dimension>::I as uom::lib::ops::Neg>::Output,
    <<D as Dimension>::Th as uom::lib::ops::Neg>::Output,
    <<D as Dimension>::N as uom::lib::ops::Neg>::Output,
    <<D as Dimension>::J as uom::lib::ops::Neg>::Output>;

Looks like I never actually used invert2 after getting it compiling. The compiler error is right that V does need to be defined. You can do that explicitly with invert2 or just write something like invert4 that is for a specific type:

let i1/*: Quantity<InvertDimension<uom::si::length::Dimension>, SI<f32>, f32>*/ = invert2::<_, f32>(l1);
fn invert4<Q>(q: Q) -> <f32 as std::ops::Div<Q>>::Output
where
    f32: std::ops::Div<Q>,
{
    1.0_f32 / q
}

I don't think you can make an InventoryQuantity<Q>. Or at least I couldn't figure it out in the limited time I had to try. The dimension, units, and value parameters of Quantity aren't associated types so you can't reference them in the type alias. The closest I could get is the following:

type InvertQuantity<D, U, V> = Quantity<InvertDimension<D>, U, V>;

If U and V are fixed you can remove the type parameters simplifying the type alias slightly so that only the dimension is required.

@jacg
Copy link
Contributor Author

jacg commented Apr 1, 2022

While looking at the latest question I realized a recip method is already defined when V: Float. If your code meets that constraint you can just use recip instead of defining a new invert method.

Unfortunately inverse was just a toy example representing the function that I really want (a Gaußian PDF) which also requires the output type to be the inverted version of one of the inputs.

Meanwhile, I'm stumbling over another problem: I need a system with different base units (mm and ps instead of m and s), which I've defined thus:

pub mod mmps {
  pub mod f32 {
    use uom::{ISQ, system};
    ISQ!(uom::si, f32, (millimeter, kilogram, picosecond, ampere, kelvin, mole, candela));
  }
}

That seems to be working OK

[aside: it was a bit of a struggle to interpret the docs:

  • Where does the name Q on https://docs.rs/uom/latest/uom/macro.ISQ.html come from?
  • are the macro_use and extern crate still needed in current editions?
  • I keep getting lost among all the ISQ macros and non-macros which appear in multiple places.

]

But I completely fail to make a reciprocal-length type that uses inverse-mm instead of inverse-m.

@iliekturtles
Copy link
Owner

This uncovers a limitation in uom where the trait alias for a custom set of base units is not public! I'll submit a PR shortly to resolve this. The fix is to make the trait alias pub:

uom/src/system.rs

Line 1631 in 66d3e85

type Units = dyn __system::Units<$V, $($name = __system::$name::$U,)+>;

You can also work around this by manually defining the trait based on what the macro does:

type Units = dyn uom::si::Units<f32, L = millimeter, M = kilogram, ...>;

With the trait alias made public or defined you can use the same InvertDimension alias from earlier and replace SI<f32> type parameter given to Quantity with the newly defined Units alias. I tested with the base example and the patch below shows the changes to take reciprocal cgs length.

diff --git a/examples/base.rs b/examples/base.rs
index b776f32..4677273 100644
--- a/examples/base.rs
+++ b/examples/base.rs
@@ -4,9 +4,19 @@
 #[macro_use]
 extern crate uom;
 
+use uom::si::Dimension;
 use uom::si::length::{centimeter, meter};
 use uom::si::time::second;
 
+type InvertDimension<D> = uom::si::ISQ<
+    <<D as Dimension>::L as uom::lib::ops::Neg>::Output,
+    <<D as Dimension>::M as uom::lib::ops::Neg>::Output,
+    <<D as Dimension>::T as uom::lib::ops::Neg>::Output,
+    <<D as Dimension>::I as uom::lib::ops::Neg>::Output,
+    <<D as Dimension>::Th as uom::lib::ops::Neg>::Output,
+    <<D as Dimension>::N as uom::lib::ops::Neg>::Output,
+    <<D as Dimension>::J as uom::lib::ops::Neg>::Output>;
+
 mod cgs {
     ISQ!(uom::si, f32, (centimeter, gram, second, ampere, kelvin, mole, candela));
 }
@@ -15,6 +25,7 @@ fn main() {
     let l1 = uom::si::f32::Length::new::<meter>(1.0);
     let l2 = cgs::Length::new::<centimeter>(1.0);
     let t1 = uom::si::f32::Time::new::<second>(15.0);
+    let r1: uom::si::Quantity<InvertDimension<uom::si::length::Dimension>, cgs::Units, f32> = l2.recip();
 
     println!("{}: {:?}", uom::si::length::description(), l1);
     println!("{}: {:?}", uom::si::length::description(), l2);

In the documentation example it is briefly mentioned where Q comes from:

An example invocation is given below for a meter-kilogram-second system setup in the module mks with a system of quantities name Q.

You're right that this is still confusing because the full context isn't shown. If you look at the example on the system! macro definition you can see that Q is defined in the system! macro invocation. ISQ is the equivalent for uom::si. The relevant part of the example is hidden in the macro documentation (line 1576):

uom/src/system.rs

Lines 1574 to 1591 in 66d3e85

/// # system! {
/// # /// System of quantities, Q.
/// # quantities: Q {
/// # length: meter, L;
/// # mass: kilogram, M;
/// # time: second, T;
/// # }
/// # /// System of units, U.
/// # units: U {
/// # mod length::Length,
/// # mod mass::Mass,
/// # mod time::Time,
/// # }
/// # }
/// mod f32 {
/// Q!(crate::mks, f32/*, (centimeter, gram, second)*/);
/// }
/// # }


macro_use and extern crate aren't be necessary, but you end us with a pretty big use uom::{...} to import all of the necessary macros and nested helper macros. This gets especially painful with storage_types!. See #232 for discussion.

iliekturtles added a commit that referenced this issue Apr 1, 2022
Part of #295 where the need to access the `Units` trait for a
user-defined system of units was uncovered.
@jacg
Copy link
Contributor Author

jacg commented Apr 9, 2022

Is there a convenient way of defining ad-hoc Quantitys such as PerLength? Can I do better than this

use uom::si::{ISQ, SI, Quantity};
use uom::typenum::{Z0, N1};

type PerLength = Quantity<ISQ<N1, Z0, Z0, Z0, Z0, Z0, Z0>, SI<f32>, f32>;

?

Just for reference, dimensioned has features which make this considerably more pleasant than seems to be currently possible in uom. From the dimensioned docs:

This macro creates a type, so it is useful when you need to directly express the type of a derived unit that is not defined in its unit system.

If you need a variable of some derived unit, then the easiest way is to manipulate constants, like so:

use dim::si::M;

let inverse_volume = 3.0 / M/M/M;

This macro is a bit fragile. It only supports the operators * and / and no parentheses. It requires the base type of your unit system and the module it was defined in to be in scope.

Use it like so:

use dim::si::{self, SI};
derived!(si, SI: InverseMeter3 = Unitless / Meter3);

You may use any of the base or derived units that come with a unit system (but none created by this macro) on the right-hand side of the expression.

@iliekturtles
Copy link
Owner

Interseting! A similar macro should be possible in uom. I'm imagining something that would take the module names and path to the Dimension alias to calculate the resulting dimension. I think I also mentioned earlier that Sub/Add could be implemented for Dimension to simplify all this.

// Dividing quanties -> subtract dimensions
type Result = ISQ<
    <$M1::Dimesion::L as uom::lib::ops::Sub<$M2::Dimension::L>>::Output,
    <$M1::Dimension::M as uom::lib::ops::Sub<$M2::Dimension::M>>::Output,
    <$M1::Dimension::T as uom::lib::ops::Sub<$M2::Dimension::T>>::Output,
    <$M1::Dimension::I as uom::lib::ops::Sub<$M2::Dimension::I>>::Output,
    <$M1::Dimension::Th as uom::lib::ops::Sub<$M2::Dimension::Th>>::Output,
    <$M1::Dimension::N as uom::lib::ops::Sub<$M2::Dimension::N>>::Output,
    <$M1::Dimension::J as uom::lib::ops::Sub<$M2::Dimension::J>>::Output>;

iliekturtles added a commit that referenced this issue Jun 25, 2022
Part of #295 where the need to access the `Units` trait for a
user-defined system of units was uncovered.
iliekturtles added a commit that referenced this issue Jun 28, 2022
Part of #295 where the need to access the `Units` trait for a
user-defined system of units was uncovered.
iliekturtles added a commit that referenced this issue Jun 28, 2022
Part of #295 where the need to access the `Units` trait for a
user-defined system of units was uncovered.
@jacg
Copy link
Contributor Author

jacg commented Aug 19, 2022

There is still #295 (comment):

To add to this when I first started working on uom I attempted to implement the relevant operator traits on Dimension. I believe I didn't follow through because it didn't save the need to write out each of Dimension's associated types when writing trait bounds.

Adding them back in could save end-users from having to write their own InversionDimension<D> or other similar type aliases to calculate a new dimension.

which, AFAICT, seems to be the only remaining unresolved point worth keeping in mind.

Should I extract this into its own issue and close this one?

jacg added a commit to jacg/petalo that referenced this issue Aug 19, 2022
The problem around which we had to hack, has now been fixed in `uom`, so
remove the hack.

iliekturtles/uom#295 (comment)
@iliekturtles
Copy link
Owner

Sorry for the delays, I was off on vacation for a while! If you don't mind, please create a new issue about investigating implementing operations for Dimension and close this one.

@jacg
Copy link
Contributor Author

jacg commented Aug 23, 2022

Done: what remains has been exctrated into #362.

@jacg jacg closed this as completed Aug 23, 2022
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

No branches or pull requests

2 participants