CLAS12 beam-spot analysis, ported from the Java/groot BeamSpot (S. Stepanyan,
CLAS12 Note 2020-003). The analysis is headless: it reads CLAS12 DSTs with
the hipo library, fits the target-window
position versus φ in θ bins, extracts the beam spot (x0, y0, z0), and writes a
single self-describing ROOT file plus the physics/CCDB text deliverables. All
plotting lives in scripts/plot_beamspot.py.
include/beam_spot.h data records (config/histograms/analysis) + free-function API
include/cli.h header-only, argparse-style command-line parser
src/beam_spot.cxx fill + analyze + I/O (free functions, no graphics)
src/main.cxx CLI + functional pipeline
external/hipo4/ vendored hipo library (built from source)
scripts/plot_beamspot.py plotting (uproot + matplotlib + mplhep)
The analysis is written in a functional style: plain data records flow through free functions, with no stateful analysis class.
beamspot::config cfg{ {10,...,30}, fit_range, target_z, bins_per_sector };
beamspot::histograms h = beamspot::fill_dst(files, cfg, n_threads); // or merge_files(...)
beamspot::analysis a = beamspot::analyze(std::move(h), cfg);
beamspot::beam_spot_result r = beamspot::results(a);
beamspot::write_root(a, path); /* + write_results_txt / write_ccdb_table */Parallel filling spawns one worker process per file group (-j N), each
running its own single-threaded hipo::chain, then merges their partial
histograms with TH1::Add (bit-identical to a single-pass fill). On the JLab
farm this beats in-process threads, which contend on shared ROOT/heap state. The
parent shows one aggregate progress bar fed by a shared-memory counter per
worker. The per-track cut/bin logic is a single higher-order for_each_hit(...)
shared by the sequential and multi-process fill paths.
Requires ROOT (driven via root-config), plus lz4 and fmt (for the
vendored hipo). hipo itself is built from source under external/hipo4/.
meson setup build
meson compile -C buildThe project pins C++17 to match the standard ROOT 6.38 was built with
(root-config --cflags reports -std=c++17). If your ROOT reports a different
standard, change cpp_std in meson.build.
- lz4 is found via pkg-config and enabled with
-D__LZ4__(CLAS12 DSTs are LZ4-compressed — without it hipo cannot read them). - fmt is found via pkg-config/cmake if available, otherwise used header-only
from
/opt/homebrew,/usr/local, or/opt/local. Adjust the prefix list inmeson.buildif fmt lives elsewhere.
./build/beamspot -o run_out path/to/rec_clas_*.hipo| Short | Long | Default | Meaning |
|---|---|---|---|
-o |
--output-dir |
beamspot_out |
output folder (created if missing) |
-p |
--prefix |
BeamSpot |
filename prefix |
-r |
--fit-range |
1.0 |
fit-range scale factor |
-z |
--target-z |
19.6 |
nominal target/foil Z (cm) |
-n |
--bins-per-sector |
10 |
φ bins per sector |
-j |
--jobs |
0 |
worker processes for the fill (0 = all cores) |
--z-min / --z-max |
15 / 25 |
z range (cm) of the z-vs-φ map that drives the fits | |
--z1d-min / --z1d-max |
15 / 25 |
z range (cm) of the 1D z-vertex QA histogram | |
-m |
--merge-histograms |
off | treat inputs as ROOT results files and Add() them |
--dry-run |
off | run the analysis but write no files | |
-V |
--version |
print version | |
-h |
--help |
usage |
Outputs (under --output-dir, with --prefix):
BeamSpot_results.root, BeamSpot_results.txt, BeamSpot_ccdb_table.txt.
Merge multiple runs without re-filling:
./build/beamspot -m -o merged run_a/BeamSpot_results.root run_b/BeamSpot_results.root(or just hadd the histograms at the shell).
scripts/run_all_runs.py runs the full chain (C++
fill + Python plots) over every numeric run directory under an input folder,
writing each run's output into its own sub-directory and then plotting the
fitted x0/y0 versus run number.
python scripts/run_all_runs.py \
--input-dir /volatile/clas12/rg-l/production/rich_060526/calib/recon \
--output-dir beamspot_runs -j 10For each numeric directory <run>/ it writes beamspot_runs/<run>/ (the usual
*_results.root + per-run figures), then beamspot_runs/beamspot_xy_vs_run.png
and beamspot_xy_vs_run.txt summarising x0/y0 across runs. Useful flags:
--pattern '*.hipo' (DST glob inside each run dir), --skip-existing (reuse
already-filled runs), --plot-only (skip the C++ fill entirely and just
(re)plot + rebuild the summary from existing *_results.root), --no-plot
(skip per-run figures), --runs 022083 022598 (subset), and the
-z/--z-min/--z-max/--z1d-min/--z1d-max pass-throughs.
scripts/compare_cpp_java.py runs this C++ code
and the original Java (java_imp/run.sh) over the same file list for one
run and diffs the fitted Z, R, Phi0, X, Y.
python scripts/compare_cpp_java.py \
/volatile/clas12/rg-l/production/rich_060526/calib/recon/022083 \
--max-files 10 -z 19.6It writes compare_out/cpp/, compare_out/java/, and three levels of
comparison:
- final constants —
compare_cpp_java.{txt,png}(value/error/diff/diff-over-σ per parameter); - filled histograms —
compare_histograms.{txt,png}: bin-by-bin diff of eachh2_z_phi_<i>(C++results.rootvs JavaBeamSpot_histos.txt). If the per-bin diffs are all zero the fills are identical and every remaining difference is fitter-only; the.pngshows the residual map per θ bin; - per-θ points —
compare_points.{txt,png}: the extractedz0/r0/phi0/x0/y0for each θ bin before thepol0average (the C++pointsTTree vs aBeamSpot_points.txtthe Java now writes fromanalyze()).
Two things it handles automatically:
- z binning. Java derives its z-vs-φ map range from the target z as
[(int)(Z−4.4), (int)(Z+15.6)]and fixes the 1D histogram at[−20, 50]. The script reproduces that (matching Java's float→int truncation) and feeds the exact values to the C++--z-min/--z-max/--z1d-min/--z1d-max, so both sides fill identical histograms. - Java needs a display. The Java only writes its results from its
graphics path, so the script wraps it in
xvfb-runwhen$DISPLAYis unset and terminates the lingering JVM once the results file appears. It needs$CLAS12DIR(COATJAVA) set, exactly likejava_imp/run.sh.
The fill loop reads CLAS12 DSTs through a hipo::chain. With -j 1 it runs a
single sequential pass; with -j N (or -j 0 = all cores) it forks N worker
processes, each filling its own histograms from a balanced subset of the files,
which are merged with TH1::Add before the fit — the result is bit-identical to
the single-process run.
Measured on one 9.1 GB DST (598,738 events, run 22083), 12-core machine:
| implementation | event rate | wall time |
|---|---|---|
| Java (groot) | 56 kHz | 10.8 s |
C++ -j 1 |
100 kHz | 7.0 s |
C++ -j 0 (12 thr) |
146 kHz | 4.4 s |
C++ is ~1.8× faster than Java single-threaded and ~2.6× with all cores. Scaling is sublinear because the work is dominated by disk I/O and LZ4 decompression rather than the (cheap) per-track cuts and histogram fills.
Plotting uses matplotlib + mplhep (no PyROOT). Everything is read with
uproot; the fit curves are redrawn from the stored parameter trees
(modulation, slice_fits) rather than from TF1s, so PyROOT is never needed.
python3 -m venv .venv && .venv/bin/pip install -r scripts/requirements.txt
.venv/bin/python scripts/plot_beamspot.py -o run_out # reads *_results.root
.venv/bin/python scripts/plot_beamspot.py -o run_out -f pdf # PDF instead of PNGFigures written into the output folder:
<prefix>_summary.png— beam spot (x₀, y₀, z₀, φ₀, r₀) vs θ with thepol0constants (the headline figure), in mplhep ROOT style.<prefix>_bin<i>.png— z-vs-φ map per θ bin with the fitted target-z points and thez₀ − A·cos(φ − φ₀)modulation curve overlaid.<prefix>_slices_bin<i>.png— the accepted z slices in a θ bin, each with its gaussian + linear-background fit.
BeamSpot_results.root contains:
h1_z,h1_phi;h2_z_phi_<i>(z vs φ per θ bin);g_results_<i>with thez0 − A·cos(φ − φ0)modulationTF1attached;slices/bin_<i>/h_slice_*z slices, each with its gaussian+linear fit;gZ/gR/gP/gX/gYsummary graphs with theirpol0fits;- a flat
pointsTTree(per-θ values + errors) and a single-rowfinalTTree(the fitted beam-spot constants), both uproot-readable.
On some macOS setups ROOT's Cling fails to JIT with:
.../cling/std_darwin.modulemap: error: header '__configuration/experimental.h' not found
This happens when Cling resolves libc++ from the CommandLineTools SDK (whose older libc++ lacks that header) while ROOT's modulemap expects the Xcode SDK libc++. It breaks all ROOT fitting/JIT/PyROOT — not just this tool. Point Cling at the Xcode libc++:
export ROOT_INCLUDE_PATH=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1(Permanent fixes: update/reinstall CommandLineTools, or rebuild ROOT.)