Skip to content

Commit 5ab1cf5

Browse files
authored
test: add Catch2 unit-test suite and CI (#6)
MCBooster is header-only and had no tests; "running tests" meant running the ROOT-dependent example executables by hand. Add a self-contained Catch2 suite under tests/ that pulls Thrust and Catch2 via FetchContent (no ROOT, TCLAP, or CUDA needed), pinned to the versions GooFit uses (Thrust 1.8.3, Catch2 v2.13.9). It builds and runs with only a C++11 compiler on the CPU CPP backend, and also on the OMP backend. Coverage: - test_vector4r: Lorentz/3-vector math (mass, Minkowski/spatial products, boost) - test_generate: PhaseSpace mass/energy-momentum conservation, weights, unweight - test_evaluate: EvaluateArray with a user IFunctionArray, mirroring GooFit usage CI runs the suite on gcc and clang x CPP/OMP backends. Also document the suite in CLAUDE.md and ignore build*/ dirs. Assisted-by: ClaudeCode:claude-opus-4.8
1 parent c8bcee8 commit 5ab1cf5

8 files changed

Lines changed: 438 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
tests:
15+
name: ${{ matrix.compiler }} / ${{ matrix.backend }}
16+
runs-on: ubuntu-latest
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
include:
21+
- compiler: gcc
22+
backend: CPP
23+
- compiler: gcc
24+
backend: OMP
25+
- compiler: clang
26+
backend: CPP
27+
- compiler: clang
28+
backend: OMP
29+
30+
steps:
31+
- uses: actions/checkout@v4
32+
33+
- name: Install Clang and OpenMP
34+
if: matrix.compiler == 'clang'
35+
run: sudo apt-get update && sudo apt-get install -y clang libomp-dev
36+
37+
- name: Select compiler
38+
run: |
39+
if [ "${{ matrix.compiler }}" = "clang" ]; then
40+
echo "CC=clang" >> "$GITHUB_ENV"
41+
echo "CXX=clang++" >> "$GITHUB_ENV"
42+
else
43+
echo "CC=gcc" >> "$GITHUB_ENV"
44+
echo "CXX=g++" >> "$GITHUB_ENV"
45+
fi
46+
47+
- name: Configure
48+
run: cmake -S tests -B build -DCMAKE_BUILD_TYPE=Release -DMCBOOSTER_BACKEND=${{ matrix.backend }}
49+
50+
- name: Build
51+
run: cmake --build build -j
52+
53+
- name: Test
54+
run: ctest --test-dir build --output-on-failure

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
webpage/
22
build/
3+
build*/
34
.settings/
45
.cproject
56
.project
67

8+
# Symlink to AGENTS.md
9+
CLAUDE.md
10+
.claude/
11+

AGENTS.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Agents notes
2+
3+
MCBooster is a **header-only** C++11 library for fast generation of phase-space Monte Carlo events (particle decays, up to nine final-state particles). It is built on [Thrust](https://thrust.github.io/), so the same source compiles to CUDA (GPU), OpenMP, TBB, or single-threaded CPP backends. The algorithm follows ROOT's `TGenPhaseSpace` / CERNLIB GENBOD (Raubold–Lynch method).
4+
5+
This is the **GooFit fork** of the original MultithreadCorner/MCBooster. There are no tagged releases here; it tracks GooFit's needs.
6+
7+
Library lives in `mcbooster/` (all headers). `src/` holds example programs, not library code.
8+
9+
## Build & run
10+
11+
The library itself needs no build — just put `mcbooster/` on the include path. The CMake build only compiles the examples in `src/`.
12+
13+
```bash
14+
mkdir build && cd build
15+
cmake ../ # prints the install prefix; add -DCMAKE_INSTALL_PREFIX=<path> for non-root
16+
make
17+
```
18+
19+
Requirements for examples: Thrust ≥1.8, [ROOT](https://root.cern.ch/), [TCLAP](http://tclap.sourceforge.net/) (all `find_package`'d as REQUIRED). CUDA ≥6.5 is optional — if absent, only the `*_OpenMP_GCC_*` targets build.
20+
21+
## Tests
22+
23+
`tests/` holds a Catch2 unit-test suite (the example executables above are *not* tests). It is a self-contained CMake project that pulls in Thrust and Catch2 via FetchContent — no ROOT, TCLAP, or CUDA needed — pinned to the versions GooFit uses (Thrust 1.8.3, Catch2 v2.13.9). The backend defaults to the CPU `CPP` Thrust backend, so the suite builds and runs with only a C++11 compiler.
24+
25+
```bash
26+
cmake -S tests -B build-tests -DMCBOOSTER_BACKEND=CPP # or OMP
27+
cmake --build build-tests -j
28+
ctest --test-dir build-tests --output-on-failure
29+
```
30+
31+
`.github/workflows/ci.yml` runs this on gcc and clang × `CPP`/`OMP`. Coverage: `test_vector4r` (Lorentz/3-vector math), `test_generate` (`PhaseSpace` mass/conservation/weights/unweighting), `test_evaluate` (`EvaluateArray` with a user `IFunctionArray`, mirroring GooFit's usage). Tests use only the public `mcbooster::` types so they are backend-agnostic.
32+
33+
Example executables follow `MCBooster_Example_<BACKEND_AND_COMPILER>_<NAME>`:
34+
- `*_B2KPiJpsi` (`src/Generate.cu`/`.cpp`) — generates B0→J/ψ K π and evaluates kinematic variables in parallel.
35+
- `*_GenerateSample` (`src/GenerateSample.cu`) — CLI-driven generator that writes a ROOT TTree.
36+
- `*_PerformanceTest` (`src/PerformanceTest.cu`/`.cpp`) — timing vs. number of events/particles.
37+
- `*_CompareWithRoot` (`src/CompareWithTGenPhaseSpace.cu`) — validates against ROOT.
38+
39+
The `.cu` and `.cpp` files for a given example are usually **the same code**`.cu` is compiled by nvcc, `.cpp` by gcc for the OpenMP-only path. Keep them in sync when editing.
40+
41+
## Backend selection (key concept)
42+
43+
The backend is chosen at compile time via the `MCBOOSTER_BACKEND` macro (`CUDA`, `OMP`, `TBB`, or `CPP`), defaulting to `CUDA` (`mcbooster/Config.h`). Examples set it with `-DMCBOOSTER_BACKEND=OMP`. Because everything routes through Thrust execution policies, **writing code only with MCBooster's provided types lets you switch backends just by recompiling** (often literally `.cu``.cpp`).
44+
45+
## Architecture
46+
47+
Everything is in `namespace mcbooster`. Headers include each other through `mcbooster/Config.h`, which selects the backend and pulls in Thrust.
48+
49+
- **`GTypes.h`** — portable scalar typedefs (`GReal_t` is `double`, or `float` under `-DFP_SINGLE`; `GInt_t`, `GBool_t`, etc.). `kMAXP 9` caps the number of particles. Use these types everywhere instead of raw C++ types.
50+
- **`GContainers.h` / `GContainersHost.h`** — the container layer. `mc_device_vector<T>` / `mc_host_vector<T>` wrap Thrust vectors so user code stays backend-agnostic. Domain typedefs: `RealVector_d`, `BoolVector_d`, `Particles_d` (a `Vector4R` device vector), and the set types `ParticlesSet_d` / `VariableSet_d` (STL vectors of pointers to those device vectors — one entry per particle/variable).
51+
- **`Vector4R.h` / `Vector3R.h`**`__host__ __device__` Lorentz/3-vector math used inside functors and user kinematics code.
52+
- **`Generate.h`** — the core. `class PhaseSpace` is the generator: construct with mother mass, daughter masses, and event count; call `Generate(mother)`, then `Unweight()`, then `Export(Events*)` (or `ExportUnweighted`). `struct Events` is the host-side output container. Generation runs the `functors/` as Thrust transforms.
53+
- **`Evaluate.h` / `EvaluateArray.h`** — free function templates that run a user functor over a `ParticlesSet_d` in parallel. `Evaluate` returns one value per event; `EvaluateArray` fills a `VariableSet_d` (several variables per event in one pass). Overloads target host output, device output, or in-place.
54+
- **`GFunctional.h`** — the two abstract functor interfaces users subclass: `IFunction<RESULT>` (one value) and `IFunctionArray` (fills a `GReal_t*` array). Both declare `__host__ __device__ virtual operator()` taking `Vector4R** particles`.
55+
- **`functors/`** — internal Thrust functors driving generation: `DecayMother`/`DecayMothers` (single fixed mother vs. per-event mothers, for sequential decays), `RandGen`, `FlagAcceptReject` + `IsAccepted` (the unweighting), `Calculate`.
56+
- **`strided_iterator.h`** — iterator helper for interleaved data layouts.
57+
58+
`MCBooster.h` is generated by CMake from `MCBooster.h.in` (carries the version); the committed copy may be regenerated by a `cmake` run.
59+
60+
## Conventions
61+
62+
- `GReal_t` not `double`, `GInt_t` not `int`, etc. — this preserves the `FP_SINGLE` switch and portability.
63+
- Functions touching kinematics are marked `__host__ __device__` so they work on both backends.
64+
- New headers should include `mcbooster/Config.h` (directly or transitively) before using Thrust.

tests/CMakeLists.txt

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Self-contained test build for MCBooster.
2+
#
3+
# MCBooster is header-only, so this project pulls in its two test-only
4+
# dependencies (Thrust and Catch2) via FetchContent and never touches the
5+
# ROOT/TCLAP example build at the repo root. Run it standalone:
6+
#
7+
# cmake -S tests -B build-tests
8+
# cmake --build build-tests
9+
# ctest --test-dir build-tests --output-on-failure
10+
#
11+
# The backend is selected with -DMCBOOSTER_BACKEND=CPP|OMP (default CPP), so CI
12+
# can exercise the library with no CUDA toolkit installed.
13+
14+
cmake_minimum_required(VERSION 3.14)
15+
project(MCBoosterTests CXX)
16+
17+
set(CMAKE_CXX_STANDARD 11)
18+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
19+
set(CMAKE_CXX_EXTENSIONS OFF)
20+
21+
set(MCBOOSTER_BACKEND "CPP" CACHE STRING "Thrust backend for the tests: CPP or OMP")
22+
set_property(CACHE MCBOOSTER_BACKEND PROPERTY STRINGS CPP OMP)
23+
24+
include(FetchContent)
25+
26+
# Thrust: pinned to the exact commit GooFit vendors for its host/OMP builds
27+
# (extern/thrust in GooFit#379; THRUST_VERSION 100803). The CCCL 3.0 path GooFit
28+
# uses only applies to its CUDA 13 builds, which these CPU/OMP tests do not take.
29+
FetchContent_Declare(
30+
thrust
31+
GIT_REPOSITORY https://github.com/NVIDIA/thrust.git
32+
GIT_TAG 8551c97870cd722486ba7834ae9d867f13e299ad)
33+
# Populate only -- Thrust 1.8.3 ships a legacy CMakeLists we do not want to run.
34+
FetchContent_GetProperties(thrust)
35+
if(NOT thrust_POPULATED)
36+
FetchContent_Populate(thrust)
37+
endif()
38+
39+
# Catch2 v2: the last single-header, C++11-friendly line. Pinned to the same tag
40+
# GooFit vendors (extern/catch2 in GooFit#379).
41+
FetchContent_Declare(
42+
catch2
43+
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
44+
GIT_TAG v2.13.9
45+
GIT_SHALLOW TRUE)
46+
FetchContent_MakeAvailable(catch2)
47+
48+
# INTERFACE target carrying everything needed to compile against the library.
49+
add_library(mcbooster INTERFACE)
50+
target_include_directories(mcbooster INTERFACE "${PROJECT_SOURCE_DIR}/.." "${thrust_SOURCE_DIR}")
51+
target_compile_definitions(mcbooster INTERFACE MCBOOSTER_BACKEND=${MCBOOSTER_BACKEND})
52+
53+
if(MCBOOSTER_BACKEND STREQUAL "OMP")
54+
find_package(OpenMP REQUIRED)
55+
target_link_libraries(mcbooster INTERFACE OpenMP::OpenMP_CXX)
56+
target_compile_definitions(
57+
mcbooster INTERFACE THRUST_DEVICE_SYSTEM=THRUST_DEVICE_SYSTEM_OMP
58+
THRUST_HOST_SYSTEM=THRUST_HOST_SYSTEM_CPP)
59+
else()
60+
target_compile_definitions(
61+
mcbooster INTERFACE THRUST_DEVICE_SYSTEM=THRUST_DEVICE_SYSTEM_CPP
62+
THRUST_HOST_SYSTEM=THRUST_HOST_SYSTEM_CPP)
63+
endif()
64+
65+
message(STATUS "MCBooster tests backend: ${MCBOOSTER_BACKEND}")
66+
67+
# Catch2's main(), compiled once and shared across the test executables.
68+
add_library(catch_main STATIC catch_main.cpp)
69+
target_link_libraries(catch_main PUBLIC Catch2::Catch2)
70+
71+
enable_testing()
72+
73+
set(MCBOOSTER_TESTS test_vector4r test_generate test_evaluate)
74+
foreach(test IN LISTS MCBOOSTER_TESTS)
75+
add_executable(${test} ${test}.cpp)
76+
target_link_libraries(${test} PRIVATE mcbooster catch_main)
77+
add_test(NAME ${test} COMMAND ${test})
78+
endforeach()

tests/catch_main.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Provides Catch2's main(). Kept in its own TU so the (slow) Catch2 main is
2+
// compiled once and linked into every test executable.
3+
#define CATCH_CONFIG_MAIN
4+
#include <catch2/catch.hpp>

tests/test_evaluate.cpp

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Exercises EvaluateArray with a user IFunctionArray functor -- the exact
2+
// pattern GooFit uses to compute kinematic variables (e.g. Dim5) in parallel
3+
// over a generated sample.
4+
#include <catch2/catch.hpp>
5+
6+
#include <vector>
7+
8+
#include <mcbooster/EvaluateArray.h>
9+
#include <mcbooster/GContainers.h>
10+
#include <mcbooster/GFunctional.h>
11+
#include <mcbooster/Generate.h>
12+
#include <mcbooster/Vector4R.h>
13+
14+
using mcbooster::GInt_t;
15+
using mcbooster::GReal_t;
16+
using mcbooster::Vector4R;
17+
18+
namespace {
19+
// Computes the three two-body invariant masses of a 3-body final state.
20+
struct InvariantMasses : public mcbooster::IFunctionArray {
21+
InvariantMasses() { dim = 3; }
22+
23+
__host__ __device__ void operator()(const GInt_t, Vector4R **p, GReal_t *out) override {
24+
out[0] = (*p[0] + *p[1]).mass();
25+
out[1] = (*p[0] + *p[2]).mass();
26+
out[2] = (*p[1] + *p[2]).mass();
27+
}
28+
};
29+
30+
const GReal_t kMotherMass = 1.86484;
31+
const std::vector<GReal_t> kMasses{0.493677, 0.139570, 0.134977};
32+
const mcbooster::GLong_t kNEvents = 2000;
33+
} // namespace
34+
35+
TEST_CASE("EvaluateArray matches a host-side recomputation", "[evaluate]") {
36+
mcbooster::PhaseSpace phsp(kMotherMass, kMasses, kNEvents);
37+
phsp.Generate(Vector4R(kMotherMass, 0.0, 0.0, 0.0));
38+
39+
mcbooster::ParticlesSet_d particles{&phsp.GetDaughters(0), &phsp.GetDaughters(1), &phsp.GetDaughters(2)};
40+
41+
mcbooster::RealVector_h m01(kNEvents);
42+
mcbooster::RealVector_h m02(kNEvents);
43+
mcbooster::RealVector_h m12(kNEvents);
44+
mcbooster::VariableSet_h variables{&m01, &m02, &m12};
45+
46+
InvariantMasses functor;
47+
mcbooster::EvaluateArray<InvariantMasses>(functor, particles, variables);
48+
49+
// Recompute on the host straight from the daughter four-vectors.
50+
mcbooster::Particles_h d0 = phsp.GetDaughters(0);
51+
mcbooster::Particles_h d1 = phsp.GetDaughters(1);
52+
mcbooster::Particles_h d2 = phsp.GetDaughters(2);
53+
54+
const GReal_t lo01 = kMasses[0] + kMasses[1];
55+
const GReal_t hi01 = kMotherMass - kMasses[2];
56+
57+
for(int i = 0; i < kNEvents; i++) {
58+
INFO("event " << i);
59+
CHECK(m01[i] == Approx((d0[i] + d1[i]).mass()).epsilon(1e-6));
60+
CHECK(m02[i] == Approx((d0[i] + d2[i]).mass()).epsilon(1e-6));
61+
CHECK(m12[i] == Approx((d1[i] + d2[i]).mass()).epsilon(1e-6));
62+
63+
// And the invariant mass must respect the Dalitz boundaries.
64+
CHECK(m01[i] >= Approx(lo01).epsilon(1e-5));
65+
CHECK(m01[i] <= Approx(hi01).epsilon(1e-5));
66+
}
67+
}

tests/test_generate.cpp

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Exercises the core phase-space generator (PhaseSpace::Generate) that GooFit
2+
// uses to produce normalization/toy samples. Runs on whichever Thrust backend
3+
// the build selects.
4+
#include <catch2/catch.hpp>
5+
6+
#include <cmath>
7+
#include <vector>
8+
9+
#include <mcbooster/GContainers.h>
10+
#include <mcbooster/Generate.h>
11+
#include <mcbooster/Vector4R.h>
12+
13+
using mcbooster::GReal_t;
14+
using mcbooster::PhaseSpace;
15+
using mcbooster::Vector4R;
16+
17+
namespace {
18+
// D0 -> K- pi+ pi0, a realistic 3-body decay.
19+
const GReal_t kMotherMass = 1.86484;
20+
const std::vector<GReal_t> kMasses{0.493677, 0.139570, 0.134977};
21+
const mcbooster::GLong_t kNEvents = 5000;
22+
} // namespace
23+
24+
TEST_CASE("PhaseSpace reports its configuration", "[generate]") {
25+
PhaseSpace phsp(kMotherMass, kMasses, kNEvents);
26+
CHECK(phsp.GetNDaughters() == 3);
27+
CHECK(phsp.GetNEvents() == kNEvents);
28+
}
29+
30+
TEST_CASE("Generated daughters have the requested masses", "[generate]") {
31+
PhaseSpace phsp(kMotherMass, kMasses, kNEvents);
32+
phsp.Generate(Vector4R(kMotherMass, 0.0, 0.0, 0.0));
33+
34+
for(int d = 0; d < 3; d++) {
35+
mcbooster::Particles_h daughters = phsp.GetDaughters(d);
36+
for(int i = 0; i < 50; i++) {
37+
INFO("daughter " << d << ", event " << i);
38+
CHECK(daughters[i].mass() == Approx(kMasses[d]).epsilon(1e-5));
39+
}
40+
}
41+
}
42+
43+
TEST_CASE("Generation conserves energy and momentum", "[generate]") {
44+
PhaseSpace phsp(kMotherMass, kMasses, kNEvents);
45+
phsp.Generate(Vector4R(kMotherMass, 0.0, 0.0, 0.0));
46+
47+
mcbooster::Particles_h d0 = phsp.GetDaughters(0);
48+
mcbooster::Particles_h d1 = phsp.GetDaughters(1);
49+
mcbooster::Particles_h d2 = phsp.GetDaughters(2);
50+
51+
for(int i = 0; i < 50; i++) {
52+
Vector4R total = d0[i] + d1[i] + d2[i];
53+
INFO("event " << i);
54+
CHECK(total.get(0) == Approx(kMotherMass).epsilon(1e-5)); // energy -> mother mass
55+
CHECK(total.get(1) == Approx(0.0).margin(1e-5)); // px
56+
CHECK(total.get(2) == Approx(0.0).margin(1e-5)); // py
57+
CHECK(total.get(3) == Approx(0.0).margin(1e-5)); // pz
58+
CHECK(total.mass() == Approx(kMotherMass).epsilon(1e-5));
59+
}
60+
}
61+
62+
TEST_CASE("Weights are finite and positive", "[generate]") {
63+
PhaseSpace phsp(kMotherMass, kMasses, kNEvents);
64+
phsp.Generate(Vector4R(kMotherMass, 0.0, 0.0, 0.0));
65+
66+
mcbooster::RealVector_h weights = phsp.GetWeights();
67+
REQUIRE(weights.size() == static_cast<size_t>(kNEvents));
68+
69+
GReal_t sum = 0.0;
70+
for(size_t i = 0; i < weights.size(); i++) {
71+
INFO("event " << i);
72+
REQUIRE(std::isfinite(weights[i]));
73+
REQUIRE(weights[i] > 0.0);
74+
sum += weights[i];
75+
}
76+
CHECK(sum > 0.0);
77+
}
78+
79+
TEST_CASE("Unweighting keeps a subset of events", "[generate]") {
80+
PhaseSpace phsp(kMotherMass, kMasses, kNEvents);
81+
phsp.Generate(Vector4R(kMotherMass, 0.0, 0.0, 0.0));
82+
83+
mcbooster::GULong_t kept = phsp.Unweight();
84+
CHECK(kept > 0);
85+
CHECK(kept <= static_cast<mcbooster::GULong_t>(kNEvents));
86+
}

0 commit comments

Comments
 (0)