1 Introduction

Feynman diagrams have been indispensable in the calculation of scattering amplitude and their comparison with experiments. As the need for amplitudes with a greater number of final-state particles increased, and as computational power increased, computer programs increasingly took on the challenge of generating Feynman diagrams and integrating the resulting amplitudes. An important early milestone in this process was the creation of HELAS: Helicity Amplitude Subroutines for Feynman Diagram Evaluation [1]. This was a set of Fortran routines for each of the vertices and propagating lines in a Feynman diagram and instructions for combining them to form complete Feynman diagrams and amplitudes. The HELAS routines form the basis of some modern matrix-element generators such as MadGraph [2, 3].

Among the strengths of Feynman diagrams is that the algorithm for generating them is completely general and their field theory foundation makes the construction of theories that are local, Lorentz invariant and renormalizable straight forward [4]. On the other hand, they require the addition of unphysical degrees of freedom for massive spin-1 and massless helicity-\({\pm }1\) bosons, and an accompanying gauge invariance to insure their cancellation. As a result, each diagram on its own is not typically physically meaningful. Only gauge invariant sets of diagrams, with significant cancellations between the members of the set, can be considered as physical. The number of diagrams in a gauge-invariant set increases exponentially with the number of legs.

Over the last several decades, a new form for scattering amplitudes has been discovered that include simpler objects that do not include unphysical degrees of freedom. In the early years, this was focused on purely massless theories and made use of “twistors” [5, 6], culminating in results that were radically simpler than their equivalent Feynman-diagram forms [7], sometimes with only one simple expression taking the place of thousands or millions of Feynman diagrams. Moreover, these simple results did not have a gauge parameter and so each part of the expression was physically meaningful and trivially gauge invariant. Further, an algorithm was found for these twistor amplitudes for any number of particles, so long as the theories were purely massless [8].

Twistors are objects that transform under a product of the Lorentz symmetry and the (little-group) helicity symmetry [4, 9], and are therefore only appropriate for massless particles. We call these helicity spinors. Further progress was made in extending these massless twistors to objects that transform under a product of the Lorentz symmetry and the (little-group) spin symmetry, which we call spin spinors, allowing them to represent massive particles [10]. This laid the foundation for generating scattering amplitudes that bypass fields and Feynman diagrams. These amplitudes also do not have any unphysical degrees of freedom and do not need or have a gauge parameter and are therefore trivially gauge invariant. We call these theories “constructive” to distinguish them from field-theory Feynman diagrams.

The 3-point vertices in the constructive Standard Model (SM) were found in [11] and some initial amplitudes were validated against Feynman diagrams [12]. However, a challenge was found when considering amplitudes with massive external particles and massless internal helicity-\({\pm }1\) particles [13], but it was resolved [14]. This opened the floodgate for further amplitude calculations in the SM and their validation against Feynman diagrams.

With these advances in constructive amplitudes, it became clear that a general computational package for calculating phase-space points of constructive amplitudes would be beneficial. It would allow the comparison with Feynman diagrams to be more efficient than comparing them analytically, as well as allowing numerical comparison when analytic comparison became too complicated. It could also potentially form the foundation for a new generation of matrix-element generators, based on constructive amplitudes in an analogous way to the HELAS package and Feynman diagrams. To this end, we created this package, SPINAS, described in this paper. This C++ package has a collection of classes and methods to support the easy implementation of constructive amplitudes. In companions to this paper, the 4-point vertices of the constructive SM were determined [15], and a comprehensive set of 4-point amplitudes in the constructive SM, validated against Feynman diagrams, was found [16]. Indeed, the implementation of a complete set of constructive 4-point amplitudes and their validation against Feynman diagrams formed a critical part of the validation of this package, as we describe in this paper.

In the rest of this paper, we describe the use of this package. We begin with a “user guide” in Sect. 2, where we describe each of the components of the SPINAS package that are directly used when implementing a new constructive amplitude. In Sect. 3, we give a complete example of using this package to implement the quantum electrodynamics (QED) process \(e,\bar{e}\rightarrow \mu ,\bar{\mu }\). In Sect. 4, we give further detail about the design of the SPINAS package that would be useful for a person contributing to this code. In fact, in Sect. 4.1, we note that this package is released under the Gnu Public License (GPL) v3, giving users the right to modify and share this package, under the condition of the GPLv3. Our intention is for the future of this package to be determined and driven by the needs of the community. Finally, in Sect. 5, we describe the validation of this package through a large number of unit tests and a comparison of constructive amplitudes and Feynman diagrams in a comprehensive set of 4-point amplitudes.

2 User guide

In this section, we give the details that are most relevant to a typical novice SPINAS user. We will consider support, compilation of the SPINAS source code, compilation of the user’s code, data types in SPINAS, the SPINAS namespace, the classes, methods and functions typically used, and some tips for troubleshooting the user’s code. For users that want to dive more deeply into the core code, we refer them to Sect. 4.

We have written and validated this package on Linux and our instructions will be for Linux. We welcome the communities contribution of support for other operating systems.

2.1 Support

Navigating the complexities of writing, compiling, linking, and running source code can be challenging. While the author aims to provide sufficient documentation for SPINAS, it is not feasible to personally address every support query. To foster a self-sustaining community, users are encouraged to collaboratively use and contribute to several support tools available on our GitHub site [17]. To streamline support and enhance collaboration, please direct inquiries to these platforms instead of the author’s personal email. The author will participate in these community efforts as time permits, but the collective expertise and collaboration of the SPINAS community are invaluable.

a. GitHub Wiki: The SPINAS Wiki [18] serves as a dynamic platform for the community to create and share supplementary documentation, including FAQs, tips, and other resources that extend beyond this article. Contributions from users of all experience levels are highly encouraged. Those interested in contributing can request editing access to enrich the Wiki with valuable information and insights.

b. Discussions: The SPINAS Discussions page [19] provides an informal space for users to initiate and engage in conversations on a wide range of topics related to SPINAS. This forum is ideal for asking and answering questions, seeking and offering help, and sharing experiences. We warmly invite members of the community to not only seek assistance here but to also support others, as even insights from newer users can be highly beneficial.

c. Issue Tracker: Bugs and feature requests are managed through the SPINAS Issue Tracker [20]. This tool is not only for reporting issues but also a platform where community members can actively contribute by addressing bugs and enhancing the software. Users at all skill levels, including those new to coding or SPINAS, are encouraged to participate, starting with simpler tasks and progressively taking on more complex challenges. More details on contributing to the codebase can be found in Sect. 4.

Users are advised to utilize the Discussions tool for preliminary discussion of any issues or feature ideas. This preliminary step often helps clarify whether a situation is indeed a bug or if a proposed feature is necessary, potentially resolved through community dialogue.

To increase the likelihood of receiving support and resolving issues, consider the following when reporting a bug:

  • Isolate the problem to the smallest possible code snippet that reproduces the issue. Large code submissions without specific focus are less likely to be addressed by the community. Responsibility lies on the reporter to provide a concise, reproducible example.

  • Ensure your bug report is clear and comprehensive, including sufficient code and a detailed explanation to enable easy replication by others. Vague reports with statements like “It doesn’t work” are less likely to receive attention.

Adherence to these guidelines greatly enhances your chances of receiving effective support and responses from the community.

d. AI: The ability of large language models (LLMs) is astonishing and only improving, especially in the area of coding. Although they can’t yet do everything we would like, nor are they always correct (so-called halucinations are an issue), they can still be extremely helpful. We encourage users to carefully take advantage of these tools to support them in the use of this code.

2.2 Dependencies

To ensure full functionality and compatibility of this package, it is essential to satisfy the following dependencies. Installation can typically be achieved through system-specific package managers or with assistance from system administrators.

a. CMake: For the potential of cross-platform compilation, we utilize CMake (version 3.5 or higher). CMake can be downloaded from the official website [21]. Detailed instructions for installation are available there.

b. C++ Compiler: As the package is developed in C++, an appropriate C++ compiler supporting the ISO/IEC 14882:2011 standard (commonly known as C++11) or later is required. Our testing exclusively employed the GNU Compiler Collection [22]. However, other compilers should also be compatible.

c. Boost Unit Test Framework: For module testing, we employ the Boost Unit Test framework (version 1.71 or higher) [23]. This framework was selected for its extensive features and compatibility with various C++ environments, contributing significantly to the robust testing of our package.

2.3 Package download

The SPINAS package is readily available for download at [24] as a .tar.gz file. For those with experience in software development or version control systems, the package’s source code is also accessible directly through our GitHub repository [17]. We actively encourage and welcome contributions from the community, as detailed in Sect. 4.

Upon downloading the SPINAS package, users should move the downloaded file to their preferred working directory. This directory will serve as the primary location for storing and working with the SPINAS code.

In order to unpack the package, the user should open a terminal and navigate to the directory containing the file and execute tar -xzvf spinas-vX.X.X.tar.gz, where X.X.X represents the version number. This command will extract the package contents into the current directory.

2.4 SPINAS compilation

Upon successfully unpacking the SPINAS package, the user can initiate the setup by first changing into the SPINAS root directory. You can verify you’re in the root directory of SPINAS by listing the contents with ls and ensuring the presence of files like CMakeLists.txt, LICENSE, and README, along with directories such as include, SM, source, tests, and user-dir.

In the root directory of SPINAS, the user should create a separate build directory using:

figure a

where the second step changes into the build directory. This directory will host all the compilation files, keeping the source code intact in the root directory.

The build process is initiated with the command:

figure b

This step configures the build environment within the build directory, leaving the root directory unchanged. You may clear the build directory at any time to reset the compilation process without affecting the source code.

Next, we compile the package using:

figure c

The optional -jN flag enables parallel compilation using N CPUs, enhancing its speed (N should be replaced with the number of CPUs to use). Absence of this flag defaults to single-CPU compilation. This process compiles the SPINAS package but, again, does not alter any files in the root directory.

After the compilation is complete and successful, the user should run the Boost Unit Tests by executing:

figure d

This step runs extensive tests on each package component. Occasional errors may arise due to numerical precision limits. However, if they do not persist upon retesting, the compilation should normally be considered successful.

If the Unit Tests are successful, the user should run the further SM process tests with the command:

figure e

This will run a comprehensive set of \(2\rightarrow 2\) processes in the SM, comparing their results with those from Feynman diagrams. Under normal circumstances, all these tests should pass.

With these steps, the SPINAS package is fully compiled and operational. It is advisable to refrain from modifying files in both the build and root directories to ensure stability and integrity of the package.

2.5 Compiling user code with SPINAS

Before the user writes code for their process, they should create a new directory to contain their source code and binaries outside the SPINAS root directory, in order to keep the source code clean. We will call this directory user-dir and assume that the user has used cd to get into user-dir, so that all terminal instructions in this subsection are assumed to be from inside this directory.

In order to write source code for a new process, the user should open a text editor and create a new file in the user-dir. We will call this file user-file.cpp. Basic no-frills text editors are emacs or vi on Linux. Once user-file.cpp is open in the text editor, the user can create their code. A complete example of creating this file as well as compiling and running it can be found in Sect. 3. Here, we will assume the file is created and saved in the user-dir.

Our next step is to compile the new code and can be done with a command such as:

figure f

all on one line. g++ refers to the C++ compiler, and it should be the same compiler that was used to compile the SPINAS package. The file user-bin is the name of the binary and can be chosen by the user. user-file.cpp is the source file to compile. /path/to/spinas should be replaced by the path to the SPINAS root directory. -I/path/to/spinas/include and -L/path/to/spinas/build give the paths to the SPINAS include files and the SPINAS library, respectively. -lspinas tells the compiler to link against the SPINAS library. -std=c++11 tells the compiler to use the C++11 standard, and is required to make your program consistent with the SPINAS package. A later standard can also be used. The flag -DWITH_LONG_DOUBLE is required to ensure that the precision of the user binary is the same as the SPINAS package. The optional flag -O3 tells the compiler to optimize the binary, making it more efficient. If errors are encountered in the compilation, the user is encouraged to begin with the first error and work their way down. Sometimes clearing up earlier bugs resolves later ones.

Once the user’s file is compiled, it can be run with a command such as

figure g

on Linux.

2.6 Data types in SPINAS

Precision consistency is crucial in numerical calculations, particularly in physics simulations. To achieve this in the SPINAS package, we have introduced two specific data types. The first is ldouble for real values, and the second is cdouble for complex values. We strongly encourage users to consistently use these types in their code. This practice not only maintains high precision but also prevents type errors that can arise from mismatched data types.

a. ldouble: The ldouble type is designed for variables holding real numbers, typically floating-point numbers. By default, ldouble is defined as long double, which typically occupies 16 bits on most modern computer architectures. This provides a higher precision compared to standard double. Here are a couple of usage examples:

figure h

In these examples, MW is assigned the mass of the W boson in GeV, and three is a floating-point representation of the integer 3. They can now be used in amplitude expressions, while maintaining a consistently high level of precision.

b. cdouble: For handling complex numbers, the cdouble type is required. It stores a pair of ldouble values representing the real and imaginary parts of a complex number. This allows for precise and accurate handling of complex algebra. Here are a few examples:

figure i

In these examples, amplitude initializes a complex number to zero, two represents the integer 2, and i represents the imaginary unit. These complex variables can also be used in amplitude calculations and, as before, the precision is kept consistent and high.

It’s also important to note that amplitude calculations in SPINAS can involve both ldouble and cdouble types. Their interactions, such as sums and products, are consistently handled to ensure precision. For example:

figure j

This expression combines both types and is perfectly valid in SPINAS.

However, mixing raw integers or explicit floating-point values directly in expressions can lead to precision issues. Therefore, it’s advisable to store such values in variables of type ldouble or cdouble before using them in calculations. For instance, the expression 3/(2.0*MW) might be less precise or even cause compilation errors compared to three/(two*MW). This approach not only facilitates successful compilation but also ensures the precision and consistency of the calculations.

Finally, as outlined in the previous subsection, users must include the -DWITH_LONG_DOUBLE flag in their compilation commands. This flag is a default in SPINAS compilation and ensures that the user code aligns with the precision standards set by the package.

2.7 The SPINAS namespace

To maintain a clear organizational structure and avoid potential naming conflicts with core C++ code or other packages, the SPINAS package encapsulates its components within the spinas namespace. This namespace includes all essential classes, functions, and data types that form the backbone of SPINAS’s functionalities. Utilizing a dedicated namespace is a standard practice in software development to segregate package-specific elements from the global scope.

Each class and function described in this documentation, pertaining to the SPINAS package, resides within the spinas namespace. Consequently, their usage in user code necessitates a prefix of spinas:: to denote their affiliation with the SPINAS package. For instance:

figure k

In this example, after including the main header file of SPINAS, a particle object p1 is declared using the particle class from the SPINAS package. By prefixing spinas:: to particle, we explicitly indicate that the class is a member of the SPINAS namespace, distinguishing it from any similarly named classes in other libraries or the standard C++ library.

It is important to note that this namespacing convention applies only when referencing class or function names from the SPINAS package. Once an object, such as p1 in the example, is instantiated, it can be used directly without the namespace prefix, as demonstrated in the object’s method call p1.set_mass(MW). This practice ensures clarity in code while leveraging the organizational benefits of a dedicated namespace.

2.8 Class: particle

In SPINAS, the particle class plays a crucial role in representing external particles involved in physical processes. This includes both incoming and outgoing particles. Each particle object encapsulates the particle’s properties such as its momentum and its representation in spinor form, crucial for calculating spinor products. It is important to create a distinct particle object for each external particle in a process, even if they represent the same Standard Model (SM) particle. For instance, if multiple electrons are part of the process, each should have its own particle object.

The constructor of the particle class requires a single parameter of type ldouble which represents the mass of the particle. Consider the following code snippet:

figure l

Here, p1 and p2 are initialized as electrons with mass me, while p3 and p4 are muons with mass mm.

To modify the mass of a particle after its instantiation, the set_mass method is provided. This method also takes a single ldouble argument, the new mass value. For example:

figure m

Here, me_new is the updated mass value for the electrons.

Setting the momentum of the particle is essential for computing the amplitude at a specific phase-space point. This is achieved using the set_momentum method. It requires a reference to a 4-dimensional array of type ldouble, representing the momentum components. For instance:

figure n

In this example, mom1 is defined as a 4-dimensional array and initialized with appropriate values, including energy computed using the square root function from the standard C++ library.

Additionally, the particle class offers the dot method for calculating the inner product of the momenta of two particles. This method accepts another particle object as its argument. Therefore, the inner product \( p_1 \cdot p_2 \) can be calculated as follows:

figure o

An essential aspect of utilizing the particle class is the setting of particle momentum for each phase-space point. This class is designed to automatically handle the computation of associated spinors and the 2x2 complex momentum matrices, which are integral in spinor product calculations. As such, the correct and timely use of the set_momentum method becomes imperative. It is crucial to invoke this method for every particle at each phase-space point before initiating any related calculations. Failure to update the particle’s momentum for a new phase-space point prior to calculations could lead to incorrect results, as the computations would be based on outdated data.

2.9 Class: propagator

The propagator class in SPINAS is designed to represent particles on internal lines in constructive diagrams. This class is primarily responsible for providing the propagator denominator. A key feature of the propagator class is that it stores the mass and width of the particle it represents, but it does not internally store the momentum. Therefore, one of the practical aspects of using the propagator class is that a single object can be reused for the same particle appearing on multiple internal lines, provided the particle’s mass and width remain constant. However, on the other hand, distinct propagator objects must be created for internal particles with different masses and widths.

The constructor of the propagator class accepts two parameters: the mass and the width of the particle. For example:

figure p

Should there be a need to modify the mass or width of a propagator, the propagator class offers the set_mass and set_width methods. These methods allow the user to update the mass and width of the particle, respectively:

figure q

The core functionality of the propagator class is encapsulated in the denominator method. This method calculates the propagator denominator and requires a single argument: a reference to a 4-dimensional array of type ldouble, representing the momentum of the propagator. Since the momentum of an internal particle is typically a combination of external momenta, it must be computed before invoking denominator. For instance, if we wanted the propagator denominator

$$\begin{aligned} \left[ \left( p_1+p_2\right) ^2-M_Z^2-i M_Z \Gamma _Z\right] , \end{aligned}$$
(1)

we could do:

figure r

In this example, propP is initialized as a 4-dimensional array to store the propagator momentum. It is calculated by combining the relevant components of external momenta. Finally, the denominator method computes the propagator denominator, returning a complex number of type cdouble, suitable for subsequent amplitude calculations.

An additional utility of the propagator class is its application in calculating Mandelstam variables. These variables, such as the s-parameter, can be conveniently computed using a propagator object. To do this, create a propagator instance with zero mass and width, and then utilize its denominator. For example:

figure s

In this snippet, prop0 is a propagator object with no mass or width. The ... indicates the process of calculating propP, as demonstrated in the previous example. The result, M12, represents the \(s\)-parameter, calculated using the denominator of prop0.

The choice of naming this parameter M12 is intentional to avoid confusion with the naming conventions typically used for spinor products. For example, a name like s12 would closely resemble s12s, a common notation for the spinor product \([\textbf{12}]\). While these names can be chosen at the user’s discretion, we recommend this naming scheme for its clarity and to prevent potential ambiguities in complex calculations.

2.10 Class: sproduct

In constructive field theories, spinor products are essential for calculating amplitudes. To facilitate this, the sproduct class in SPINAS is designed to store references to particles involved in a spinor product and to compute these products efficiently. This class manages the Lorentz indices, ensuring correct matching. Once a sproduct object is constructed, the user needs to update it only when the phase-space point changes, and then use its methods to derive the spinor product for the desired spin configuration.

Constructing an object of the sproduct class involves passing multiple arguments. The first argument specifies the type of the left spinor, either an angle spinor (ANGLE) or a square spinor (SQUARE). The second argument is a reference to the particle corresponding to this left spinor. Subsequent arguments can include up to six intermediate momenta, represented by references to their respective particles, sandwiched between the left and right spinors. The final argument is a reference to the particle associated with the right spinor. For example, to define spinor products such as \(\langle \textbf{13}\rangle \), , , , and , the declarations in the header file might look like:

figure t

These objects can then be instantiated in the process constructor as follows:

figure u

In this context, specifying whether the spinors are helicity or spin spinors is unnecessary, as this information is deduced from the masses of the particles, which are inherent to the particle objects. Additionally, the type of the right-end spinor (angle or square) is inferred based on the left spinor type and the number of intermediate momenta. It is also important to note that by default, the spinors are assumed to have upper spin indices if they are massive. This assumption aligns with the conventional treatment of uncontracted spin indices in amplitude formulas. The method to specify lower indices for spin contractions will be covered later in this subsection.

Moreover, the sproduct objects incorporate references to the particle objects, which inherently include their momenta. Consequently, it is unnecessary to explicitly supply phase-space points to the sproduct instances; this information is implicitly derived from the associated particle objects. However, it is important to note that the sproduct class retains the calculations performed for previous phase-space points, to enhance efficiency of the amplitude calculation. Therefore, in order to update these calculations for new phase-space configurations, the update method must be used. This update should occur after setting the new momenta for the particles, but before calculating the amplitude expression. For instance, when a new phase-space point is selected, the user should first update the momenta of all particles and then refresh all the spinor products as follows:

figure v

For the practical application of updated spinor products in amplitude calculations, the sproduct class provides the v method, which stands for “value". This naming choice is intended to keep amplitude expressions compact, thereby enhancing their readability and simplifying debugging of the amplitude expression. The v method returns a complex number of type cdouble and is designed to be directly integrated into amplitude expressions. Furthermore, this method supports four different spin combinations, adapting to the nature of the spinors involved. If both spinors represent massless particles and are thus helicity spinors without spin indices, the v method requires no arguments. For example, in a scenario where particle 4 is massless, the spinor product \([4|p_1p_2|4]\) would be integrated into an amplitude expression as follows:

figure w

When dealing with spinor products involving both massive and massless spinors, the sproduct class adapts to represent their distinct spin properties. In such cases, where one spinor is massive (with a spin index) and the other is massless (without a spin index), the spinor product has only one spin index. Consequently, the v method in this scenario takes a single integer argument. This argument represents twice the spin of the massive particle’s spinor and accepts only two values: \(+1\) for spin \(+\frac{1}{2}\) and \(-1\) for spin \(-\frac{1}{2}\). No other values are permissible. It’s important to note that particles with higher spin require symmetric combinations of spinors, which will be discussed further in Sect. 2.11. The user does not need to specify which spinor the spin index refers to; this is automatically determined based on the particles’ properties, with the spin index pertaining to the massive particle’s spinor. For instance, if we aim to compute the spin-up component for particle 2 in the spinor products and , we might define ds2=1 (double the spin of particle 2) and use it as follows:

figure x

Here, because particle 4 is massless, the sproduct object interprets the spin argument appropriately for each term, recognizing it as pertaining to the massive spinor in the product (the left spinor in the first term and the rightspinor in the second term).

In cases where both spinors are massive and thus each has a spin index, the v method requires two arguments. Each argument corresponds to twice the spin of each respective spinor. For example, let’s say we want to evaluate spinor products with a specific spin combination defined by ds1=1, ds2=-1, and ds3=-1. The spinor products \(\langle \textbf{13}\rangle \) and \(\langle \textbf{1}|p_4|\textbf{2}]\) in an amplitude expression would be represented as:

figure y

In this context, the user must input the spin arguments in the order corresponding to the spinors in the product. The design of the sproduct class and the convention for naming spinor-product variables and spin indices are intended to assist in maintaining this order, thus ensuring clarity and accuracy in the representation of spinor products in amplitude calculations.

In most situations, the constructors and methods previously described for the sproduct class will suffice. Typically, spin contractions should simplified during the preliminary stages of amplitude calculation, meaning that by the time one reaches the phase-space point calculations, no spin contractions should remain. However, there is an exception to this rule, particularly relevant in scenarios involving production processes with spin-correlated decays. In such cases, it becomes necessary to contract the spins of the final states in the production process with the spins of the initial states in the decays. This requirement implies that one spin index should be upper, as has been the default assumption so far, and the other should be lower. To accommodate this, the sproduct class includes constructors that allow specifying the position of the spin index.

It is essential to note that when the position of a spin index is not explicitly stated, it is conventionally assumed to be upper. Therefore, the constructor examples provided earlier, which do not specify the spin index position, are all assumed to have upper indices. However, if a scenario requires the left spinor to have a lower index, the argument LOWER is added immediately after the particle reference for the left spinor. For instance:

figure z

In these examples, the left spinor is set to have a lower index in the first and third cases (a13a and a142s), while it remains upper in the second case (s24s). For all three examples, the right spinor is assumed to be upper since its position was not specified.

When the intention is to designate the right spinor as lower, the LOWER keyword should be placed after the reference to the right spinor. For example:

figure aa

In these examples, the right spin index is set to lower for the second and third constructs (s42s and a142s), while it remains upper for the first one (a13a). The left spinor maintains an upper index in all three cases.

Additionally, it is possible to configure both the left and right spinors as lower. This is achieved by adding the LOWER argument after both particle references. For instance:

figure ab

In this configuration, both sides of the spinor products are set to have lower indices.

It is important to note that the specification of the spin index (upper or lower) is exclusively determined in the constructor of the sproduct class. All other methods, including update() and v(...), function identically regardless of this specification. The update() method does not require any arguments, while the v(...) method accepts between zero and two arguments, corresponding to twice the spin of the spinors. The distinction between upper and lower indices is established solely by the constructor, streamlining the process and maintaining consistency across different methods of the class.

2.11 Class: process

The process class is a fundamental component of SPINAS, designed to underpin process calculations. Its structure allows for expansion with additional methods in future updates. The primary function of this class is to act as a base class, enabling users to define their specific process classes by inheriting from process. For instance, in simulating the process \( e,\bar{e} \rightarrow \mu ,\bar{\mu } \), a class named eemm could be declared to inherit from process:

figure ac

Inheriting from the process class endows eemm with a pre-defined set of methods, augmenting the user’s custom functionalities.

The methods in process currently fall into two categories. The first includes methods for handling spin combinations for particles of spin 1, such as the W and Z bosons. The second category consists of methods for validating squared amplitude calculations against established Feynman-diagram results, specifically in \(2\rightarrow 2\) processes. These methods are crucial for ensuring the accuracy and reliability of user-generated calculations.

2.11.1 Higher-Spin Spinors

In amplitude calculations involving spin-1 massive bosons, such as the \( W \) and \( Z \) bosons, each boson is represented by a pair of spin-\(\frac{1}{2}\) spinors. While manually incorporating the necessary loops and normalization factors is feasible for a single, or even two, spin-1 bosons, this task becomes more challenging as their number increases. To aid in this process, SPINAS provides several functions designed to streamline these calculations.

Understanding these functions requires revisiting how we represent the spin states of spin-1 particles through the combination of two spin-\(\frac{1}{2}\) spinors. The three spin states are constructed as follows:

$$\begin{aligned} |1,+1\rangle&= \bigg |\frac{1}{2},+\frac{1}{2}\bigg \rangle \bigg |\frac{1}{2},+\frac{1}{2}\bigg \rangle \end{aligned}$$
(2)
$$\begin{aligned} |1,0\rangle&= \frac{1}{\sqrt{2}}\left( \bigg |\frac{1}{2},+\frac{1}{2}\bigg \rangle \bigg |\frac{1}{2},-\frac{1}{2}\bigg \rangle + \bigg |\frac{1}{2},-\frac{1}{2}\bigg \rangle \bigg |\frac{1}{2},+\frac{1}{2}\bigg \rangle \right) \end{aligned}$$
(3)
$$\begin{aligned} |1,-1\rangle&= \bigg |\frac{1}{2},-\frac{1}{2}\bigg \rangle \bigg |\frac{1}{2},-\frac{1}{2}\bigg \rangle \end{aligned}$$
(4)

Depending on the spin state, different approaches are required. For instance, a loop over combinations is needed for the \(0\) spin state [as in Eq. (3)], while it is not required for the \(+1\) and \(-1\) states [as in Eqs. (2) and (4)]. Normalization factors, such as \(1/\sqrt{2}\) in Eq. (3), might also be necessary. Additionally, integers representing the double spin indices for each spinor are required to obtain symmetric combinations as in Eq. (3).

To facilitate amplitude calculations with spin-1 massive bosons, SPINAS provides several methods. The first method, get_num_spin_loops, determines the number of iterations required in order to include all the spinor combinations. The number of arguments is equal to the number of spin-1 particles in the process, with each argument being twice the spin component of the spin-1 particle (values of \(-2, 0\), or \(+2\)). For instance, if particle 3 is the sole massive spin-1 boson in the process, the number of required spin combinations can be obtained as follows:

figure ad

Here, ds3 represents double the spin component of particle 3 (\(-2,0\), or \(+2\)). For a process involving two massive spin-1 bosons, such as particles 2 and 4, the method is called with both their spin components:

figure ae

The return value of this method varies depending on the spin components. For example, it returns 1 for ds2=+2 and ds4=-2, 2 for ds2=0 and ds4=-2, and 4 for ds2=0 and ds4=0. As another example, if particles 1,3 and 4 were massive spin-1 bosons,

figure af

and so on. This method supports up to six massive spin-1 bosons. For processes involving more than six bosons, a method is provided taking an array of spins and a length, as in:

figure ag

In this example, particles 1, 3, 4, 6 and 8 are spin-1 bosons, with double spins ds1, ds3, ds4, ds6 and ds8.

The second method, get_spin_normalization, calculates the normalization factor for the spinor combination. It takes the same arguments as get_num_spin_loops and returns a value of type ldouble. For a process with two massive spin-1 bosons, the normalization factor is determined as follows:

figure ah

Using the same examples as before, this function would return 1 for ds2=+2 and ds4=-2, \(1/\sqrt{2}\) for ds2=0 and ds4=-2, and 1/2 for ds2=0 and ds4=0. Once again, this method works for up to six spin-1 bosons. If more than six external particles are spin 1, a general form is supplied taking the same form as for get_num_spin_loops. For example:

figure ai

In scenarios involving multiple iterations through spin loops, with massive spin-1 bosons, it is essential to determine the specific spins for each spinor. Consider a situation where particles 1, 3, and 4 are massive spin-1 bosons. We would require pairs of integer variables to represent the double spins of the spinors for each boson, as shown below:

figure aj

These variables, which can take values of either \(-1\) or \(+1\), will be assigned based on the current iteration of the spin loop. For instance, the assignment process for three such spin-1 particles can be implemented as follows:

figure ak

In this method, the arguments include the double spin component (values of \(-2, 0\), or \(+2\)) for each spin-1 boson and the corresponding pairs of double spins for the spinors. The iteration variable, i in this case, is also passed. This method assigns appropriate values to the double spins of the spinors (ds1a, ds1b, ds3a, ds3b, ds4a, and ds4b) based on the spin states of the bosons and the current iteration, without modifying the double spins of the bosons (ds1, ds3, ds4) or the iteration variable.

With these methods, all possible spin combinations are considered, and normalization factors are correctly applied. For example, the amplitude calculation within the loop might include expressions like:

figure al

Here, amp is initialized to zero before the loop and accumulates the amplitude contribution from each iteration. A detailed example involving one massive spin-1 boson can be found in Sect. 3.5, with additional examples for varying numbers of spin-1 bosons available in the SM directory, as discussed in Sect. 2.12.

Once again, this method is defined for up to six spin-1 bosons. If a greater number of spin-1 bosons are present, the general method should be used. For example:

figure am

Normally, we anticipate that these general methods for more than six spin-1 bosons would not be necessary for hand-written code. On the other hand, computer-generated code would likely use them.

The methodology outlined above is tailored specifically for massive spin-1 bosons and is not required for massless helicity-\({\pm }1\) bosons, such as photons or gluons. These particles lack a spin index, as each helicity state is directly represented by a product of two helicity spinors. Consequently, the spinor products for these particles do not possess a spin index. Detailed examples that showcase the implementation of amplitudes involving external photons and gluons are available in the SM directory of the SPINAS package, with further discussion in Sect. 2.12.

It’s important to note that, although the SPINAS package can be used for higher-spin particles, the current methods in the process class do not extend to massive particles with spins higher than 1. For these particles, the user will currently have to determine the number of loops, normalization factor and double-spin indices on their own. Expansion of these capabilities to encompass higher-spin particles is an area open for future development by the scientific community. Researchers interested in contributing to this aspect can find foundational information and guidance in Sect. 4.11 and the source code for this class, which lays out the groundwork necessary for such an extension.

2.11.2 Testing \(2\rightarrow 2\) Processes

The SPINAS package supports processes of any multiplicity. However, this initial version is focused on successfully achieving and testing \(2\rightarrow 2\) processes against Feynman rules. For this reason, we currently have a set of functions that support the comparison of constructive amplitudes against Feynman rules for \(2\rightarrow 2\) processes. We intend to add support for testing of higher-multiplicity amplitudes in future versions, including a more general testing framework, and we invite collaboration with the community as we work towards this goal. We are also open to the community adding other useful testing methods.

In the current version, we have four methods, namely: test_2to2_amp2, test_2to2_amp2_rotations, test_2to2_amp2_boosts, and test_2to2_amp2_boosts_and_rotations. All four functions perform a series of tests of the squared amplitude and return the number of tests where the discrepancy is greater than the allowed threshold. The tests are considered successful if 0 is returned. If any comparisons with Feynman diagrams are greater than the threshold, these functions also print a message with details about the comparison. The arguments for all four methods are exactly the same. We will first compare the methods and when to use them. Afterwards, we will describe the arguments of these methods.

The fundamental testing method is test_2to2_amp2, which evaluates the squared amplitude in the center of momentum (CM) frame across 20 evenly distributed phase-space points. Specifically, it considers scenarios where particle 1 travels along the z-direction, testing polar-angle points \(\cos \theta _3 \in \{-0.95, -0.85,..., 0.85, 0.95\}\) with a fixed azimuthal angle \(\phi _3 = 0\). Further details on the method’s arguments will be discussed later. Generally, this method suffices for assessing squared amplitudes, summed over spins. It has been observed that when this method yields successful results, the subsequent methods typically also succeed, and vice versa. However, there is an exception to this, which we will address later in this subsection. Users are advised to first achieve agreement with this fundamental test before proceeding to the additional methods.

Lorentz invariance is a fundamental property of the squared amplitude and, therefore, it must remain constant under both rotations and boosts, after summing over spins. To rigorously test this invariance in \(2 \rightarrow 2\) processes, SPINAS includes three additional methods beyond the base test_2to2_amp2.

The method test_2to2_amp2_rotations extends the base test by applying random rotations. Starting from the same equally spaced phase-space points, it generates a random spatial rotation for each point and applies it to the momenta of all four particles (\(p_1, p_2, p_3\), and \(p_4\)). The squared amplitude is then recalculated and compared against Feynman diagram results. This comparison is performed for 10 random rotations at each initial polar angle.

For testing boosts, the method test_2to2_amp2_boosts is employed. Similar to the rotation test, it begins with the phase-space points from the base test but introduces random Lorentz boosts to the CM-frame momenta. The method tests the invariance of the squared amplitude under these boosts, again comparing the results with those from Feynman diagrams for 10 random boosts per polar angle. Before moving on, it is crucial to note that in certain high-energy processes with significant diagram cancellations at high energies, such as \(W,\bar{W} \rightarrow W,\bar{W}\), numerical precision limits can cause slight differences in this and the following test, despite analytical verifications of the cancellations [15]. This issue should be considered when evaluating the test results that include random boosts.

The final method, test_2to2_amp2_boosts_and_rotations, combines both rotations and boosts. For each polar angle, it iterates through 10 sets of random rotations and boosts applied to the CM-frame momenta. The resulting squared amplitudes are then compared to the expected results from Feynman diagrams, testing the comprehensive Lorentz invariance of the amplitude.

It is important to emphasize that for these tests to yield meaningful results, the particle spins must be summed over in the squared amplitude calculations. Also, these tests serve dual purposes: they validate the user’s implementation of amplitudes using the SPINAS package and simultaneously verify the package’s underlying algorithms. The success of these tests underpins our confidence in the correctness and reliability of SPINAS, as further elaborated in Sect. 5.2.

The testing methods in SPINAS for \(2 \rightarrow 2\) processes, as previously mentioned, share the same set of arguments. The first argument is a lambda function, which references the user’s squared amplitude function. This approach allows flexibility in testing different forms of the squared amplitude. For instance, in processes involving massless helicity-\({\pm }1\) particles like photons or gluons, users might want to test the squared amplitude both after and before summing over the particle’s helicity. The lambda function enables specifying the desired form of the squared amplitude for each test.

Consider the process \( e,\gamma ^{{\pm }} \rightarrow e,h \) as an example. The squared amplitude’s Lorentz invariance holds irrespective of whether the photon’s helicity is summed over. Consequently, it is beneficial to test both scenarios - with and without helicity summation. This can be implemented as follows:

figure an

In this case, the user’s process class (e.g., eAehAmp) should have methods like amp2 and amp2_Aplus, with the former summing over helicities and the latter focusing on a specific helicity.

The subsequent arguments for these testing methods include the masses of the particles, the spatial momentum of the incoming particle in the CM frame, and the Feynman-diagram data for comparison. For example:

figure ao

The momenta for these tests are determined by the methods and given by:

$$\begin{aligned} p_1^\mu&= \left( E_1,0,0,p_{in}\right) \end{aligned}$$
(5)
$$\begin{aligned} p_2^\mu&= \left( E_2,0,0,-p_{in}\right) \end{aligned}$$
(6)
$$\begin{aligned} p_3^\mu&= \left( E_3,p_{out}\sin (\theta ),0,p_{out}\cos (\theta )\right) \end{aligned}$$
(7)
$$\begin{aligned} p_4^\mu&= \left( E_4,-p_{out}\sin (\theta ),0,-p_{out}\cos (\theta )\right) \end{aligned}$$
(8)

The energy \(E_i\) is determined as \(E_i = \sqrt{m_i^2 + |\vec {p}_i|^2}\). The incoming momentum is determined by the argument of test_2to2_amp2 that we called pspatial in the example above. That is, \(p_{in}\) is equal to pspatial. The outgoing momentum \(p_{out}\) is calculated using the formula:

$$\begin{aligned} p_{out} = \frac{1}{2\sqrt{s}}\sqrt{[s-(m_3+m_4)^2][s-(m_3-m_4)^2]}, \end{aligned}$$
(9)

where \(s = (p_1 + p_2)^2\). Each phase-space point is defined by \(p_{in}\) and the polar angle, with twenty polar angles chosen as \(\cos (\theta ) \in \{-0.95, -0.85,..., 0.85, 0.95\}\).

The Feynman-rule data arrays dataFD and dataFD_Aplus are 20-dimensional arrays of type ldouble, containing values corresponding to these angles. For this example, they look like:

figure ap

and should contain high-precision, in order for the tests to be successful. For this particlar process, it turns out that dataFD_Aplus has the same values as dataFD because the squared amplitude is the same for both helicities and is averaged over in the case where we sum over the helicities. In other cases, this may not be the case. For further examples, see the process files in the SM directory.

It is important to note that, in order to achieve agreement between the squared amplitudes from Feynman diagrams and constructive calculations, the width must treated exactly the same. This could be non-trivial since the width is treated in a program-specific way. For example, in CalcHEP [26], by default, the width is turned off in t- and u-channel diagrams and only kept near the resonance in s-channel diagrams. This is to reduce violations of gauge invariance. For our \(2\rightarrow 2\) process tests, we have found it best to set the widths to zero for all diagrams and to avoid the propagators going on shell when choosing phase-space points.

It is also important to ensure the amplitude calculations have other factors treated the same. For example, if testing the squared amplitude, whether the spins and colors are summed/averaged must be treated the same as also the symmetry factor. Furthermore, exactly the same coupling constants must be used. Any running of the couplings must be taken into account.

2.12 SM directory

The SM directory within SPINAS provides the implementation of a complete set of \(2 \rightarrow 2\) processes in the Standard Model (SM). This directory serves as a valuable resource for users encountering difficulties in implementing their processes, offering a range of similar examples for reference. Additionally, these examples can be utilized as templates for developing processes that extend beyond the Standard Model (BSM). In such cases, users are advised to create copies of these files in a separate directory for modification, to maintain the integrity of the original examples.

Many of the 4-point amplitudes are related to each other by crossing symmetry and are grouped into crossing-symmetry groups of amplitudes. From each of these groups, we have included at least two processes related by crossing symmetry. A list of all these processes can be found in Table 1.

Table 1 This is a list of all the SM processes implemented and found in the SM directory. Each line contains the implemented processes related by crossing symmetry

For each crossing-symmetry group, we have included notes in one of the process files, describing how its amplitude is obtained from the other in the group. We have also included notes in many of these processes describing how the \(2\rightarrow 2\) process with two outgoing particles is obtained from the all-ingoing process amplitude. These methods can also be found in [15, 16].

Our processes are grouped in the table with all four-fermion processes, followed by two-fermion and then all-boson processes. The fermions in our processes only include separate generations when doing so includes distinct diagrams not present with another generation. Thus, we have \(e,\bar{e}\rightarrow e,\bar{e}\) in addition to \(e,\bar{e}\rightarrow \mu ,\bar{\mu }\) because the first has diagrams that are not present in the second. However, we did not include \(e,\bar{e}\rightarrow \tau ,\bar{\tau }\) because it was exactly the same as the process \(e,\bar{e}\rightarrow \mu ,\bar{\mu }\), except for a change to a mass. Any \(2\rightarrow 2\) process in the SM, no matter what generation or crossing form, should be obtainable from these with a simple set of changes: a rearrangement of particles, a switch between ingoing and outgoing momenta, and a change in masses and possibly the addition of CKM elements if desired. This should serve as a solid foundation for users creating their own processes.

This set of processes is also a foundational part of our validation, as described in Sect. 5.2.

3 Complete example of SPINAS use

In this section, we give a complete, detailed walk through of implementing the Quantum Electrodynamics (QED) process \(e,\bar{e}\rightarrow \mu ,\bar{\mu }\), focusing on the photon contribution for simplicity. At the end, we will give some details about implementing an amplitude with massive spin-1 particles. This is designed to be a practical guide for users crafting their own process files. Tackling more complex process amplitudes can pose significant challenges, particularly for newcomers, with the accurate alignment of relative signs and factors being a common stumbling block. To assist users in this endeavor, we have compiled a comprehensive set of Standard Model (SM) processes and put them in the SM directory. These can serve as valuable references for further exploration. By understanding the concepts presented in this and the preceding section, and after successfully compiling and running the examples, users will be well-equipped to comprehend the code used in other SM processes. Our aim is to lay a robust foundation for the effective utilization of this package.

3.1 Setting up the files and directories

Before beginning, the user should create a directory for their code. In this example, we will call this directory user-dir/ and we will assume that all files, compilations and running of the binary are done within this directory. We recommend against adding the user’s file directly in one of the SPINAS directories to prevent potential conflicts and ensure easier updates and maintenance of the SPINAS code base. Inside this directory, the user would usually also create a subdirectory for their header file. We will call this directory user-dir/include/. For this example, we will create two files. The first is the header file, which we will call eemm.h, and we will put this in the user-dir/include/ directory. The second is the source code file for this process. We will call this eemm.cpp and put it in the user-dir/ directory.

3.2 The header file: eemm.h

The header file is where we declare the classes, their variables and methods and any other user-defined functions. We will call our class eemm and it can be declared with:

figure aq

: public spinas::process declares it to be a subclass of spinas::process, giving it some useful built-in methods, such as normalization routines when including massive spin-1 bosons in the incoming and/or outgoing states (see Sect. 2.11 for further details), and some built-in tests to compare your squared amplitude with the results from Feynman diagrams when the process is \(2\rightarrow 2\). We will include an example of using the built-in tests for this process later in this section. We note the use of spinas:: to indicate that process is inside the spinas namespace. We leave it to expert users to understand how and when to modify this. For this example, we will always include spinas:: when referring to SPINAS classes and methods.

The variables for this class should include all the coupling constants, masses, particle variables, propagators, sproducts and any other variables the user needs to implement their process. For example, for this process, we include:

figure ar

We recommend to declare all class variables to be private and emphasize the importance of encapsulation in object-oriented programming, which enhances the security and robustness of the code by preventing external access to internal states. We declare the following set of variables for this class. e is the electric charge of the positron, me is the mass of the electron and mm is the mass of the muon. Note that we always use the built-in type ldouble when declaring real variables to avoid type issues in the amplitude expressions. Next, we declare a variable for each of the external particles. We prefer to call these particles p1, p2, p3 and p4, where the p stands for particle and the integer represents which particle they are in the process. These are declared to be of type particle, the built-in class for particles in SPINAS, which contains all the methods required for calculating spinors and momenta for the particles.

Our next line contains prop declared as type propagator. In this case, we only have one propagator, but in other processes, we need to declare separate objects of this type for each internal particle. Note that if the same particle is present on multiple internal lines, only one propagator object needs to be declared for each particle, but can be reused for each internal line containing that particle. Although not required, we often find it convenient to declare a variable of type cdouble to store the complex value of the propagator for a particular phase-space point. If the same particle is present in multiple different internal lines, unique variables of this type will be necessary, one for each line. Just as we always use ldouble for real variables, we always use cdouble for complex variables to ensure no type issues when writing the amplitude expressions.

Finally, we declare all the spinor products we will need for this process in the line spinas::sproduct a13a, s13s,... s24s;. sproduct is the class that contains the particles in a product of spinors and momenta. We need a separate object of this type for each spinor product appearing in our amplitude. Although the naming can be anything the user likes, we find it convenient to add an a at the beginning and/or end of the object’s name if the spinors at those ends are angle spinors and an s if they are square spinors. The integers represent the external particles for the spinors on the left and right end and, if there are any momenta in between the spinors, the integers in the middle would represent those momenta. For example, for \(\langle \textbf{13}\rangle \), we create the object a13a in this example. If we required the spinor product \(\langle \textbf{1}|p_4|\textbf{3}]\), we would use the object name a143s, for convenience and clarity of notation.

We next move on to declare the methods, beginning with the constructor for this process. The arguments of this constructor should include the values of coupling constants and masses and anything else about this process that does not depend on the phase-space point. In this example, it is just the electric charge and the masses. We can do this with a declaration such as

figure as

Notice that, in the first line, we declare these methods to be public. This allows them to be used outside the object. Adding & to the end of ldouble is not required but allows the value to be passed more efficiently to the method. Using the keyword const is also not required, but should be used along with pass by reference to remove the possibility that this class change the values of the variables being passed. We will use this method of passing arguments throughout this example.

We sometimes want to calculate the same amplitude for different mass values. In order to do this, we can create the method

figure at

In order to calculate different phase-space points, we must have a method that updates the momenta of all the particles, propagators and spinor products. Therefore, we must declare a method along the lines of

figure au

In this example, the momenta of the external particles (the phase-space point) are passed as separate 4-dimensional arrays of type ldouble. The name of this method and the way the phase-space point is passed in is up to the user. However, if the user would like to use the built-in tests for \(2\rightarrow 2\) processes, this is the declaration that is expected. Moreover, the user is free to overload this method or define different methods of passing the phase-space point, as long as they are consistent and as long as they faithfully update all the particles, propagators and spinor products appropriately. This method, whatever its declaration, must be called every time the phase-space point is changed.

Typically, the user would declare a method to calculate and return the amplitude for a particular spin combination, for example

figure av

where ds1, ds2, ds3 and ds4 are double the spins of particles 1, 2, 3 and 4. The spins are doubled so that integers can be used, removing issues with the way fractions are stored as real variables. The name of this method is free for the user to choose but the return value should be of type cdouble, since the amplitude is complex.

In addition to this, we often want the squared amplitude, so can declare

figure aw

This method does not need arguments because it will iterate through the spins of the squared amplitude and return the sum. Since squaring removes the complex nature, this method returns type ldouble. As before, this naming scheme is convenient, but not required.

This is all that is required in the header, but the user can supplement this with class methods that are useful for their particular calculation.

Finally, rigorously testing your process is critical. We suggest creating a function outside the class declaration. This external test simulates how an external function would interact with your class, ensuring that your implementation behaves as expected when integrated into larger applications. So, after all the class methods are declared, and the closing curly brace of the class is closed, the user can declare a function such as

figure ax

The user can name this test function as they like. It will create objects of class eemm and run its methods. In particular, it is important to test the amplitude against known values. Both the arguments and the return value are up to the user. We will give an example using this declaration below.

3.3 The source file: eemm.cpp

Our next task is to create the source file, where we define all the constructors, methods and functions. The first few lines of the source file are the include statements:

figure ay

We have included iostream so that our test function, test_eemm, can output messages to the user. spinas.h is the header file that includes all the SPINAS declarations and eemm.h is the header file that we created in the previous subsection. Any other headers that the user requires can be included here.

Our next block of code defines the constructor for this class and looks like

figure az

After the initial line, which we recognize from the declaration in the header, we set the values of the class variables e, me and mm to the values given by the user when initializing this class. The class object prop is set equal to an object of type spinas::propagator with the arguments 0,0 since the internal photon has no mass and no width. The four particles in this class are set equal to objects of type spinas::particle, each with its mass. The spinor products a13a through s24s are set equal to objects of type spinas::sproduct. The first argument of the sproduct constructor is whether the spinor on the left end is of type ANGLE or SQUARE. We do not specify the type of the spinor on the right end because it is inferred from the left end and the number of particles in the spinor product. In this case, since all the spinor products have an even number of particles, the spinor at the right end is of the same type. Note that we do not need to specify whether the spinors are massless helicity spinors or massive spin spinors since this is inferred from the properties of the particle. The user is free to perform other calculations here as necessary.

Our next block of code is a method to change the masses of the particles and looks like

figure ba

Once again, we need to include eemm:: before the name to specify the class that this method belongs to. The first couple of lines set the masses to the arguments of the function. This is followed calling the set_mass method of each of the particles. If the internal particle were massive and its mass was being changed, we would also have to set the mass of the propagator. Since in this case, the internal particle is a photon, that is unnecessary here. Note that we do not need to do anything to the spinor products here because their mass is automatically inferred from the properties of these particles. Once again, this can be adjusted according to the needs of the user as long as the particles and the propagators are updated appropriately.

The next block of code updates the momenta for the process.

figure bb

We first see that the set_momentum method must be called for each particle. Following this, all the spinor products must be reset with the new momenta. To do this, we call the update method for each spinor product. This is important because the sproduct objects remember their values for different spins in order to speed up the calculation. Calling the update method resets all the values so that they must be recalculated for the new momenta. Following this, we update the propagator denominator. To do this, we calculate the momentum of the internal line. In this case, because it is an S-channel diagram, we find the sum of the momenta for particles 1 and 2. We then set pDenS equal to prop.denominator(propP). This uses the propagator object that we declared in the header prop and initialized in the constructor above with no mass and no width. We use its denominator method to calculate the propagator denominator with these values. This method must be called every time the phase-space point is changed. The user is free to change this method’s name and the arguments and method that the phase-space point is communicated to it. However, if the user would like to use the built-in testing functions for \(2\rightarrow 2\) processes, this is the form that is expected.

Our next method calculates the amplitude and returns it.

figure bc

In the first line, we have a comment with the amplitude in human-readable form. This is, of course, not required, but is convenient for checking the expression against the code. The next line initializes a constant of type ldouble. This is to avoid errors associated with algebra that includes different types and is recommended. After the electric charge squared is included, we have the sum of spinor products. Because all the spinors are massive and have two spins, we see that the v method for each sproduct is called with two arguments. These values are always twice the spin of the spin spinor, namely either +1 or -1. These are the only values allowed. (For a massive particle of spin 1, there will always be two spinors in each term for that particle and their spins will add to give the spin of the higher-spin particle. We will see an example of this in Sect. 3.5.) The method is called v for “value" and was purposely made as short as possible to enhance the readability of these expressions. We can see that twice the spins of particles, ds1 through ds4, appears once in each term and in the appropriate position in the appropriate spinor. This must be done carefully in order to achieve correct results. Further, although the overall sign does not matter, the relative signs between terms and any relative factors certainly do matter and must be accounted for correctly. Once we are done with the numerator, we divide by the propagator denominator. Before moving on, we note again that the set_momenta method must be called before this amp method is called any time the phase-space point changes.

We note that the sproduct class is designed to reduce the number of times a spinor product is calculated, thereby increasing the efficiency of the calculation. In particular, if the same sproduct is called again with the same spins, and the momenta have not been changed, it will simply return the previously calculated value. It will not calculate it again. For example, in the first term, we have a13a.v(ds1,ds3)*s24s.v(ds2,ds4). Suppose, after calling set_momenta with a new phase-space point, we first call amp with the values \(-1, -1, -1, -1\) for the double spins ds1, ds2, ds3 and ds4. On this pass, both a13a.v(ds1,ds3) and s24s.v(ds2,ds4) will be calculated and stored before returning their values. However, suppose that later the values \(-1, -1, -1, +1\) are called (without calling set_momenta again). In this case, a13a.v(ds1,ds3) will simply return the previously calculated value since it has seen these double spins before. s24s.v(ds2,ds4), on the other hand, will calculate the spinor product fresh since it hasn’t seen these double spin values yet. Each sproduct object will store the value for all four spin combinations (for two massive spinors) and reuse them until its update method is called (in the set_momenta method).

Although some users may only need the amplitude, very often, we would like to square the amplitude and sum (average) it over the spins. For this, we create

figure bd

We begin by creating a constant variable four, a variable amp2 of type ldouble to store the accumulated squared amplitude and a variable M of type cdouble to store the amplitude for a particular spin combination. We next set up loops for each double spin which, in this case, switches between -1 and +1. For each double-spin combination, the amp method that we just defined is called and stored in M. In the following line, its absolute value is taken and squared and then added to amp2. Once the loop is complete, we return it divided by 4 since we are averaging over the initial spins. As usual, the name and other properties can be chosen by the user, but if they want to use the built-in testing features, this is the form expected. In the case that different diagrams have different QCD color combinations, they will have to be calculated separately, squared separately and added with their appropriate color factors. Interested users should consult one of the QCD examples in the SM directory.

This is all that is required to calculate both the amplitude and the squared amplitude. However, it is essential that the code be tested. For this, we add one more function that runs over multiple phase-space points and compares the result with known values. For example, for this process we could create

figure be

We first note that this time we did not use eemm:: at the beginning of this function name. That is because this is not a method of the eemm class. It is a function outside of this class, intended to call this class in the same way as an external function in a larger calculation. In the next line, we create a variable of type int to store the number of times our tests fail. This is not necessary, but we find it helpful. We next print a message to standard out, telling the user that we are testing the process e, E -> m, M (QED). This is not required and it can be modified as convenient. This line is the reason we included the iostream header at the beginning of this file. After creating a new block of code with the curly braces { and } for convenience, we create a new integer called i to keep track of the number of failed tests for this mass combination. We then create variables for the charge and masses and use them to initialize a new object of type eemm that we call eemmAmp. We note that we cannot use the same name for the object as for the class. We also note that these variables e, me and mmu are not the same as the variables inside our object eemmAmp, so changing these variables will not automatically change the charge and masses inside eemmAmp. For that, we would need to call eemmAmp.set_masses(me,mmu); with the new values. We next define the variable pspatial to store the spatial part of the incoming momentum, where we are assuming we are in the center of mass frame. Following this we create an array of size 20 and type ldouble to store the expected values for the squared amplitude at the angles given by \(\cos (\theta )=0.95, 0.85, \cdots -0.85, -0.95\). There should be 20 values, equally spaced, altogether if using this built-in test. The test is run on the next line with the built-in method test_2to2_amp2. The first argument of this method is the function for the amplitude squared. We chose to use a lambda expression to allow different squared amplitude methods to be called. An example of when we find this useful is when we have photons or gluons in the initial states and want to test the squared amplitude with specific helicity combinations as well as the fully summed squared amplitude. For examples, see the photon and gluon processes in the SM directory. Finally, we include the masses of each of the four particles, the spatial part of the incoming momentum and the already-known data for comparison. On the next line we have ... representing other tests. We emphasize that the user does not have to use the built-in tests and can write any test code here that validates their amplitude code.

Once the source file for the process is complete, we need a main function to create a binary to run the tests. We could put it in the same file, but typically this process will be just one of multiple for a given calculation. So, we will create a file called test.cpp in the user-dir/ and add the following lines to it:

figure bf

After including the header for SPINAS and for this process, we create a main function. In our case, the only purpose of the main function is to run our tests, but the user could add other code here as desired.

3.4 Compiling and running

Once the files are created, we compile the code with a command (on Linux) such as

figure bg

all on one line. An explanation of the parts of this command can be found in Sect. 2.5. We are assuming the user-dir/ is inside the main SPINAS directory and that this command is being run from inside user-dir/. If the user’s directory is located somewhere else, -I../include and -L../build will have to be modified to represent the paths to the SPINAS include and build directories.

Once the user’s code is compiled, it can be run with a command (on Linux)

figure bh

If the user created the files with exactly the same contents as described here, the output will be * e, E -> m, M (QED). If there were any discrepancies, the output should have a list of the differences between the user’s code and the Feynman-diagram results. If the user has success here, we recommend they modify the amplitude in some way and see what happens with this test to get a better feel for what they would see if the amplitude expression were not correct. For example, try switching a relative sign between terms, or try changing the overall factor. Of course, the user can add further tests and more output as they choose. This just gives a starting point.

3.5 Some notes on massive spin-1 amplitudes

Each process will come with its own challenges and we have included a complete set of processes from the SM in the SM directory. Before leaving this section, however, we would like to outline the unique code when dealing with spin-1 particles. Our spinors are all spin-\(\frac{1}{2}\) and only have two possible values, one for spin \(+\frac{1}{2}\) and one for spin \(-\frac{1}{2}\). Therefore, the spin-1 object is obtained by a symmetric combination of these spin-\(\frac{1}{2}\) objects, in the usual product-representation way. See Sect. 2.11.1 for further details. In this subsection, we will demonstrate using the functions involved with these spins in a couple of processes.

Our first examle involves the process \(A^{+},Z\rightarrow e,\bar{e}\). The amplitude for this process is given by

$$\begin{aligned} \mathcal {M}&= pre\frac{[1|p_3p_4|1](g_{Re} [24]\langle 23\rangle + g_{Le} \langle 24\rangle [23]}{(t-m_e^2) (u-m_e^2)}\nonumber \\&\quad + pre[12]\left( \frac{g_{Re} [14]\langle 23\rangle }{(u-m_e^2)} - \frac{g_{Le} [13]\langle 24\rangle }{(t-m_e^2)} \right) , \end{aligned}$$
(10)

where pre is the constant prefactor. We see that, in this example, the square bracket \(|1]\) appears twice in every term. This is the photon and is a massless helicity spinor and already gives helicity \(+1\) for the photon. On the other hand, we see that every term has two massive spin spinors for the Z boson, namely a \(|\textbf{2}\rangle \) and a \(|\textbf{2}]\). These massive spin spinors each have two spin choices. If the spin of the Z boson is \({\pm }1\), then there is only one combination. However, if the spin ofthe Z boson is 0, then there are two combinations that must be added, and this must be followed by a normalization factor.

To make this more straight forward, we have created the functions get_num_spin_loops, get_spin_normalization and get_spinor_spins. Here is an example for this process.

figure bi

After creating a complex variable, called amplitude to store the amplitude, we introduce two new integers, ds2a and ds2b. These store double the spin of each massive spin spinor that make up the spin-1 Z boson. Each of them will take either the value \(+1\) or \(-1\). We next create an integer called nCombs, which stores the number of different spin combinations that must be added to achieve the double spin given by ds2, which is double the Z-boson spin. It’s argument is the double spin of the Z boson. For example, if the double spin ds2 is either \(+2\) or \(-2\), then, nCombs will be equal to 1. If, on the other hand, the double spin ds2 is 0, then nCombs will be 2. On the next line, we create a variable of type ldouble which contains the normalization for the spin. It’s argument is, once again, the double spin of the Z boson. It gives 1 when ds2 is \({\pm }2\) and \(1/\sqrt{2}\) when ds2 is 0.

At this point, we are ready to iterate through the different spin combinations, so we create a for loop that adds the contribution from each spin combination. Before we actually calculate the contribution to the amplitude for this spin combination, we must determine what the double spins ds2a and ds2b are. For this, we have the line get_spinor_spins, which takes as arguments, the double spin of the Z boson ds2, the spinor double spins ds2a and ds2b and the current iteration i. The values of ds2a and ds2b are updated according to the value of ds2 and i. With this, we are ready to calculate this contribution to the amplitude. After entering the first loop if the photon is helicity \(+1\), we add the expression to amplitude. We first multiply the entire term by normFactor described above, the prefactor pre (containing the coupling constants and anything that could be factored out) and the sproduct s1341s.v(), which represents the value of \([1|p_3p_4|1]\). Since the photon is massless, there are no spin choices and, therefore, no arguments to this value. Next, in parentheses, we add two pieces for the left- and right-chiral pieces of the amplitude contribution. For the right chirality, we have s24s.v(ds2a,ds4)*a23a.v(ds2b,ds3). We see that the first of these spinor products uses the argument sp2a and the second uses the argument sp2b. The order does not matter since it is symmetrized, as long as every term has each of them once. The ... represent other terms that are not shown, but can be seen in the file AZee.cpp in the SM directory. Finally, at the end, we return the amplitude.

Using these functions may seem overkill since the previous example was so simple. However, these functions work on amplitudes with greater number of external massive spin-1 particles, where it becomes increasingly more complicated. Let us give one more example. Consider the process \(A^+,Z\rightarrow W,\bar{W}\). This process contains three massive spin-1 bosons and each spin combination for the ZW and \(\bar{W}\) will have a different number of sub-spins, different normalization and different combination of sub-spins for each iteration. This can become quite complicated. For this reason, we have created our functions to work for any number of spin-1 particles, with the caveat that if the process contains more than six massive spin spinors, the user should use the more general form of the functions described in Sect. 2.11.1. Here is the outline of the code that can be seen in SM/AZWW.cpp,

figure bj

We focus here on the parts that contain the spins of the massive spin-1 particles. Shortly after opening the function, we declare six new integers ds3a, ds3b, ds4a, ds4b, ds2a and ds2b, two for each particle. After this, we set the number of combinations nCombs by use of get_num_spin_loops, however, this time it uses all three double spins to determine the number of loops that are appropriate. The normalization factor normFactor is obtained from get_spin_normalization, again with all three double spins as arguments. For both of these functions, the order of the double spins does not matter. Once we begin iterating through the spin combinations, we set the sub-spins by use of the function get_spinor_spins. This time, it contains the spins and sub-spins of each particle in turn. The order of the particles in this function does not matter, but for each particle, the double spin of the particle must be first followed by its sub-spins. Finally, when we add the contribution to the amplitude, each sub-spin is present once per term. The order doesn’t matter here.

3.6 Complete set of SM processes

While this section presents one complete example of a process implementation, the SPINAS codebase offers a comprehensive collection of \(2\rightarrow 2\) SM processes. These can be found in the SM directory. A full listing of these processes is provided in Sect. 2.12, serving as a practical resource for users. By examining a process similar to their research interest, users can gain valuable insights into applying SPINAS in their own work.

4 Design and implementation

This section delves into the design and implementation aspects of the SPINAS package, primarily aimed at individuals interested in contributing to its development. While the typical user may not require this level of detail, these insights are valuable for those looking to understand the inner workings of SPINAS and contribute effectively. Contributions from the community are highly encouraged and are integral to the evolution of this software.

4.1 License

SPINAS is distributed under the GNU General Public License (GPL) Version 3, aligning with our commitment to openness and collaborative development within the scientific community. This license ensures that all enhancements and modifications to the software remain freely accessible. It grants users the freedom to run, study, share, and modify SPINAS. The full GPL V3 license text is available in the software repository and detailed at [25], providing comprehensive information about the associated rights and responsibilities.

4.2 SPINAS repository and contributing

The SPINAS project is hosted on GitHub, offering an accessible platform for downloading the software, contributing to its development, and engaging with the user community. The repository, found at:

figure bk

which serves as a hub for the latest stable release, issue tracking, feature requests, and viewing the development history.

We actively encourage community contributions to SPINAS, encompassing various forms of participation:

  • Community Support: Contributing through discussions, answering queries, and sharing experiences with SPINAS.

  • Documentation: Enhancing and expanding documentation to facilitate user understanding and engagement.

  • Issue Reporting: Utilizing GitHub’s issue tracker for bug reports and feature suggestions, aiding in continuous improvement.

  • Code Contributions: Developing new features, optimizing code, and resolving bugs.

SPINAS’s future development relies on community involvement, with a vision for collaborative growth that extends beyond our team to include valuable user contributions. This community-driven model is crucial for maintaining SPINAS as a state-of-the-art tool in particle physics research.

4.3 Build system

The SPINAS package currently employs CMake as its build system, selected for its wide acceptance, cross-platform support, and adaptability in handling complex build scenarios. CMake streamlines the compilation process, ensuring a user-friendly and flexible platform suitable for various computational requirements. While CMake meets the current needs effectively, we remain open to the possibility of adopting a different build system in the future. Any such transition would be guided by the goal of further enhancing the user experience, particularly in simplifying and optimizing the build process across different platforms.

A central aspect of the SPINAS compilation strategy is the strategic use of compiler flags. These flags play a crucial role in tailoring the software to specific needs. In the current iteration of SPINAS, compiler flags are primarily utilized to determine the precision of calculations, as described in Sect. 4.6.

4.4 Directories

The SPINAS package is organized into several directories, each serving a distinct purpose in the framework’s structure and functionality. This organization facilitates ease of navigation and maintenance of the codebase.

  • Source Directory: Located in source, this directory contains all the source files of SPINAS. These files comprise the core functionalities and algorithms that drive the package.

  • Include Directory: The include directory is dedicated to all the core header files of SPINAS.

  • Tests Directory: All unit tests for SPINAS components, leveraging the Boost testing framework, are housed in the tests directory.

  • SM Processes Directory: The SM directory contains all the \(2 \rightarrow 2\) processes within the Standard Model (SM). As we expand to include higher-multiplicity SM processes, we anticipate subdividing this directory into more specialized categories.

  • User Directory: The user-dir directory serves as a template for users looking to develop their own code using SPINAS. It includes the complete example, as detailed in Sect. 3, providing a practical guide and starting point for user-implemented projects.

This directory structure is designed not only for the current state of SPINAS but also with an eye towards its future development.

4.5 The SPINAS header file

In order that the user does not need to specify each individual header file, we have created the spinas.h header file which includes all the others.

4.6 Data types

To facilitate consistent precision management across the package and in user-implemented code, we have introduced a types.h header file. This file defines the central floating-precision and complex types, controlled by a compiler flag, which currently sets the precision to either long double or double. Within this file, types such as ldouble and cdouble are defined to reflect the desired precision. By employing these types throughout the SPINAS codebase and encouraging users to do the same, precision modifications can be implemented uniformly, simply by adjusting the relevant compiler flag.

In an early, non-public version of SPINAS, support for arbitrary precision was explored. Developers interested in this feature or looking to contribute towards its integration into future versions are encouraged to contact the author. Such collaboration aligns with our ongoing commitment to enhancing SPINAS’s capabilities in response to evolving user needs and scientific advancements.

4.7 Complex vectors and complex matrices

In the realm of constructive theory, 2-dimensional complex vectors and 2x2 complex matrices are indispensable components. To this end, the SPINAS package incorporates these elements through the implementation of two classes: cvector and cmatrix. It’s important to note that these classes primarily serve as internal tools within SPINAS, facilitating higher-level operations rather than being intended for direct use by end-users. Their design is streamlined, encompassing only those properties and methods that are essential for the specific demands of constructive calculations.

In addition to directly specifying the components of these objects, cmatrix can also be constructed with a momentum and a choice of whether the Lorentz indices are upper or lower. Additionally, these classes have some standard methods, such as get_conjugate for cvector and get_det for cmatrix to obtain the determinant, corresponding with \(p^2\) for the momentum.

A key feature of these classes is that they overload standard algebraic operators such as +, -, *, and /. This overloading simplifies the implementation of mathematical operations, making the code more intuitive and aligned with conventional mathematical expressions. Such design choices in cvector and cmatrix contribute to the efficiency and readability of computations within the SPINAS package.

4.8 Particles

A central role in SPINAS is played by the particle class, which implements many properties of the particles related to the spinor algebra and is one of the classes directly used by the end user.

The construction of a particle object requires the particle’s mass as the sole argument. Users have the flexibility to modify the mass post-construction using set_mass and can retrieve the current mass value with get_mass. Setting the particle’s momentum is accomplished via set_momentum (details in Sect. 2.8), and the momentum can be accessed using get_momentum. It’s important to note that the momentum should be set after specifying the mass, as the set_momentum method updates numerous other internal variables. This sequence aligns with the typical order followed in amplitude calculations for phase-space points. Additionally, the dot method provides the inner product of the momenta of this particle and another. The other methods, described below, are primarily intended for internal use.

One of the main design principles of SPINAS was to maximally store calculations so that if they were needed multiple times within an amplitude calculation, they would not be recalulated, but rather simply returned. Consequently, the particle class maintains private variables that store a particle’s properties and representations. These include the momentum’s magnitude and its polar and azimuthal angles, recalculated whenever the momentum is altered. The other particle representations are only calculated when they are first needed. This means that aspects of the particle that are not needed in a calculation are never calculated. Each property is calculated when it is first needed and then stored for later reuse. They are not reset and recalculated until after the momentum is changed.

The other private variables include the 2x2 complex matrix of upper and lower Lorentz indices in cmatrix objects and are obtained with the methods umat and lmat, respectively. The spinor forms of the particles are stored in 2-dimensional complex vectors of type cvector. If the particle is massless, the left- and right-angle helicity spinors are obtained with the methods langle and rangle, respectively, while the left- and right-square helicity spinors are obtained with the methods lsquare and rsquare, all with no arguments. If the particle is massive and the spinor has an upper spin index, which is the default, the left- and right-angle spinors can be obtained with the methods langle and rangle, respectively, while the left- and right-square helicity spinors can be obtained with the methods lsquare and rsquare, this time with one argument, which is double the desired spin (either \({\pm }1\)). Finally, if the particle is massive and the spinor has a lower spin index, the same methods can be called, this time with two arguments. The first argument is double the spin (either \({\pm }1\)) and the second argument is LOWER. Strictly speaking, the case with upper indices could be obtained in the same way, where the second argument is UPPER. The values LOWER and UPPER are set to boolean values in the header files in order to simplify and reduce mistake in implementation.

4.9 Spinor products

At the heart of amplitude calculations in SPINAS lies the sproduct class, which handles the product of two spinors with an intervening set of momenta. The constructor of this class, detailed in Sect. 2.10, allows users to specify the nature of the left spinor as either an angle spinor (ANGLE) or a square spinor (SQUARE), with the right spinor determined by the class. Additionally, users can determine the position of the spin indices as either upper (UPPER) or lower (LOWER), as elaborated in the preceding subsection. As discussed in the previous subsection, we have created these compile-time constants in order to simplify the use of these classes.

A key design philosophy of SPINAS is the efficient reuse of calculations, a principle that is extensively applied in the sproduct class. This class is constructed with references to the participating particles, leveraging the fact that spinors and momentum matrices are properties of these particles. By extracting spinor and momentum matrix information directly from the particle class, sproduct avoids redundant calculations. For instance, if the same particle spinor (e.g., \(\langle \textbf{1}|\)) is used in multiple spinor products such as \(\langle \textbf{1}|p_2|\textbf{3}]\) and \(\langle \textbf{14}\rangle \), it is computed only once and then reused in both instances. This approach extends to momentum matrices as well, ensuring that any matrix, such as that for \(p_2\), is computed just once irrespective of its frequency of use in different spinor products. As a result, the more complex the amplitude, the greater the efficiency gains from this implementation.

Additionally, sproduct stores the computed value of each spinor product and reuses these values in subsequent calculations. For example, if \(\langle \textbf{12}\rangle \) occurs multiple times within the same or different diagrams, it is calculated only once, with the result being reused in each future use. This efficiency underscores the importance of the update method. When a new phase-space point is set, invoking update resets the sproduct object, clearing its memory of previous calculations and ensuring accuracy in the new context.

4.10 Propagators

The propagator class is designed to calculate the propagator denominator. It stores the mass and width of a particle but it does not store the momentum. Our reason for this was so that the same propagator object could be used for multiple lines, containing the same particle. This is partly because the momenta passing through the propagators are combinations of the external momenta and need to be calculated for each phase-space point. In order to simplify the user’s code, we encourage the user to create a variable of cdouble type for each propagator denominator and set its value after updating the phase-space point.

We acknowledge that this design choice is subject to the evolving needs of the SPINAS user community. Therefore, we are open to feedback and suggestions on the future development of this class, aiming to align it closely with user requirements and advancements in the field.

4.11 Processes

The process class was designed to store other methods useful to general processes. It currently contains methods for testing \(2\rightarrow 2\) processes as well as methods to determine spin properties for massive spin-1 particles. In the future, support for testing higher-multiplicity amplitudes and for higher-spin particles is likely to be added. Furthermore, the community may decide to add other properties and methods.

4.12 Other functions

Within the utilities file of the SPINAS package, we have incorporated several auxiliary functions that extend beyond the scope of specific classes. These functions were created to facilitate validation and testing, but are available for other uses.

One such function is rotate_momentum, which performs the rotation of a 4-dimensional momentum. It requires three arguments: the 4-dimensional momentum array to be rotated, a 3-dimensional normalized array defining the rotation axis, and the rotation angle. While we currently do not offer a function for rotation based on Euler angles, we encourage the community to contribute such a feature if deemed beneficial.

Another important function is boost_momentum, which applies a Lorentz boost to a 4-momentum. This function takes two arguments: the 4-dimensional momentum array undergoing the boost and a 3-dimensional array representing the boost’s velocity. Similar to rotation, we have not implemented a function for boosting based on rapidity, but we welcome community contributions in this area.

Additionally, we have included functions designed to randomly generate 4-momenta. The first, choose_random_momentum, randomly generates a 4-momentum for a particle with mass, ensuring that the energy component satisfies the condition \(E>|\vec {p}|\). This function also randomly determines the mass of the particle. The second function, choose_random_massless_momentum, generates a massless 4-momentum. Both functions share the same set of arguments: a 4-dimensional array to store the generated 4-momentum and the minimum and maximum values for the momentum vector components.

5 Testing and validation

Testing and validation are crucial components in the development of any software, particularly for a complex system like SPINAS that deals with particle physics simulations. In SPINAS, two primary testing methodologies are employed: Boost Unit Tests and a comprehensive set of Standard Model 2\(\rightarrow \)2 process tests. These tests not only ensure the correctness of the implementation but also validate the results against established benchmarks. If a bug is discovered, we will add a test for the bug before fixing it.

5.1 Boost unit tests

Boost Unit Tests provide a robust framework for performing unit testing in C++ applications. In SPINAS, these tests are used to validate the functionality of individual components and modules. In this subsection, we will describe the tests we have performed for the components of the SPINAS package. For many of these tests, we have repeated the test one hundred times, each time with new randomly generated points. When generating random values, we generally generate them between \(-50\) and 50 if either sign is allowed or between 0 and 50 if non-negative. Occasionally, a random point is chosen that pushes the precision too far and the test reports test failures. However, this is usually a case of loss of precision in the tail of randomly generated tests. Further evidence of this is the testing of the SM processes described in Sect. 5.2. If this occurs, we encourage the user to rerun the Boost tests. If the error does not occur again and the SM tests pass, the issue is likely not a sign of a problem with the package, but rather a sign of the limitations of the precision.

5.1.1 cvector

We have tested complex vectors in the cvector class in the tests/cvector.cpp file. We test the following things:

  • the constructor;

  • the conjugation method; and

  • multiplication of two cvector objects as well as a cvector object and a cmatrix object in either order.

5.1.2 cmatrix

We have tested complex matrices in the cmatrix class in the tests/cmatrix.cpp file. We test:

  • the constructor;

  • the determinant;

  • addition of two complex matrices;

  • subtraction of two complex matrices;

  • multiplication of two complex matrices (matrix multiplication).

5.1.3 propagator

We have tested the propagator class in tests/propagator.cpp file. We test:

  • the constructor, and

  • the denominator.

5.1.4 particle

We have tested the particle class in tests/particle.cpp file. We test:

  • the constructor;

  • the set_momentum method;

  • momentum matrices with upper and lower Lorentz indices;

Furthermore, we have checked various identities for the spinors. We describe these identities in App. A. We begin with massless helicity spinors. For these, we test:

  • \(|i\rangle =[i|^*\),

  • \(\langle i|=|i]^*\),

  • \(p_i|i\rangle =0\),

  • \([i|p_i=0\),

  • \(\langle ii\rangle =0\),

  • \([ii]=0\),

  • \(|i\rangle [i|=p_i\),

  • \(|i]\langle i|=p_i\),

  • \(p_i|i]=0\),

  • \(\langle i|p_i =0\),

and

  • \(\mathcal {J}^{(3)}|i\rangle = -\frac{1}{2}|i\rangle \),

  • \(\mathcal {J}^{({\pm })}|i\rangle = 0\),

  • \(\mathcal {J}^{(3)}\langle i|= -\frac{1}{2}\langle i|\),

  • \(\mathcal {J}^{({\pm })}\langle i|= 0\),

  • \(\mathcal {J}^{(3)}[i|= +\frac{1}{2}[i|\),

  • \(\mathcal {J}^{({\pm })}[i|= 0\),

  • \(\mathcal {J}^{(3)}|i]= +\frac{1}{2}|i]\), and

  • \(\mathcal {J}^{({\pm })}|i]= 0\),

where the \(\mathcal {J}^{({\pm })}\) identities include two identities that were each tested.

For massive spinors, we check the related identities, taking into account the spin indices:

  • \(|\textbf{i}\rangle ^{\textrm{I}}=\left( [\textbf{i}|_{\textrm{I}}\right) ^*\),

  • \(\langle \textbf{i}|^{\textrm{I}}=\left( |\textbf{i}]_{\textrm{I}}\right) ^*\),

  • \(|\textbf{i}\rangle _{\textrm{I}}=-\left( [\textbf{i}|^{\textrm{I}}\right) ^*\),

  • \(\langle \textbf{i}|_{\textrm{I}}=-\left( |\textbf{i}]^{\textrm{I}}\right) ^*\),

  • \(\langle \textbf{ii}\rangle ^{\textrm{I J}}=-m_i\epsilon ^{\textrm{IJ}}\),

  • \(\langle \textbf{ii}\rangle ^{\textrm{I}}_{\textrm{J}}=-m_i\delta ^{\textrm{I}}_{\textrm{J}}\),

  • \(\langle \textbf{ii}\rangle _{\textrm{I}}^{\textrm{J}}=m_i\delta _{\textrm{I}}^{\textrm{J}}\),

  • \(\langle \textbf{ii}\rangle _{\textrm{I J}}=m_i\epsilon _{\textrm{I J}}\),

  • \([\textbf{ii}]_{\textrm{I J}}=-m_i\epsilon _{\textrm{I J}}\),

  • \([\textbf{ii}]_{\textrm{I}}^{\textrm{J}}=-m_i\delta _{\textrm{I}}^{\textrm{J}}\),

  • \([\textbf{ii}]^{\textrm{I}}_{\textrm{J}}=m_i\delta ^{\textrm{I}}_{\textrm{J}}\),

  • \([\textbf{ii}]^{\textrm{I J}}=m_i\epsilon ^{\textrm{I J}}\),

  • \(|\textbf{i}\rangle ^{\textrm{I}}[\textbf{i}|_{\textrm{I}} = p_i\),

  • \(|\textbf{i}\rangle _{\textrm{I}}[\textbf{i}|^{\textrm{I}}=-p_i\),

  • \(|\textbf{i}]_{\textrm{I}}\langle \textbf{i}|^{\textrm{I}}=p_i\),

  • \(|\textbf{i}]^{\textrm{I}}\langle \textbf{i}|_{\textrm{I}}=-p_i\),

  • \(p_i|\textbf{i}\rangle ^{\textrm{I}}=-m_i|\textbf{i}]^{\textrm{I}}\),

  • \(\left( \langle \textbf{i}|^{\textrm{I}}\right) p_i=m_i[\textbf{i}|^{\textrm{I}}\),

  • \(p_i|\textbf{i}\rangle _{\textrm{I}}=-m_i|\textbf{i}]_{\textrm{I}}\),

  • \(\left( \langle \textbf{i}_{\textrm{I}}\right) p_i=m_i[\textbf{i}|_{\textrm{I}}\),

  • \(p_i|\textbf{i}]^{\textrm{I}}=-m_i|\textbf{i}\rangle ^{\textrm{I}}\),

  • \(\left( [\textbf{i}|^{\textrm{I}}\right) p_i=m_i\langle \textbf{i}|^{\textrm{I}}\),

  • \(p_i|\textbf{i}]_{\textrm{I}}=-m_i|\textbf{i}\rangle _{\textrm{I}}\),

  • \(\left( [\textbf{i}]_{\textrm{I}}\right) p_i=m_i\langle \textbf{i}|_{\textrm{I}}\),

and

  • \(\mathcal {J}|\textbf{i}\rangle ^{\textrm{I}} = |\textbf{i}\rangle ^{\textrm{J}} J_{\textrm{J}}^{\ \textrm{I}}\),

  • \(\mathcal {J}\langle \textbf{i}|^{\textrm{I}} = \langle \textbf{i}|^{\textrm{J}} J_{\textrm{J}}^{\ \textrm{I}}\),

  • \(\mathcal {J}|\textbf{i}\rangle _{\textrm{I}} = |\textbf{i}\rangle _{\textrm{J}} J_{\ \textrm{I}}^{\textrm{J}}\),

  • \(\mathcal {J}\langle \textbf{i}|_{\textrm{I}} = \langle \textbf{i}|_{\textrm{J}} J_{\ \textrm{I}}^{\textrm{J}}\),

  • \(\mathcal {J}|\textbf{i}]^{\textrm{I}} = |\textbf{i}]^{\textrm{J}} J_{\textrm{J}}^{\ \textrm{I}}\),

  • \(\mathcal {J}[\textbf{i}|^{\textrm{I}} = [\textbf{i}|^{\textrm{J}} J_{\textrm{J}}^{\ \textrm{I}}\),

  • \(\mathcal {J}|\textbf{i}]_{\textrm{I}} = |\textbf{i}]_{\textrm{J}} J_{\ \textrm{I}}^{\textrm{J}}\), and

  • \(\mathcal {J}[\textbf{i}|_{\textrm{I}} = [\textbf{i}|_{\textrm{J}} J_{\ \textrm{I}}^{\textrm{J}}\),

where each of these rows corresponds with three identities which were each tested. They were with \(\mathcal {J}^{(3)}\) and \(j^{(3)}\) or \(\mathcal {J}^{(+)}\) and \(j^{(+)}\) or \(\mathcal {J}^{(-)}\) and \(j^{(-)}\).

5.1.5 Sproduct

We have tested a significant number of spinor product identities in the sproduct class in tests/sproduct.cpp file. We have tested these identities for a variety of randomly generated masses and momenta, including both massive cases and massless cases (when appropriate).

In our first series of tests, we have considered spinor-product identities where all the spin indices on the left are contracted so that the right side of the equality does not have any spinor indices. For these, not only do we randomly choose the momenta and masses for each of the tests, but we also test after random rotations and boosts.

For our first batch of tests, we have taken the product of two spinor products with no intermediate momenta. We have tested:

  • \([\textrm{i j}]\langle \textrm{j i}\rangle =2p_i \! \cdot \! p_j\) in the following ways:

    • \([\textrm{i j}]\langle \textrm{j i}\rangle =2p_i \! \cdot \! p_j\) with \(m_i=0\) and \(m_j=0\);

    • \([\textrm{i} \textbf{j}^{\textrm{J}}]\langle \textbf{j}_{\textrm{J}} \textrm{i}\rangle =2p_i \! \cdot \! p_j\) with \(m_i=0\) and \(m_j\ne 0\);

    • \([\textbf{i}^{\textrm{I}} \textrm{j}]\langle \textrm{j} \textbf{i}_{\textrm{I}}\rangle =2p_i \! \cdot \! p_j\) with \(m_i\ne 0\) and \(m_j=0\); and

    • \([\textbf{i}^{\textrm{I}} \textbf{j}^{\textrm{J}}]\langle \textbf{j}_{\textrm{J}} \textbf{i}_{\textrm{I}}\rangle =2p_i \! \cdot \! p_j\) with \(m_i\ne 0\) and \(m_j\ne 0\);

  • \([\textrm{i j}][\textrm{j i}]=-2m_im_j\) in the following ways:

    • \([\textrm{i j}][\textrm{j i}]=-2m_im_j\) with \(m_i=0\) and \(m_j=0\);

    • \([\textrm{i} \textbf{j}^{\textrm{J}}][\textbf{j}_{\textrm{J}} \textrm{i}]=-2m_i m_j\) with \(m_i=0\) and \(m_j\ne 0\);

    • \([\textbf{i}^{\textrm{I}} \textrm{j}][\textrm{j} \textbf{i}_{\textrm{I}}]=-2m_im_j\) with \(m_i\ne 0\) and \(m_j=0\); and

    • \([\textbf{i}^{\textrm{I}} \textbf{j}^{\textrm{J}}][\textbf{j}_{\textrm{J}} \textbf{i}_{\textrm{I}}]=-2m_i m_j\) with \(m_i\ne 0\) and \(m_j\ne 0\);

  • \(\langle \textrm{i j}\rangle \langle \textrm{j i}\rangle =-2m_im_j\) in the following ways:

    • \(\langle \textrm{i j}\rangle \langle \textrm{j i}\rangle =-2m_im_j\) with \(m_i=0\) and \(m_j=0\);

    • \(\langle \textrm{i} \textbf{j}^{\textrm{J}}\rangle \langle \textbf{j}_{\textrm{J}} \textrm{i}\rangle =-2m_im_j\) with \(m_i=0\) and \(m_j\ne 0\);

    • \(\langle \textbf{i}^{\textrm{I}} \textrm{j}\rangle \langle \textrm{j} \textbf{i}_{\textrm{I}}\rangle =-2m_im_j\) with \(m_i\ne 0\) and \(m_j=0\); and

    • \(\langle \textbf{i}^{\textrm{I}} \textbf{j}^{\textrm{J}}\rangle \langle \textbf{j}_{\textrm{J}} \textbf{i}_{\textrm{I}}\rangle =-2m_im_j\) with \(m_i\ne 0\) and \(m_j\ne 0\);

We also tested products of two spinor products with one intermediate momentum:

  • \([\textbf{i}^{\textrm{I}} \textrm{j}]\langle \textrm{j}|p_k|\textbf{i}_{\textrm{I}}]= -2m_ip_j \! \cdot \! p_k\) with the properties:

    • \(m_i\ne 0\) since this identity does not apply otherwise;

    • \(m_j=0\); and

    • \(m_k=0\) and \(m_k\ne 0\);

  • \([\textbf{i}^{\textrm{I}} \textbf{j}^{\textrm{J}}]\langle \textbf{j}_{\textrm{J}}|p_k|\textbf{i}_{\textrm{I}}]= -2m_ip_j \! \cdot \! p_k\) with the properties:

    • \(m_i\ne 0\) since this identity does not apply otherwise;

    • \(m_j\ne 0\); and

    • \(m_k=0\) and \(m_k\ne 0\);

Our next batch of tests has two momenta both in the same spinor product:

  • \([\textbf{i}^{\textrm{I}}\textbf{j}^{\textrm{J}}][\textbf{j}_{\textrm{J}}|p_k p_l|\textbf{i}_{\textrm{I}}]=-2m_im_j p_k \! \cdot \! p_l\) with the properties:

    • \(m_i\ne 0\) and \(m_j\ne 0\) since this identity does not apply otherwise;

    • \(m_k=0\) and \(m_k\ne 0\); and

    • \(m_l=0\) and \(m_l\ne 0\);

  • \(\text{ Re }\left( [\textrm{i j}]\langle \textrm{j}|p_k p_l|\textrm{i}\rangle \right) = 2p_i \! \cdot \! p_j p_k \! \cdot \! p_l - 2p_i \! \cdot \! p_k p_j \! \cdot \! p_l + 2p_i \! \cdot \! p_l p_j \! \cdot \! p_k\) with the properties:

    • \(m_i=0\) and \(m_j=0\);

    • \(m_k=0\) and \(m_k\ne 0\); and

    • \(m_l=0\) and \(m_l\ne 0\);

  • \(\text{ Re }\left( [\textbf{i}^{\textrm{I}} \textrm{j}]\langle \textrm{j}|p_k p_l|\textbf{i}_{\textrm{I}}\rangle \right) = 2p_i \! \cdot \! p_j p_k \! \cdot \! p_l - 2p_i \! \cdot \! p_k p_j \! \cdot \! p_l + 2p_i \! \cdot \! p_l p_j \! \cdot \! p_k\) with the properties:

    • \(m_i\ne 0\) and \(m_j=0\);

    • \(m_k=0\) and \(m_k\ne 0\); and

    • \(m_l=0\) and \(m_l\ne 0\);

  • \(\text{ Re }\left( [\textrm{i} \textbf{j}^{\textrm{J}}]\langle \textbf{j}_{\textrm{J}}|p_k p_l|\textrm{i}\rangle \right) = 2p_i \! \cdot \! p_j p_k \! \cdot \! p_l - 2p_i \! \cdot \! p_k p_j \! \cdot \! p_l + 2p_i \! \cdot \! p_l p_j \! \cdot \! p_k\) with the properties:

    • \(m_i=0\) and \(m_j\ne 0\);

    • \(m_k=0\) and \(m_k\ne 0\); and

    • \(m_l=0\) and \(m_l\ne 0\);

  • \(\text{ Re }\left( [\textbf{i}^{\textrm{I}} \textbf{j}^{\textrm{J}}]\langle \textbf{j}_{\textrm{J}}|p_k p_l|\textbf{i}_{\textrm{I}}\rangle \right) = 2p_i \! \cdot \! p_j p_k \! \cdot \! p_l - 2p_i \! \cdot \! p_k p_j \! \cdot \! p_l + 2p_i \! \cdot \! p_l p_j \! \cdot \! p_k\) with the properties:

    • \(m_i\ne 0\) and \(m_j\ne 0\);

    • \(m_k=0\) and \(m_k\ne 0\); and

    • \(m_l=0\) and \(m_l\ne 0\);

We follow this with a series of tests where the two momenta are in separate spinor products:

  • \([\textbf{i}^{\textrm{I}}|p_l|\textbf{j}^{\textrm{J}}\rangle \langle \textbf{2}_{\textrm{J}}|p_k|\textbf{i}_{\textrm{I}}]=2m_im_jp_k \! \cdot \! p_l\) with the properties:

    • \(m_i\ne 0\) and \(m_j\ne 0\) since this identity does not otherwise apply;

    • \(m_k=0\) and \(m_k\ne 0\); and

    • \(m_l=0\) and \(m_l\ne 0\);

  • \(\textrm{Re}\left( [\textrm{i}|p_l|\textrm{j}\rangle [\textrm{j}|p_k|\textrm{i}\rangle \right) = -2p_i \! \cdot \! p_l p_j \! \cdot \! p_k + 2p_i \! \cdot \! p_j p_k \! \cdot \! p_l - 2p_i \! \cdot \! p_k p_j \! \cdot \! p_l\) with the properties that:

    • \(m_i=0\) and \(m_j=0\);

    • \(m_k=0\) and \(m_k\ne 0\); and

    • \(m_l=0\) and \(m_l\ne 0\);

  • \(\textrm{Re}\left( [\textbf{i}^{\textrm{I}}|p_l|\textrm{j}\rangle [\textrm{j}|p_k|\textbf{i}_{\textrm{I}}\rangle \right) = -2p_i \! \cdot \! p_l p_j \! \cdot \! p_k + 2p_i \! \cdot \! p_j p_k \! \cdot \! p_l - 2p_i \! \cdot \! p_k p_j \! \cdot \! p_l\) with the properties that:

    • \(m_i\ne 0\) and \(m_j=0\);

    • \(m_k=0\) and \(m_k\ne 0\); and

    • \(m_l=0\) and \(m_l\ne 0\);

  • \(\textrm{Re}\left( [\textrm{i}|p_l|\textbf{j}^{\textrm{J}}\rangle [\textbf{j}_{\textrm{J}}|p_k|\textrm{i}\rangle \right) = -2p_i \! \cdot \! p_l p_j \! \cdot \! p_k + 2p_i \! \cdot \! p_j p_k \! \cdot \! p_l - 2p_i \! \cdot \! p_k p_j \! \cdot \! p_l\) with the properties that:

    • \(m_i=0\) and \(m_j\ne 0\);

    • \(m_k=0\) and \(m_k\ne 0\); and

    • \(m_l=0\) and \(m_l\ne 0\);

  • \(\textrm{Re}\left( [\textbf{i}^{\textrm{I}}|p_l|\textbf{j}^{\textrm{J}}\rangle [\textbf{j}_{\textrm{J}}|p_k|\textbf{i}_{\textrm{I}}\rangle \right) = -2p_i \! \cdot \! p_l p_j \! \cdot \! p_k + 2p_i \! \cdot \! p_j p_k \! \cdot \! p_l - 2p_i \! \cdot \! p_k p_j \! \cdot \! p_l\) with the properties that:

    • \(m_i=0\) and \(m_j\ne 0\);

    • \(m_k=0\) and \(m_k\ne 0\); and

    • \(m_l=0\) and \(m_l\ne 0\);

We also test with two momenta in one spinor product and one momentum in the other spinor product:

  • \(\textrm{Re}\left( [\textrm{i}|p_l|\textbf{j}^{\textrm{J}}\rangle \langle \textbf{j}_{\textrm{J}}|p_mp_k|\textrm{i}\rangle \right) = -2m_j(p_i \! \cdot \! p_l p_k \! \cdot \! p_m-p_i \! \cdot \! p_m p_k \! \cdot \! p_l+p_i \! \cdot \! p_k p_l \! \cdot \! p_m)\) with the properties that:

    • \(m_j\ne 0\) since the identity does not apply otherwise;

    • \(m_i=0\);

    • \(m_k=0\) and \(m_k\ne 0\);

    • \(m_l=0\) and \(m_l\ne 0\); and

    • \(m_m=0\) and \(m_m\ne 0\);

  • \(\textrm{Re}\left( [\textbf{i}^{\textrm{I}}|p_l|\textbf{j}^{\textrm{J}}\rangle \langle \textbf{j}_{\textrm{J}}|p_mp_k|\textbf{i}_{\textrm{I}}\rangle \right) = -2m_j(p_i \! \cdot \! p_l p_k \! \cdot \! p_m-p_i \! \cdot \! p_m p_k \! \cdot \! p_l+p_i \! \cdot \! p_k p_l \! \cdot \! p_m)\) with the properties that:

    • \(m_j\ne 0\) since the identity does not apply otherwise;

    • \(m_i\ne 0\);

    • \(m_k=0\) and \(m_k\ne 0\);

    • \(m_l=0\) and \(m_l\ne 0\); and

    • \(m_m=0\) and \(m_m\ne 0\);

Next, we have tested spinor products where not all the spin indices are summed over, so that there are spinor products on both sides of the equation. We begin with tests that contract an angle and a square spin spinor to obtain a momentum. We have tested:

  • \([\textrm{i}\textbf{j}_{\textrm{J}}]\langle \textbf{j}^{\textrm{J}}\textrm{k}\rangle =[\textrm{i}|p_j|\textrm{k}\rangle \) with:

    • \(m_j\ne 0\), \(m_i=0\) and \(m_k=0\);

  • \([\textbf{i}^{\textrm{I}}\textbf{j}_{\textrm{J}}]\langle \textbf{j}^{\textrm{J}}\textrm{k}\rangle =[\textbf{i}^{\textrm{I}}|p_j|\textrm{k}\rangle \) with:

    • \(m_j\ne 0\), \(m_i\ne 0\) and \(m_k=0\);

  • \([\textrm{i}\textbf{j}_{\textrm{J}}]\langle \textbf{j}^{\textrm{J}}\textbf{k}^{\textrm{K}}\rangle =[\textrm{i}|p_j|\textbf{k}^{\textrm{K}}\rangle \) with:

    • \(m_j\ne 0\), \(m_i=0\) and \(m_k\ne 0\);

  • \([\textbf{i}^{\textrm{I}}\textbf{j}_{\textrm{J}}]\langle \textbf{j}^{\textrm{J}}\textbf{k}^{\textrm{K}}\rangle =[\textbf{i}^{\textrm{I}}|p_j|\textbf{k}^{\textrm{K}}\rangle \) with:

    • \(m_j\ne 0\), \(m_i\ne 0\) and \(m_k\ne 0\);

  • \([\textrm{i}\textbf{j}^{\textrm{J}}]\langle \textbf{j}_{\textrm{J}}\textrm{k}\rangle =-[\textrm{i}|p_j|\textrm{k}\rangle \) with:

    • \(m_j\ne 0\), \(m_i=0\) and \(m_k=0\);

  • \([\textbf{i}^{\textrm{I}}\textbf{j}^{\textrm{J}}]\langle \textbf{j}_{\textrm{J}}\textrm{k}\rangle =-[\textbf{i}^{\textrm{I}}|p_j|\textrm{k}\rangle \) with:

    • \(m_j\ne 0\), \(m_i\ne 0\) and \(m_k=0\);

  • \([\textrm{i}\textbf{j}^{\textrm{J}}]\langle \textbf{j}_{\textrm{J}}\textbf{k}^{\textrm{K}}\rangle =-[\textrm{i}|p_j|\textbf{k}^{\textrm{K}}\rangle \) with:

    • \(m_j\ne 0\), \(m_i=0\) and \(m_k\ne 0\);

  • \([\textbf{i}^{\textrm{I}}\textbf{j}^{\textrm{J}}]\langle \textbf{j}_{\textrm{J}}\textbf{k}^{\textrm{K}}\rangle =-[\textbf{i}^{\textrm{I}}|p_j|\textbf{k}^{\textrm{K}}\rangle \) with:

    • \(m_j\ne 0\), \(m_i\ne 0\) and \(m_k\ne 0\);

  • \(\langle \textrm{i}\textbf{j}^{\textrm{J}}\rangle [\textbf{j}_{\textrm{J}}\textrm{k}]=\langle \textrm{i}|p_j|\textrm{k}]\) with:

    • \(m_j\ne 0\), \(m_i=0\) and \(m_k=0\);

  • \(\langle \textbf{i}^{\textrm{I}}\textbf{j}^{\textrm{J}}\rangle [\textbf{j}_{\textrm{J}}\textrm{k}]=\langle \textbf{i}^{\textrm{I}}|p_j|\textrm{k}]\) with:

    • \(m_j\ne 0\), \(m_i\ne 0\) and \(m_k=0\);

  • \(\langle \textrm{i}\textbf{j}^{\textrm{J}}\rangle [\textbf{j}_{\textrm{J}}\textbf{k}^{\textrm{K}}]=\langle \textrm{i}|p_j|\textbf{k}^{\textrm{K}}]\) with:

    • \(m_j\ne 0\), \(m_i=0\) and \(m_k\ne 0\);

  • \(\langle \textbf{i}^{\textrm{I}}\textbf{j}^{\textrm{J}}\rangle [\textbf{j}_{\textrm{J}}\textbf{k}^{\textrm{K}}]=\langle \textbf{i}^{\textrm{I}}|p_j|\textbf{k}^{\textrm{K}}]\) with:

    • \(m_j\ne 0\), \(m_i\ne 0\) and \(m_k\ne 0\);

  • \(\langle \textrm{i}\textbf{j}_{\textrm{J}}\rangle [\textbf{j}^{\textrm{J}}\textrm{k}]=-\langle \textrm{i}|p_j|\textrm{k}]\) with:

    • \(m_j\ne 0\), \(m_i=0\) and \(m_k=0\);

  • \(\langle \textbf{i}^{\textrm{I}}\textbf{j}_{\textrm{J}}\rangle [\textbf{j}^{\textrm{J}}\textrm{k}]=-\langle \textbf{i}^{\textrm{I}}|p_j|\textrm{k}]\) with:

    • \(m_j\ne 0\), \(m_i\ne 0\) and \(m_k=0\);

  • \(\langle \textrm{i}\textbf{j}_{\textrm{J}}\rangle [\textbf{j}^{\textrm{J}}\textbf{k}^{\textrm{K}}]=-\langle \textrm{i}|p_j|\textbf{k}^{\textrm{K}}]\) with:

    • \(m_j\ne 0\), \(m_i=0\) and \(m_k\ne 0\);

  • \(\langle \textbf{i}^{\textrm{I}}\textbf{j}_{\textrm{J}}\rangle [\textbf{j}^{\textrm{J}}\textbf{k}^{\textrm{K}}]=-\langle \textbf{i}^{\textrm{I}}|p_j|\textbf{k}^{\textrm{K}}]\) with:

    • \(m_j\ne 0\), \(m_i\ne 0\) and \(m_k\ne 0\);

We have also tested contractions of two square brackets and two angle brackets, which give a mass. We have done the following tests:

  • \([\textrm{i}\textbf{j}_{\textrm{J}}][\textbf{j}^{\textrm{J}}\textrm{k}]=m_j[\textrm{i k}]\) with:

    • \(m_j\ne 0\), \(m_i=0\) and \(m_k=0\);

  • \([\textbf{i}^{\textrm{I}}\textbf{j}_{\textrm{J}}][\textbf{j}^{\textrm{J}}\textrm{k}]=m_j[\textbf{i}^{\textrm{I}} \textrm{k}]\) with:

    • \(m_j\ne 0\), \(m_i\ne 0\) and \(m_k=0\);

  • \([\textrm{i}\textbf{j}_{\textrm{J}}][\textbf{j}^{\textrm{J}}\textbf{k}^{\textrm{K}}]=m_j[\textrm{i} \textbf{k}^{\textrm{K}}]\) with:

    • \(m_j\ne 0\), \(m_i=0\) and \(m_k\ne 0\);

  • \([\textbf{i}^{\textrm{I}}\textbf{j}_{\textrm{J}}][\textbf{j}^{\textrm{J}}\textbf{k}^{\textrm{K}}]=m_j[\textbf{i}^{\textrm{I}} \textbf{k}^{\textrm{K}}]\) with:

    • \(m_j\ne 0\), \(m_i\ne 0\) and \(m_k\ne 0\);

  • \([\textrm{i}\textbf{j}^{\textrm{J}}][\textbf{j}_{\textrm{J}}\textrm{k}]=-m_j[\textrm{i k}]\) with:

    • \(m_j\ne 0\), \(m_i=0\) and \(m_k=0\);

  • \([\textbf{i}^{\textrm{I}}\textbf{j}^{\textrm{J}}][\textbf{j}_{\textrm{J}}\textrm{k}]=-m_j[\textbf{i}^{\textrm{I}} \textrm{k}]\) with:

    • \(m_j\ne 0\), \(m_i\ne 0\) and \(m_k=0\);

  • \([\textrm{i}\textbf{j}^{\textrm{J}}][\textbf{j}_{\textrm{J}}\textbf{k}^{\textrm{K}}]=-m_j[\textrm{i} \textbf{k}^{\textrm{K}}]\) with:

    • \(m_j\ne 0\), \(m_i=0\) and \(m_k\ne 0\);

  • \([\textbf{i}^{\textrm{I}}\textbf{j}^{\textrm{J}}][\textbf{j}_{\textrm{J}}\textbf{k}^{\textrm{K}}]=-m_j[\textbf{i}^{\textrm{I}} \textbf{k}^{\textrm{K}}]\) with:

    • \(m_j\ne 0\), \(m_i\ne 0\) and \(m_k\ne 0\);

  • \(\langle \textrm{i}\textbf{j}^{\textrm{J}}\rangle \langle \textbf{j}_{\textrm{J}}\textrm{k}\rangle =m_j[\textrm{i k}]\) with:

    • \(m_j\ne 0\), \(m_i=0\) and \(m_k=0\);

  • \(\langle \textbf{i}^{\textrm{I}}\textbf{j}^{\textrm{J}}\rangle \langle \textbf{j}_{\textrm{J}}\textrm{k}\rangle =m_j[\textbf{i}^{\textrm{I}} \textrm{k}]\) with:

    • \(m_j\ne 0\), \(m_i\ne 0\) and \(m_k=0\);

  • \(\langle \textrm{i}\textbf{j}^{\textrm{J}}\rangle \langle \textbf{j}_{\textrm{J}}\textbf{k}^{\textrm{K}}\rangle =m_j[\textrm{i} \textbf{k}^{\textrm{K}}]\) with:

    • \(m_j\ne 0\), \(m_i=0\) and \(m_k\ne 0\);

  • \(\langle \textbf{i}^{\textrm{I}}\textbf{j}^{\textrm{J}}\rangle \langle \textbf{j}_{\textrm{J}}\textbf{k}^{\textrm{K}}\rangle =m_j[\textbf{i}^{\textrm{I}} \textbf{k}^{\textrm{K}}]\) with:

    • \(m_j\ne 0\), \(m_i\ne 0\) and \(m_k\ne 0\);

  • \([\textrm{i}\textbf{j}^{\textrm{J}}][\textbf{j}_{\textrm{J}}\textrm{k}]=-m_j[\textrm{i k}]\) with:

    • \(m_j\ne 0\), \(m_i=0\) and \(m_k=0\);

  • \([\textbf{i}^{\textrm{I}}\textbf{j}^{\textrm{J}}][\textbf{j}_{\textrm{J}}\textrm{k}]=-m_j[\textbf{i}^{\textrm{I}} \textrm{k}]\) with:

    • \(m_j\ne 0\), \(m_i\ne 0\) and \(m_k=0\);

  • \([\textrm{i}\textbf{j}^{\textrm{J}}][\textbf{j}_{\textrm{J}}\textbf{k}^{\textrm{K}}]=-m_j[\textrm{i} \textbf{k}^{\textrm{K}}]\) with:

    • \(m_j\ne 0\), \(m_i=0\) and \(m_k\ne 0\);

  • \([\textbf{i}^{\textrm{I}}\textbf{j}^{\textrm{J}}][\textbf{j}_{\textrm{J}}\textbf{k}^{\textrm{K}}]=-m_j[\textbf{i}^{\textrm{I}} \textbf{k}^{\textrm{K}}]\) with:

    • \(m_j\ne 0\), \(m_i\ne 0\) and \(m_k\ne 0\);

In the following, we have tested an identity under momentum conservation. That is, we took \(p_l=-p_i-p_j-p_k\), numerically. We then plugged it in to \([\textrm{i}|p_l|\textrm{j}\rangle \) and compared it with \(m_j[\textrm{ij}]-m_i\langle \textrm{ij}\rangle -[\textrm{i}|p_k|\textrm{j}\rangle \). We did this for a variety of masses. In each case, \(m_l\) was determined by momentum conservation. We tested:

  • \([\textrm{i}|p_l|\textrm{j}\rangle =-[\textrm{i}|p_k|\textrm{j}\rangle \) with:

    • \(m_i=0\) and \(m_j=0\); and

    • \(m_k=0\) and \(m_k\ne 0\);

  • \([\textbf{i}^{\textrm{I}}|p_l|\textrm{j}\rangle =-m_i\langle \textbf{i}^{\textrm{I}}\textrm{j}\rangle -[\textbf{i}^{\textrm{I}}|p_k|\textrm{j}\rangle \) with:

    • \(m_i\ne 0\) and \(m_j=0\); and

    • \(m_k=0\) and \(m_k\ne 0\);

  • \([\textrm{i}|p_l|\textbf{j}^{\textrm{J}}\rangle =m_j[\textrm{i}\textbf{j}^{\textrm{J}}]-[\textrm{i}|p_k|\textbf{j}^{\textrm{J}}\rangle \) with:

    • \(m_i=0\) and \(m_j\ne 0\); and

    • \(m_k=0\) and \(m_k\ne 0\);

  • \([\textbf{i}^{\textrm{I}}|p_l|\textbf{j}^{\textrm{J}}\rangle =m_j[\textbf{i}^{\textrm{I}}\textbf{j}^{\textrm{J}}]-m_i\langle \textbf{i}^{\textrm{I}}\textbf{j}^{\textrm{J}}\rangle -[\textbf{i}^{\textrm{I}}|p_k|\textbf{j}^{\textrm{J}}\rangle \) with:

    • \(m_i\ne 0\) and \(m_j\ne 0\); and

    • \(m_k=0\) and \(m_k\ne 0\);

Finally, another test of momentum conservation was performed where \(p_l=-p_i-p_j-p_k\) was calculated and used in \([\textrm{i}|p_l|\textrm{j}\rangle +[\textrm{j}|p_l|\textrm{i}\rangle =-[\textrm{i}|p_k|\textrm{j}\rangle -[\textrm{j}|p_k|\textrm{i}\rangle \) where \(m_i=m_j\). The mass \(m_l\) was determined from momentum conservation. We tested:

  • \([\textrm{i}|p_l|\textrm{j}\rangle +[\textrm{j}|p_l|\textrm{i}\rangle =-[\textrm{i}|p_k|\textrm{j}\rangle -[\textrm{j}|p_k|\textrm{i}\rangle \) with:

    • \(m_i=0\) and \(m_j=0\); and

    • \(m_k=0\) and \(m_k\ne 0\);

  • \([\textbf{i}^{\textrm{I}}|p_l|\textbf{j}^{\textrm{J}}\rangle +[\textbf{j}^{\textrm{J}}|p_l|\textbf{i}^{\textrm{I}}\rangle =-[\textbf{i}^{\textrm{I}}|p_k|\textbf{j}^{\textrm{J}}\rangle -[\textbf{j}^{\textrm{J}}|p_k|\textbf{i}^{\textrm{I}}\rangle \) with:

    • \(m_i\ne 0\) and \(m_j\ne 0\); and

    • \(m_k=0\) and \(m_k\ne 0\);

5.2 SM \(\mathbf {2\rightarrow 2}\) process tests

SPINAS complements its unit tests with a robust suite of tests for \(2 \rightarrow 2\) processes in the Standard Model (SM), further validating the package’s implementation of spinors, our understanding of the relationship of processes related by crossing symmetry and its simulation capabilities in particle physics.

For testing, we utilize methods test_2to2_amp2, test_2to2_amp2_rotations, test_2to2_amp2_boosts, and test_2to2_amp2_boosts_and_rotations, detailed in Sect. 2.11.2. The squared amplitudes used for comparison are generated using CalcHEP [26].

As mentioned at the end of Sect. 2.11.2, we have set the widths to zero for all our tests. Furthermore, since CalcHEP gives the squared amplitude after summing (averaging) the spins and colors in the final (initial) states, we have done the same. We have also included symmetry factors to match the same done in CalcHEP. Obviously, the couplings must also be the same. Typically, that means turning off any running of the coupling during the comparison to ensure identical values.

The full list of \(2 \rightarrow 2\) SM processes tested can be found in Sect. 2.12. We present the masses and momenta used in each test, as well as the file name for the process in App. B. The momentum given is the magnitude of the vector momentum of the incoming particle in the CM frame, denoted as \(p_{\text {in}}\). The relations between each momentum component and \(p_{\text {in}}\) are given in Eqs. (5) through (9), with more details on the testing methodology provided in Sect. 2.11.2.

We have used the same electric coupling constant, strong coupling constant, and Weinberg angle for each process. All dimensionful variables are given in GeV. We chose a variety of masses and momenta, including cases where the momentum was near threshold and far above threshold. We also considered mass hierarchies that were SM like, as well as when the masses were similar and when they were inverted. The exact mass and momentum values are less important than covering a range of combinations that change the importance of different diagrams within the process. In order to keep this list reasonable, we will only show the values from the first process in each set. The values for the second and/or third processes of the set were mostly identical, with small changes when the momentum is near threshold. The specifics for each process follow the order in Table 1. To keep the main part of this document clean, we have included these values in App. B.

6 Conclusion and future work

This paper has presented SPINAS, a versatile C++ package designed for efficient calculation of scattering amplitudes at specific phase-space points. Utilizing spin and helicity spinors, SPINAS offers a complementary approach to traditional Feynman diagram calculations, setting the stage for comparative efficiency analysis between these two methods in future research endeavors.

Section 2 detailed each high-level component of SPINAS and how to use it in an amplitude calculation. It culminates with a comprehensive list of implemented Standard Model (SM) processes. These examples, cover at least two members of every crossing-symmetry classes of \(2\rightarrow 2\) processes in the SM, up to changes of mass for different fermion generations and serve as practical templates for users to base their custom implementations.

We followed this in Sect. 3 with a complete example of using this package to calculate the \(e,\bar{e}\rightarrow \mu ,\bar{\mu }\) amplitude in QED. In both of these sections, we have focused on a novice user, attempting to give all the required details.

Section 4 shifted focus to the design principles behind SPINAS, targeting potential contributors. We discussed the package’s release under the GNU Public License v3 and encouraged active community participation in its development. We envision SPINAS as a collaborative, community-driven project and eagerly anticipate its future evolution as guided by user contributions and feedback.

In Sect. 5, we outlined the rigorous validation processes applied to SPINAS. This included comprehensive testing of each class and method, as well as validations of every \(2 \rightarrow 2\) process against established Feynman diagram results, across a range of masses and momenta. These tests not only affirm the robustness of SPINAS but also validate the implemented amplitude calculations.

Looking ahead, a key objective is to develop an algorithm for the automatic generation of constructive amplitudes across any process and multiplicity within any constructive model. With this goal, we hope to significantly advance the field of amplitude calculations.