CM-IV / ballistics-rs

A Rust crate for external ballistics
https://crates.io/crates/ballistics_rs
MIT License
1 stars 0 forks source link

Using units from international standards #1

Open FarmingtonS9 opened 3 weeks ago

FarmingtonS9 commented 3 weeks ago

Hello CM-IV, I just stumbled across your project. I don't have an issue but rather a consideration, Out of curiosity, have you thought about using constants defined by and derived from SI Units? From there, you can convert to Imperial units using the From and/or Into traits. It is an added layer of complexity but it might help with people who are not clued into the imperial system (Percentage of the world that uses metric, by various metrics). In essence, offering both sides units they can understand. If it is for yourself, it'll likely not be worth the hassle but I'm merely thinking from its potential use by others (academic, military, hobby, etc). Anyway, thank you for reading this. I hope I am being helpful and not pushy or anything bad.

CM-IV commented 2 weeks ago

Hey there, thanks for the first issue. This is a pet project I'm doing on the side as it interests me. You bring up a good point about most people not using the imperial measuring system.

There are a few more functions I'd like to implement first in imperial units, going along with the source material I'm working with - but afterwards I can see that being the goal for the project. I am open to pull requests if you're interested in helping out.

FarmingtonS9 commented 2 weeks ago

Sweet! No worries. If I get around to write something up, I'll do a PR for you. I have my own personal project too. But it does intrigue me as a challenge to get better at contributing to other people's projects.

CM-IV commented 2 weeks ago

One way to go about it is to have an enum for UnitSystem and have a trait called UnitConversion with a fn convert() taking in the UnitSystem. The actual functions could still use imperial by default, but if a unit_system is passed in as a parameter specifying SI Units a conversion would take place:

#[bon]
impl SpeedOfSound {
    /// Calculates the speed of sound in air given the temperature.
    ///
    /// # Parameters
    /// - `temperature`: The temperature in degrees Fahrenheit.
    ///
    /// # Returns
    /// A `SpeedOfSound` instance representing the speed of sound at the given temperature.
    #[builder(finish_fn = solve)]
    pub fn calculate(
        temperature: Temperature, 
        #[builder(default)] 
        unit_system: UnitSystem
    ) -> Self {
        let speed = SpeedOfSound(49.0223 * (temperature.0 + 459.67).sqrt());

        match unit_system {
            UnitSystem::Imperial => speed,
            UnitSystem::SI => speed.convert(UnitSystem::SI)
        }
    }
}

#[bon]
impl KineticEnergy {
    /// Calculates the kinetic energy of a bullet given its weight and velocity.
    ///
    /// # Parameters
    /// - `bullet_weight`: The weight of the bullet in grains.
    /// - `velocity`: The velocity of the bullet in feet per second (ft/s).
    ///
    /// # Returns
    /// A `KineticEnergy` instance representing the kinetic energy of the bullet.
    #[builder(finish_fn = solve)]
    pub fn calculate(
        bullet_weight: BulletMass, 
        velocity: Velocity, 
        #[builder(default)] 
        unit_system: UnitSystem
    ) -> Self {
        let ke = KineticEnergy((bullet_weight.0 * velocity.0.powi(2)) / 450800.0);

        match unit_system {
            UnitSystem::Imperial => ke,
            UnitSystem::SI => ke.convert(UnitSystem::SI)
        }
    }
}
FarmingtonS9 commented 2 weeks ago

Looks neat. I like the use of an enum to choose your unit system and determines which way you want to go. I'm not sure if it's possible, but would you want to enforce the units throughout the calculation? Or is it better to trust the user is using the unit system correctly (i.e not mixing different units systems in their calculations)?

CM-IV commented 2 weeks ago

I asked a buddy of mine and he recommended to divvy it up more type-wise, with structs for SpeedUnit, DistanceUnit, etc that would help with enforcing the usage of correct units. That's a good question though, Rust's type system should make it possible to enforce correct units are being passed to the functions - that's why I'd like to keep using this Newtype struct approach if I can. I just provided one way of doing this off the top of my head, if you've got another idea maybe with generics or something via Unit<T> then I'm open to it.

FarmingtonS9 commented 2 weeks ago

Sounds like your buddy would help you a lot with the design of the unit system. He might be better to consult with (I'm still learning how to design programs). I'm not sure if you have come across these crates, but Quantity or Quantities might be of interest when dealing with units.

CM-IV commented 1 week ago

Here's another approach, no longer using the synonym crate for Newtypes.

use std::marker::PhantomData;

pub struct SI;
pub struct Imperial;

pub trait UnitSystem<'a>: Sized + 'a {}

impl<'a> UnitSystem<'a> for SI {}

impl<'a> UnitSystem<'a> for Imperial {}

/// Speed of sound given temperature
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
pub struct SpeedOfSound<T: for<'a> UnitSystem<'a>>(f64, PhantomData<T>);

impl<T: for<'a> UnitSystem<'a>> SpeedOfSound<T> {
    pub const fn new(value: f64) -> Self {
        Self(value, PhantomData)
    }
    pub fn value(&self) -> f64 {
        self.0
    }
}

pub const SPEED_OF_SOUND_SEA_LEVEL: SpeedOfSound<Imperial> = SpeedOfSound::new(1116.28);

macro_rules! unit_system_match {
    ($type:ty, $imperial_expr:expr, $si_expr:expr) => {
        {
            use std::any::TypeId;

            if TypeId::of::<$type>() == TypeId::of::<Imperial>() {
                $imperial_expr
            } else if TypeId::of::<$type>() == TypeId::of::<SI>() {
                $si_expr
            } else {
                unreachable!("Unexpected unit system")
            }
        }
    };
}

#[bon]
impl<T: for<'a> UnitSystem<'a>> SpeedOfSound<T> {
    /// Calculates the speed of sound in air given the temperature.
    ///
    /// # Parameters
    /// - `temperature`: The temperature in degrees Fahrenheit or Celsius.
    ///
    /// # Returns
    /// A `SpeedOfSound` instance representing the speed of sound at the given temperature.
    #[builder(finish_fn = solve)]
    pub fn calculate(temperature: Temperature<T>) -> Self {
        unit_system_match!(T,
            {
                let temp_f = temperature.value();
                SpeedOfSound::new(49.0223 * (temp_f + 459.67).sqrt())
            },
            {
                let temp_c = temperature.value();
                let temp_k = temp_c + 273.15;
                SpeedOfSound::new(331.3 * (temp_k / 273.15).sqrt())
            }
        )
    }
}