#dimensional-analysis #units #unit #time-unit #si-units #num-traits #const-generics

nightly no-std diman

零成本编译时维度分析

4 个版本 (破坏性)

0.5.0 2024年2月25日
0.4.0 2024年1月7日
0.3.0 2023年12月1日
0.2.0 2023年5月27日

#232 in 科学

Download history 11/week @ 2024-03-07 16/week @ 2024-03-14 15/week @ 2024-03-28 15/week @ 2024-04-04

每月下载量 69 次

MIT/Apache

78KB
1K SLoC

Diman是一个用于零成本编译时单位检查的库。

use diman::si::dimensions::{Length, Time, Velocity};
use diman::si::units::{seconds, meters, kilometers, hours, hour};

fn get_velocity(x: Length<f64>, t: Time<f64>) -> Velocity<f64> {
    x / t
}

let v1 = get_velocity(36.0 * kilometers, 1.0 * hours);
let v2 = get_velocity(10.0 * meters, 1.0 * seconds);

assert_eq!(v1, v2);
assert_eq!(format!("{} km/h", v1.value_in(kilometers / hour)), "36 km/h");

Diman在编译时防止单位错误

let time = 1.0 * seconds;
let length = 10.0 * meters;
let sum = length + time;

这会导致编译错误

let sum = length + time;
                   ^^^^
= note: expected struct `Quantity<_, Dimension { length: 1, time: 0, mass: 0, temperature: 0, current: 0, amount_of_substance: 0, luminous_intensity: 0 }>`
        found struct `Quantity<_, Dimension { length: 0, time: 1, mass: 0, temperature: 0, current: 0, amount_of_substance: 0, luminous_intensity: 0 }>`

免责声明

Diman是使用Rust的const generics功能实现的。虽然自Rust 1.51以来min_const_generics已稳定,但Diman使用更复杂的泛型表达式,因此需要两个当前不稳定的功能generic_const_exprsadt_const_params

此外,Diman处于开发初期阶段,API将会有所变化。

如果您无法为您的项目使用不稳定Rust或需要稳定的库,请考虑使用uomdimensioned,这两个库都不需要任何实验功能,并且在总体上更加成熟。

功能

  • 物理量之间的无效操作(例如,添加长度和时间)会变成编译错误。
  • 新创建的量会自动转换为底层基础表示。这意味着使用的类型是维度(如Length),而不是具体的单位(如meters),这使得代码更有意义。
  • 可以通过unit_system!宏定义维度和单位系统。这使用户能够完全自由地选择维度,并将它们作为用户库的一部分,因此可以针对它们实现任意新的方法。
  • rational-dimensions功能允许使用有理指数的量和单位。
  • f32f64浮点存储类型(分别在f32f64功能门后面)。
  • 默认启用std功能。如果禁用,Diman将是一个no_std crate,因此适用于使用在嵌入式设备(如GPU设备内核)上的情况。
  • num-traits-libm 功能使用 libmno_std 环境中提供数学函数。虽然可以在 std 中使用 libm,但 libm 的实现通常较慢,因此这不太可能被期望。
  • 通过 glam(在 glam-vec2glam-vec3glam-dvec2glam-dvec3 功能之后)提供向量存储类型。
  • 通过 serde(在 serde 功能门后面,有关更多信息,请参阅官方文档)进行序列化和反序列化。
  • 使用 hdf5-rs(在 hdf5 功能门后面)支持 HDF5。
  • 量(Quantities)实现了 Equivalence 特性,因此可以通过 mpi(在 mpi 功能门后面)使用 MPI 进行发送。
  • 可以通过 rand(在 rand 功能门后面,有关更多信息,请参阅官方文档)生成随机量。

Quantity 类型

物理量由 Quantity<S, D> 结构体表示,其中 S 是基础存储类型(f32f64、...)而 D 是量的维度。只要维度允许,Quantity 应该像其基础存储类型那样表现。

算术和数学

如果维度匹配,两个量的加法和减法是允许的

let l = 5.0 * meters + 10.0 * kilometers;

两个量的乘法和除法会产生一个新的量

let l = 5.0 * meters;
let t = 2.0 * seconds;
let v: Velocity<f64> = l / t;

如果 D 是无量纲的,那么 Quantity 和存储类型的加法和减法是可能的

let l1 = 5.0 * meters;
let l2 = 10.0 * kilometers;
let x = l1 / l2 - 0.5;
let y = 0.5 - l1 / l2;

Quantity 实现了 S 的无量纲方法,例如为无量纲量提供 sincos 等等。

let l1 = 5.0f64 * meters;
let l2 = 10.0f64 * kilometers;
let angle_radians = (l1 / l2).asin();

通过 squaredcubedpowisqrtcbrt 支持指数运算和相关操作。

let length = 2.0f64 * meters;
let area = length.squared();
assert_eq!(area, 4.0 * square_meters);
assert_eq!(area.sqrt(), length);
let vol = length.cubed();
assert_eq!(vol, 8.0 * cubic_meters);
assert_eq!(vol.cbrt(), length);
let foo = length.powi::<4>();

注意,与浮点等效不同,powi 接收其指数作为泛型,而不是作为普通函数参数。不支持使用非常数的整数对有量纲量进行指数运算,因为编译器无法推断返回类型的维度。然而,可以使用 powf 将无量纲量提升到任意幂。

let l1 = 2.0f64 * meters;
let l2 = 5.0f64 * kilometers;
let x = (l1 / l2).powf(2.71);

创建和转换

可以通过乘以单位或通过在单位上调用 .new 函数来创建新的量

let l1 = 2.0 * meters;
let l2 = meters.new(2.0);
assert_eq!(l1, l2);

有关 dimans SI 模块支持的单位的完整列表,请参阅 定义。可以通过单位的乘法/除法在原地定义复合单位

let v1 = (kilometers / hour).new(3.6);
let v2 = 3.6 * kilometers / hour;
assert_eq!(v1, 1.0 * meters_per_second);
assert_eq!(v2, 1.0 * meters_per_second);

注意,目前,通过这种方式定义的单位创建量比仅从单个单位(仅乘以一次)创建量要承担少量性能开销。一旦 const_fn_floating_point_arithmetic 或类似功能稳定,这将得到修复。

可以使用 value_in 函数将量转换为底层存储类型

let length = 2.0f64 * kilometers;
assert_eq!(format!("{} m", length.value_in(meters)), "2000 m");

这也适用于复合单位

let vel = 10.0f64 * meters_per_second;
assert_eq!(format!("{} km/h", vel.value_in(kilometers / hour)), "36 km/h");

对于无量纲量,.value()提供了对底层存储类型的访问。或者,无量纲量也实现了Deref以执行相同的操作。

let l1: Length<f64> = 5.0 * meters;
let l2: Length<f64> = 10.0 * kilometers;
let ratio_value: f64 = (l1 / l2).value();
let ratio_deref: f64 = *(l1 / l2);
assert_eq!(ratio_value, ratio_deref);

未检查的创建和转换

如果绝对需要,.value_unchecked()为所有量提供了对底层存储类型的访问。这不是单位安全的,因为返回值将取决于单位系统!

let length: Length<f64> = 5.0 * kilometers;
let value: f64 = length.value_unchecked();
assert_eq!(value, 5000.0); // This only holds in SI units!

同样,如果绝对需要,可以使用Quantity::new_unchecked从存储类型构造新的量。此操作也不是单位安全的

let length: Length<f64> = Length::new_unchecked(5000.0);
assert_eq!(length, 5.0 * kilometers); // This only holds in SI units!

当使用仅接受原始存储类型作为参数的第三方库时,value_uncheckednew_unchecked的组合非常有用。例如,假设我们有一个函数foo,它接受一个Vec<f64>并返回一个Vec<f64>,假设它对数字进行排序或执行其他单位安全操作。然后我们可以合理地编写

   let lengths: Vec<Length<f64>> = vec![
       1.0 * meters,
       2.0 * kilometers,
       3.0 * meters,
       4.0 * kilometers,
   ];
   let unchecked = lengths.into_iter().map(|x| x.value_unchecked()).collect();
   let fooed = foo(unchecked);
   let result: Vec<_> = fooed
       .into_iter()
       .map(|x| Length::new_unchecked(x))
       .collect();

调试

Debug已实现,并将以其基本表示形式打印量。

let length: Length<f64> = 5.0 * kilometers;
let time: Time<f64> = 1.0 * seconds;
assert_eq!(format!("{:?}", length / time), "5000 m s^-1")

自定义单位系统

unit_system

Diman还提供了unit_system宏,用于为SI单独未涵盖的所有内容定义自定义单位系统。宏将为可使用的新量类型添加新类型,并实现所有必需的方法和特性。例如,考虑以下宏调用

diman::unit_system!(
    quantity_type Quantity;
    dimension_type Dimension;

    dimension Length;
    dimension Time;
    dimension Mass;

    dimension Velocity = Length / Time;
    dimension Frequency = 1 / Time;
    dimension Energy = Mass * Velocity^2;

    #[prefix(kilo, milli)]
    #[base(Length)]
    #[symbol(m)]
    unit meters;

    #[base(Time)]
    #[symbol(s)]
    unit seconds;

    unit hours: Time = 3600 * seconds;
    unit meters_per_second: Velocity = meters / seconds;
    unit kilometers_per_hour: Velocity = kilometers / hours;
    constant SPEED_OF_LIGHT = 299792458 * meters_per_second;
);


fn too_fast(x: Length<f64>, t: Time<f64>) -> bool {
    x / t > 0.1f64 * SPEED_OF_LIGHT
}

too_fast(100.0 * kilometers, 0.3 * hours);

宏接受五个不同的关键字

  1. quantity_type指定量类型的名称。对于编译器错误消息有指向的内容是必需的。
  2. dimension_type指定维度类型的名称。对于编译器错误消息有指向的内容是必需的。
  3. dimension定义了一个新的维度,它是一个类型。没有右侧的维度是基维度(例如,本例中的LengthTime),而有右侧的维度是导出维度(例如,本例中的Velocity)。
  4. unit定义了新的单位,它们是对应量的方法,constant定义了常量。没有右侧的单位是特定基维度的基单位,这意味着它们是内部用转换系数1表示的单位。基单位需要使用#[base(...)]属性来指定它们是哪个维度的基单位。有右侧的单位是从其他单位派生的。
  5. constant定义了一个新的常量。

SI前缀

单位前缀可以自动使用 #[prefix(...)] 属性为单位语句生成。例如

#[base(Length)]
#[prefix(kilo, milli)]
#[symbol(m)]
unit meters;

将自动生成单位 meters,符号为 m,以及 kilometersmillimeters,符号分别为 kmmm,对应于 1e3 m1e-3 m。为了简化,提供了属性 #[metric_prefixes],它将自动生成从 atto-exa- 的所有公制前缀。

别名

可以使用 #[alias(...)] 宏自动生成单位别名。例如

#[alias(metres)]
unit meters;

将自动生成一个与 meters 完全相同的定义的单位 metres。这与预期的前缀一起工作(即为每个带有前缀的单位生成一个别名)。

量积和商

有时,计算中的中间类型是那些没有很好名称且也不太需要的量。在这种情况下,需要将定义添加到单位系统中可能会很麻烦。这就是为什么提供了 ProductQuotient 类型。

use diman::si::dimensions::{Length, Time};
use diman::{Product, Quotient};

fn foo(l: Length<f64>, t: Time<f64>) -> Product<Length<f64>, Time<f64>> {
    l * t
}

fn bar(l: Length<f64>, t: Time<f64>) -> Quotient<Length<f64>, Time<f64>> {
    l / t
}

有理维度

与只有整数值不同,rational-dimensions 功能允许在基本维度中使用有理指数的量。这使得可以表达定义维度和单位,例如

unit_system!(
    // ...
    dimension Sorptivity = Length Time^(-1/2);
    unit meters_per_sqrt_second: Sorptivity = meters / seconds^(1/2);
    // ...
);
let l = 2.0 * micrometers;
let t = 5.0 * milliseconds;
let sorptivity: Sorptivity = l / t.sqrt();

使用 rational-dimensions 生成的单位系统支持没有这些功能的单位系统支持的特设集。然而,这个功能只有在必要时才应该启用,因为维度不匹配时的编译器错误将更难以阅读。

serde

如果启用了 serde 功能门,则通过 serde 提供单位的序列化和反序列化。

#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Parameters {
    my_length: Length<f64>,
    my_vel: Velocity<f64>,
}

let params: Parameters =
     serde_yaml::from_str("
        my_length: 100 m
        my_vel: 10 m s^-1
    ").unwrap();
assert_eq!(
    params,
    Parameters {
        my_length: 100.0 * meters,
        my_vel: 10.0 * meters_per_second,
    }
)

rand

如果启用了 rand 功能门,则 Diman 允许通过 rand 生成随机量。


let mut rng = rand::thread_rng();
for _ in 0..100 {
    let start = 0.0 * meters;
    let end = 1.0 * kilometers;
    let x = rng.gen_range(start..end);
    assert!(Length::meters(0.0) <= x);
    assert!(x < Length::meters(1000.0));
}

依赖项

~0.4–2.7MB
~61K SLoC