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

Allow defining new units for existing quantities #173

Closed
nakedible opened this issue Jan 1, 2020 · 9 comments
Closed

Allow defining new units for existing quantities #173

nakedible opened this issue Jan 1, 2020 · 9 comments

Comments

@nakedible
Copy link

I've come up on several cases where a unit I was looking for was missing for a quantity. Most recently, I wanted to use "liters of ideal gas" as an amount of substance unit.

There doesn't seem to be any way to add any units to a quantity definition later on as the units are all defined through the quantity! macro. Not sure if is technically feasible as I don't really understand the implementation but something like this would be wonderful:

quantity_unit!(AmountOfSubstance, @ideal_gas_stp_liter: 0.044031629; "STPl", "STP liter", "STP liters")
@iliekturtles
Copy link
Owner

Good idea. Extracting the unit-specific setup should be possible. Most (all?) of the code is in the following block:

uom/src/quantity.rs

Lines 153 to 278 in 3a236e5

$(quantity!(@unit $(#[$unit_attr])* @$unit);
impl super::Unit for $unit {
#[inline(always)]
fn abbreviation() -> &'static str {
$abbreviation
}
#[inline(always)]
fn singular() -> &'static str {
$singular
}
#[inline(always)]
fn plural() -> &'static str {
$plural
}
}
impl Unit for $unit {})+
storage_types! {
types: Float;
$(impl $crate::Conversion<V> for super::$unit {
type T = V;
#[inline(always)]
fn coefficient() -> Self::T {
quantity!(@coefficient $($conversion),+)
}
#[inline(always)]
#[allow(unused_variables)]
fn constant(op: $crate::ConstantOp) -> Self::T {
quantity!(@constant op $($conversion),+)
}
}
impl super::Conversion<V> for super::$unit {})+
}
storage_types! {
types: PrimInt, BigInt;
pub type T = $crate::num::rational::Ratio<V>;
#[inline(always)]
fn from_f64(value: f64) -> T {
<T as $crate::num::FromPrimitive>::from_f64(value).unwrap()
}
$(impl $crate::Conversion<V> for super::$unit {
type T = T;
#[inline(always)]
fn coefficient() -> Self::T {
from_f64(quantity!(@coefficient $($conversion),+))
}
#[inline(always)]
#[allow(unused_variables)]
fn constant(op: $crate::ConstantOp) -> Self::T {
from_f64(quantity!(@constant op $($conversion),+))
}
}
impl super::Conversion<V> for super::$unit {})+
}
storage_types! {
types: BigUint;
pub type T = $crate::num::rational::Ratio<V>;
#[inline(always)]
fn from_f64(value: f64) -> T {
use $crate::num::FromPrimitive;
let c = $crate::num::rational::Ratio::<$crate::num::BigInt>::from_f64(value)
.unwrap();
T::new(c.numer().to_biguint().unwrap(), c.denom().to_biguint().unwrap())
}
$(impl $crate::Conversion<V> for super::$unit {
type T = T;
#[inline(always)]
fn coefficient() -> Self::T {
from_f64(quantity!(@coefficient $($conversion),+))
}
#[inline(always)]
#[allow(unused_variables)]
fn constant(op: $crate::ConstantOp) -> Self::T {
from_f64(quantity!(@constant op $($conversion),+))
}
}
impl super::Conversion<V> for super::$unit {})+
}
storage_types! {
types: Ratio;
#[inline(always)]
fn from_f64(value: f64) -> V {
<V as $crate::num::FromPrimitive>::from_f64(value).unwrap()
}
$(impl $crate::Conversion<V> for super::$unit {
type T = V;
#[inline(always)]
fn coefficient() -> Self::T {
from_f64(quantity!(@coefficient $($conversion),+))
}
#[inline(always)]
#[allow(unused_variables)]
fn constant(op: $crate::ConstantOp) -> Self::T {
from_f64(quantity!(@constant op $($conversion),+))
}
}
impl super::Conversion<V> for super::$unit {})+
}

The only thing that wouldn't work is the FromStr impl which currently uses a match statement with all units:

uom/src/quantity.rs

Lines 514 to 517 in 3a236e5

match abbr.trim() {
$($abbreviation => Ok(Self::new::<super::super::$unit>(value)),)+
_ => Err(UnknownUnit),
}

@anicusan
Copy link

For future readers of this question: I managed to define a new unit for an existing quantity by reverse-engineering the uom crate after all macro expansions (using cargo expand).

For example, defining daltons as a unit of mass with base storage f64 would be done by:

// Define daltons for mass (= 1.66053906660e-27 kg (CODATA 2018))
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, Debug, Hash)]
pub struct dalton;

impl uom::si::Unit for dalton {
    #[inline(always)]
    fn abbreviation() -> &'static str {
        "Da"
    }
    #[inline(always)]
    fn singular() -> &'static str {
        "dalton"
    }
    #[inline(always)]
    fn plural() -> &'static str {
        "daltons"
    }
}
impl uom::si::mass::Unit for dalton {}

impl uom::Conversion<f64> for dalton {
    type T = f64;
    #[inline(always)]
    fn coefficient() -> Self::T {
        1.66053906660_E-27
    }
    #[inline(always)]
    #[allow(unused_variables)]
    fn constant(op: uom::ConstantOp) -> Self::T {
        match op {
            uom::ConstantOp::Add => -0.0,
            uom::ConstantOp::Sub => 0.0,
        }
    }
}
impl uom::si::mass::Conversion<V> for dalton {}

This way, you can use the unit interchangeably with any other from uom::si::mass - except using the FromStr trait's functionality.

@iliekturtles : Would it be possible to define the above in a friendlier user-facing macro? And thank you for this crate, it is a blessing for simulation code!

@iliekturtles
Copy link
Owner

@anicusan, extracting the relevant code from quantity! into a new user-facing unit! macro should definitely be possible. As I mentioned above I think that everything would work but the quantity's FromStr implementation. I'm imagining that the inner part of the macro loop starting on line 153 would be moved to the new unit! macro, including some of the internal macro arms. This would allow the existing quantity! macro and end-users to use the same code.

I'll take a look at doing this this weekend if no one else wants to work on a PR.

@iliekturtles
Copy link
Owner

I've started looking at this and run into the issue that there are multiple $($unit) loops in the quantity! macro. I'm still thinking about the issue but this may require multiple calls:

unit!(@meter: 1.0; "m", "meter", "meters"); // Setup struct
unit!@impls meter); // Setup impls that are currently defined in `storage_types!` blocks.

@iliekturtles
Copy link
Owner

I did some more work on this weekend and think I have it working. I haven't actually pushed the changes yet as I got side tracked on #206 which will make implementing this feature easier.

@iliekturtles
Copy link
Owner

Initial PR ready for some review. Documentation in src/unit.rs isn't updated yet and I have more changes to allow the quantity! macro to also be used outside the mod where system! was executed.

@iliekturtles
Copy link
Owner

@nakedible #209 is now ready for review/testing and I would be eternally grateful if you could take a look. A new unit! macro is now available and will allow you to add new units. See the documentation and examples/unit.rs for details.

I ended up stopping short of making the quantity! macro be able to be executed outside of direct submodules of the location where the system! macro was executed. Executing quantity! in an external crate runs into issues with attempting to implement external traits for external types and needing access to private items.

iliekturtles added a commit that referenced this issue Dec 13, 2020
The new `unit!` macro allows for new units to be defined outside of the
`quantity!` macro. Units defined using this macro will not be included
in the quantity unit enum or associated functions, or in the `FromStr`
implementation. Using this macro will create submodules for the
underlying storage types that are enabled (e.g. `mod f32`).

Resolves #173.
iliekturtles added a commit that referenced this issue Dec 28, 2020
The new `unit!` macro allows for new units to be defined outside of the
`quantity!` macro. Units defined using this macro will not be included
in the quantity unit enum or associated functions, or in the `FromStr`
implementation. Using this macro will create submodules for the
underlying storage types that are enabled (e.g. `mod f32`).

Resolves #173.
@nakedible
Copy link
Author

@iliekturtles It's been a while, and I managed to test this.

Using the unit! macro seems to work fine, as long as I use import macros via the old way of extern crate and #[macro_use].

However, if I try to simply say use uom::unit, I get an error about storage_types not being defined. If I add that, then I get errors for each of storage_type_* as well, and the saga never ends.

Not sure if this is worth a bug, but I'd prefer being able to import the unit macro without importing anything else.

@iliekturtles
Copy link
Owner

Glad it's working!

#232 exists for this issue. I just re-worded to better highlight the issue with the nested macros. The original wording made it just seem like an internal documentation issue rather than a problem for end-users.

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

3 participants