#amplitude #parameters #events #dataset #rustitude #physics #calculate

rustitude-core

用于创建和操作粒子物理振幅分析模型的库

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

Download history 438/week @ 2024-04-29 153/week @ 2024-05-06 368/week @ 2024-05-13 149/week @ 2024-05-20 28/week @ 2024-05-27 148/week @ 2024-06-03 196/week @ 2024-06-10 580/week @ 2024-06-17 13/week @ 2024-06-24 11/week @ 2024-07-01 134/week @ 2024-07-15 248/week @ 2024-07-22 476/week @ 2024-07-29 18/week @ 2024-08-05

每月876次下载
3 crates 中使用

自定义许可

200KB
3K SLoC

揭示振幅分析的奥秘

GitHub Release GitHub last commit GitHub Actions Workflow Status GitHub License Crates.io Version docs.rs Codecov PyPI - Version Read the Docs

目录

概述

此包包含所有核心机制

  • rustitude 将自动并行化数据集中事件的振幅。开发者无需亲自编写并行化代码。
  • 在结构体上实现 Node 是使用它的唯一所需。这意味着开发者只需编写两到三个方法来描述其振幅的所有功能,其中一个只是给出振幅输入参数的名称和顺序。
  • rustitude 的一个主要目标是牺牲内存来提高处理速度。这是通过预先计算振幅的部分来实现的,这些部分在自由参数输入改变时不会改变。最简单的例子是 Ylm 振幅(球谐函数),给定 lm 的值可以完全预先计算。为了在评估时间计算这个振幅,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)是首选。

接下来,我们实现了针对 structNode 特性。在 Rust 中,特性有点类似于面向对象语言中的抽象类或接口,它们提供了一组必须由 struct 实现的方法。这些方法中的第一个是 fn precalculate,其签名如下: fn precalculate(&mut self, dataset: &Dataset) -> Result<(), RustitudeError>。正如签名所暗示的,它接受一个 Dataset 并以某种方式修改 struct。如果在评估过程中出现任何问题,它应引发一个 RustitudeError。该函数的预期用途是在振幅的数学表达式中预先计算一些术语,这些术语在更新振幅的自由参数输入时不会改变。在这种情况下,四个输入参数 $\alpha$、$\beta$、$\gamma$ 和 $\deltadalitz_zdalitz_sin3thetalambda 独立,因此我们可以提前安全地计算它们,并在以后需要时从相应的 Vec 中提取。在这里我不会深入 Rust 的语法,但典型的预先计算函数将首先并行迭代数据集的事件(需要使用 use rayon::prelude::*; 来使用 par_iter),并将迭代器收集或解压缩到 Vec 或一组 Vec 中。

计算步骤的签名是 fn calculate(&self, parameters: &[f64], event: &Event) -> Result<Complex64, RustitudeError>。这意味着我们需要取一个参数列表和一个单个事件,并将它们转换为一个复数值。《Event》结构体包含一个 index 字段,可以用来访问前一步中创建的预先计算的存储数组。

最后,parameters 函数返回一个参数名称列表,这些名称的顺序是期望在 calculate 中输入的。如果振幅没有任何自由参数(例如,在我的 YlmZlm 振幅实现中),我们可以完全省略这个函数,因为默认实现返回 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