20 个版本 (9 个主要更新)
新版本 9.0.0 | 2024年8月18日 |
---|---|
8.0.0 | 2024年8月2日 |
7.1.0 | 2024年8月1日 |
7.0.0 | 2024年7月30日 |
0.3.4 | 2024年5月6日 |
#4 in #amplitude
每月876次下载
在 3 crates 中使用
200KB
3K SLoC
揭示振幅分析的奥秘
目录
概述
此包包含所有核心机制
rustitude
将自动并行化数据集中事件的振幅。开发者无需亲自编写并行化代码。- 在结构体上实现
Node
是使用它的唯一所需。这意味着开发者只需编写两到三个方法来描述其振幅的所有功能,其中一个只是给出振幅输入参数的名称和顺序。 rustitude
的一个主要目标是牺牲内存来提高处理速度。这是通过预先计算振幅的部分来实现的,这些部分在自由参数输入改变时不会改变。最简单的例子是Ylm
振幅(球谐函数),给定l
和m
的值可以完全预先计算。为了在评估时间计算这个振幅,rustitude
只需要在数组中查找一个值即可!
安装
Cargo 提供了将此包包含到项目中的常用命令
cargo add rustitude-core
然而,此项目最好与一些预先制作的振幅一起使用(请参阅 rustitude-gluex
,这些与 rustitude
元包捆绑在一起,该元包还导出此包的预导入。这可以通过相同的命令安装
cargo add rustitude
实现振幅
尽管典型使用可能是使用各种组合的预先制作的振幅,但了解如何设计与此包无缝工作的振幅非常重要。让我们写下 rustitude
版本的 OmegaDalitz 振幅
use rayon::prelude::*;
use rustitude_core::prelude::*;
#[derive(Default)]
pub struct OmegaDalitz {
dalitz_z: Vec<f64>,
dalitz_sin3theta: Vec<f64>,
lambda: Vec<f64>,
}
impl Node for OmegaDalitz {
fn precalculate(&mut self, dataset: &Dataset) -> Result<(), RustitudeError> {
(self.dalitz_z, (self.dalitz_sin3theta, self.lambda)) = dataset
.events
.read()
.par_iter()
.map(|event| {
let pi0 = event.daughter_p4s[0];
let pip = event.daughter_p4s[1];
let pim = event.daughter_p4s[2];
let omega = pi0 + pip + pim;
let dalitz_s = (pip + pim).m2();
let dalitz_t = (pip + pi0).m2();
let dalitz_u = (pim + pi0).m2();
let m3pi = (2.0 * pip.m()) + pi0.m();
let dalitz_d = 2.0 * omega.m() * (omega.m() - m3pi);
let dalitz_sc = (1.0 / 3.0) * (omega.m2() + pip.m2() + pim.m2() + pi0.m2());
let dalitz_x = f64::sqrt(3.0) * (dalitz_t - dalitz_u) / dalitz_d;
let dalitz_y = 3.0 * (dalitz_sc - dalitz_s) / dalitz_d;
let dalitz_z = dalitz_x * dalitz_x + dalitz_y * dalitz_y;
let dalitz_sin3theta = f64::sin(3.0 * f64::asin(dalitz_y / f64::sqrt(dalitz_z)));
let pip_omega = pip.boost_along(&omega);
let pim_omega = pim.boost_along(&omega);
let pi_cross = pip_omega.momentum().cross(&pim_omega.momentum());
let lambda = (4.0 / 3.0) * f64::abs(pi_cross.dot(&pi_cross))
/ ((1.0 / 9.0) * (omega.m2() - (2.0 * pip.m() + pi0.m()).powi(2)).powi(2));
(dalitz_z, (dalitz_sin3theta, lambda))
})
.unzip();
Ok(())
}
fn calculate(&self, parameters: &[f64], event: &Event) -> Result<Complex64, RustitudeError> {
let dalitz_z = self.dalitz_z[event.index];
let dalitz_sin3theta = self.dalitz_sin3theta[event.index];
let lambda = self.lambda[event.index];
let alpha = parameters[0];
let beta = parameters[1];
let gamma = parameters[2];
let delta = parameters[3];
Ok(f64::sqrt(f64::abs(
lambda
* (1.0
+ 2.0 * alpha * dalitz_z
+ 2.0 * beta * dalitz_z.powf(3.0 / 2.0) * dalitz_sin3theta
+ 2.0 * gamma * dalitz_z.powi(2)
+ 2.0 * delta * dalitz_z.powf(5.0 / 2.0) * dalitz_sin3theta),
))
.into())
}
fn parameters(&self) -> Vec<String> {
vec![
"alpha".to_string(),
"beta".to_string(),
"gamma".to_string(),
"delta".to_string(),
]
}
}
让我们逐步分析这段代码。首先,我们需要定义一个struct
,它包含关于振幅的所有通用信息,在这个例子中还包括某种类型的Vec
来存储预计算的数据。我们把这些预计算的数据视为一个单独的数据集,每个数据集都有自己的振幅struct
的副本。因为这个特定的振幅没有输入参数,我们可以在它上面使用#[derive(Default)]
来创建默认构造函数,这样振幅就可以使用类似于let amp = OmegaDalitz::default();
的方式来初始化。如果我们想要一个参数化的构造函数,我们必须自己定义,虽然Rust没有为构造函数提供默认名称,但pub fn new(...) -> rustitude_core::Amplitude_64
(或_32
)是首选。
接下来,我们实现了针对 struct
的 Node
特性。在 Rust 中,特性有点类似于面向对象语言中的抽象类或接口,它们提供了一组必须由 struct
实现的方法。这些方法中的第一个是 fn precalculate
,其签名如下: fn precalculate(&mut self, dataset: &Dataset) -> Result<(), RustitudeError>
。正如签名所暗示的,它接受一个 Dataset
并以某种方式修改 struct
。如果在评估过程中出现任何问题,它应引发一个 RustitudeError
。该函数的预期用途是在振幅的数学表达式中预先计算一些术语,这些术语在更新振幅的自由参数输入时不会改变。在这种情况下,四个输入参数 $\alpha
$、$\beta
$、$\gamma
$ 和 $\delta
与 dalitz_z
、dalitz_sin3theta
和 lambda
独立,因此我们可以提前安全地计算它们,并在以后需要时从相应的 Vec
中提取。在这里我不会深入 Rust 的语法,但典型的预先计算函数将首先并行迭代数据集的事件(需要使用 use rayon::prelude::*;
来使用 par_iter
),并将迭代器收集或解压缩到 Vec
或一组 Vec
中。
计算步骤的签名是 fn calculate(&self, parameters: &[f64], event: &Event) -> Result<Complex64, RustitudeError>
。这意味着我们需要取一个参数列表和一个单个事件,并将它们转换为一个复数值。《Event》结构体包含一个 index
字段,可以用来访问前一步中创建的预先计算的存储数组。
最后,parameters
函数返回一个参数名称列表,这些名称的顺序是期望在 calculate
中输入的。如果振幅没有任何自由参数(例如,在我的 Ylm
和 Zlm
振幅实现中),我们可以完全省略这个函数,因为默认实现返回 vec![]
。
就这些了!然而,了解允许通过 Python 接口使用此振幅需要采取的步骤很重要。
Python 绑定
要创建 Python 绑定,需要将 pyo3
包含为依赖项
use pyo3::prelude::*;
use rustitude_core::amplitude::Amplitude_64;
// OmegaDalitz code here
#[pyfunction(name = "OmegaDalitz")]
fn omega_dalitz(name: &str) -> PyResult<Amplitude_64> {
Ok(Amplitude_64::new(name, OmegaDalitz::default()))
}
pub fn pyo3_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(omega_dalitz, m)?)?;
Ok(())
}
我们更愿意绑定一个返回 Amplitude_64
(或其 PyResult
)的函数,而不是直接绑定结构体,这个包装结构体实现了 #[pyclass]
,并且可以在 Python 接口中使用。然后需要将 pyo3_module
函数添加到 py-rustitude
软件包的 lib.rs
文件中。这一步有点问题,因为它意味着无法在不修改 py-rustitude
本身的情况下添加新的振幅。因此,如果开发者希望在 Python 中使用自定义振幅,他们可能需要与自己的仓库分支而不是由 cargo
安装的仓库一起工作。这是 pyo3
的一个限制,因为它不识别跨软件包的类绑定。还有通过纯 Python 类实现 Amplitude
的机制,但由于 GIL 的原因,它目前与多线程不兼容(见Python 文档)。
待办事项
不分先后,以下是我停止做出任何破坏性更改之前可能需要做的事情的列表
- 没有
uproot
后端的纯 Rust 解析 ROOT 文件(我在oxyroot
上有一些适度的成功,但还有几个读取大文件的问题) - 添加绘图方法
- 在编译时检查参数数量是否与输入匹配会很好,但不确定是否可行
- 振幅序列化(我希望能够像发送和共享
AmpTools
配置文件一样发送和共享振幅配置,但包含振幅本身) - 大量文档
- 大量测试
依赖项
~37MB
~742K SLoC