Table of Contents
Overview
This crate contains all of the core mechanics
rustitudewill automatically parallelize amplitudes over the events in a dataset. There's no reason for a developer to ever write parallelized code themselves.- Implementing
Nodeon a struct is all that is needed to use it as an amplitude. This means developers need only write two to three total methods to describe the entire functionality of their amplitude, and one of these just gives the names and order of the amplitude's input parameters. - A major goal of
rustitudewas to increase processing speed by sacrificing memory. This is done by precalculating parts of amplitudes which don't change when the free parameter inputs change. The simplest example of this is theYlmamplitude (spherical harmonic), which can be entirely precalculated given the value oflandm. To calculate this amplitude at evaluation time,rustitudejust needs to look up a value in an array!
Installation
Cargo provides the usual command for including this crate in a project:
cargo add rustitude-core
However, this project is best-used with some pre-made amplitudes (see rustitude-gluex, and these come bundled with the rustitude meta-crate, which also re-exports this crate's prelude. This can be installed via the same command:
cargo add rustitude
Implementing an Amplitude
While typical usage might be to use pre-made amplitudes in various combinations, it is important to know how to design an amplitude which will work seamlessly with this crate. Let's write down the rustitude version of the OmegaDalitz amplitude:
use *;
use *;
Let's walk through this code. First, we need to define a struct which has all of the general information about the amplitude, and in this case some kind of Vec for storing precalculated data. We consider this precalculated data to correspond to a single dataset, and each dataset gets its own copy of the amplitude struct. Because this particular amplitude doesn't have any input parameters, we can #[derive(Default)] on it to make a default constructor, which allows the amplitude to be initialized with something like let amp = OmegaDalitz::default();. If we wanted a parameterized constructor, we have to define our own, and while Rust has no default name for constructors, pub fn new(...) -> rustitude_core::Amplitude_64 (or _32) is preferred.
Next, we implement the Node trait for the struct. Traits in Rust are kind of like abstract classes or interfaces in object-oriented languages, they provide some set of methods which a struct must implement. The first of these methods is fn precalculate(&mut self, dataset: &Dataset) -> Result<(), RustitudeError>. As the signature suggests, it takes a Dataset and mutates the struct in some way. It should raise a RustitudeError if anything goes wrong in the evaluation. The intended usage of this function is to precalculate some terms in the amplitude's mathematical expression, things which don't change when you update the free parameter inputs to the amplitude. In this case, the four input parameters, $\alpha$, $\beta$, $\gamma$, and $\delta$, are independent from dalitz_z, dalitz_sin3theta, and lambda, so we can safely calculate those ahead of time and just pull from their respective Vecs when needed later. I won't go too far into Rust's syntax here, but typical precalculation functions will start by iterating over the dataset's events in parallel (the line use rayon::prelude::*; is needed to use par_iter here) and collecting or unzipping that iterator into a Vec or group of Vecs.
The calculate step has the signature fn calculate(&self, parameters: &[f64], event: &Event) -> Result<Complex64, RustitudeError>. This means we need to take a list of parameters and a single event and turn them into a complex value. The Event struct contains an index field which can be used to access the precalculated storage arrays made in the previous step.
Finally, the parameters function just returns a list of the parameter names in the order they are expected to be input into calculate. In the event that an amplitude doesn't have any free parameters (like my implementation of the Ylm and Zlm amplitudes), we can omit this function entirely, as the default implementation returns vec![].
And that's it! However, it is important to be aware of the steps which need to be taken to allow this amplitude to be used through the Python interface.
Python Bindings
To make Python bindings, pyo3 needs to be included as a dependency:
use *;
use Amplitude_64;
// OmegaDalitz code here
Rather than bind the struct directly, we prefer to bind a function which returns a Amplitude_64 (or a PyResult of one), a wrapper struct that implements #[pyclass] and can be used in the Python interface. The pyo3_module function will then need to be added to the py-rustitude crate's lib.rs file. This step is a bit problematic, since it means new amplitudes cannot be added without modifying py-rustitude itself. For this reason, developers may want to work with their own fork of the repository rather than using the one installed by cargo if they wish to use Python with custom amplitudes. This is a limitation of pyo3, which doesn't recognize class bindings across crates. There is also a mechanism to implement Amplitudes via pure Python classes, but it is currently incompatible with multithreading due to the GIL (see the Python docs).
TODOs
In no particular order, here is a list of what (probably) needs to be done before I will stop making any breaking changes:
- Pure Rust parsing of ROOT files without the
uprootbackend (I have some moderate success withoxyroot, but there are still a few issues reading larger files) - Add plotting methods
- A way to check if the number of parameters matches the input at compile time would be nice, not sure if it's possible though
- Amplitude serialization (I want to be able to send and share an amplitude configuration just like an
AmpToolsconfig file, but with the amplitude itself included) - Lots of documentation
- Lots of tests