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 科学
每月下载量 69 次
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_exprs和adt_const_params。
此外,Diman处于开发初期阶段,API将会有所变化。
如果您无法为您的项目使用不稳定Rust或需要稳定的库,请考虑使用uom或dimensioned,这两个库都不需要任何实验功能,并且在总体上更加成熟。
功能
- 物理量之间的无效操作(例如,添加长度和时间)会变成编译错误。
- 新创建的量会自动转换为底层基础表示。这意味着使用的类型是维度(如
Length),而不是具体的单位(如meters),这使得代码更有意义。 - 可以通过
unit_system!宏定义维度和单位系统。这使用户能够完全自由地选择维度,并将它们作为用户库的一部分,因此可以针对它们实现任意新的方法。 rational-dimensions功能允许使用有理指数的量和单位。f32和f64浮点存储类型(分别在f32和f64功能门后面)。- 默认启用
std功能。如果禁用,Diman将是一个no_stdcrate,因此适用于使用在嵌入式设备(如GPU设备内核)上的情况。 num-traits-libm功能使用 libm 在no_std环境中提供数学函数。虽然可以在std中使用 libm,但 libm 的实现通常较慢,因此这不太可能被期望。- 通过
glam(在glam-vec2、glam-vec3、glam-dvec2和glam-dvec3功能之后)提供向量存储类型。 - 通过
serde(在serde功能门后面,有关更多信息,请参阅官方文档)进行序列化和反序列化。 - 使用
hdf5-rs(在hdf5功能门后面)支持 HDF5。 - 量(Quantities)实现了
Equivalence特性,因此可以通过mpi(在mpi功能门后面)使用 MPI 进行发送。 - 可以通过
rand(在rand功能门后面,有关更多信息,请参阅官方文档)生成随机量。
Quantity 类型
物理量由 Quantity<S, D> 结构体表示,其中 S 是基础存储类型(f32、f64、...)而 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 的无量纲方法,例如为无量纲量提供 sin、cos 等等。
let l1 = 5.0f64 * meters;
let l2 = 10.0f64 * kilometers;
let angle_radians = (l1 / l2).asin();
通过 squared、cubed、powi、sqrt、cbrt 支持指数运算和相关操作。
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_unchecked和new_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);
宏接受五个不同的关键字
quantity_type指定量类型的名称。对于编译器错误消息有指向的内容是必需的。dimension_type指定维度类型的名称。对于编译器错误消息有指向的内容是必需的。dimension定义了一个新的维度,它是一个类型。没有右侧的维度是基维度(例如,本例中的Length和Time),而有右侧的维度是导出维度(例如,本例中的Velocity)。unit定义了新的单位,它们是对应量的方法,constant定义了常量。没有右侧的单位是特定基维度的基单位,这意味着它们是内部用转换系数1表示的单位。基单位需要使用#[base(...)]属性来指定它们是哪个维度的基单位。有右侧的单位是从其他单位派生的。constant定义了一个新的常量。
SI前缀
单位前缀可以自动使用 #[prefix(...)] 属性为单位语句生成。例如
#[base(Length)]
#[prefix(kilo, milli)]
#[symbol(m)]
unit meters;
将自动生成单位 meters,符号为 m,以及 kilometers 和 millimeters,符号分别为 km 和 mm,对应于 1e3 m 和 1e-3 m。为了简化,提供了属性 #[metric_prefixes],它将自动生成从 atto- 到 exa- 的所有公制前缀。
别名
可以使用 #[alias(...)] 宏自动生成单位别名。例如
#[alias(metres)]
unit meters;
将自动生成一个与 meters 完全相同的定义的单位 metres。这与预期的前缀一起工作(即为每个带有前缀的单位生成一个别名)。
量积和商
有时,计算中的中间类型是那些没有很好名称且也不太需要的量。在这种情况下,需要将定义添加到单位系统中可能会很麻烦。这就是为什么提供了 Product 和 Quotient 类型。
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