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_std
crate,因此适用于使用在嵌入式设备(如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