This repository contains some common WCTE analysis tools as a python package, the tools for processing the data are stored here as well as tools for analysing processed data. If you follow the installation instructions you can use these tools running import analysis_tools.
Examples for using some of these tools can be found in the analysis_examples directory and are also listed https://wcte.hyperk.ca/wg/simulation-and-analysis/data-analysis-1 .
The scripts used in the WCTE data production are included in the scripts repository.
The layout of this repository is as follows:
analysis_tools/
├── analysis_examples/ #contains examples on using the analysis tools
├── analysis_tools/ #contains the python package for analysis - can be installed using instructions below
├── cpp_merger/ #contains the cpp merging code for the data production
├── scripts/ #contains the python scripts for running the production
├── data/ #contains data needed for the package functions
├── extern/ #contains external packages linked to this repository as submodules
├── include/ #contains data used by the beam analysis scripts
├── notebooks/ #contains older example notebooks (from August 2025 workshop)
Installation:
git clone <git repo location>
cd analysis_tools
pip install -e .
The -e flag allows you to edit the package
If using on lxplus you will need to setup this in a python virtual environment
Some of the analysis examples use external packages linked to this repository as submodules. To install the external packages run:
git submodule update --init --recursive
Since some of the modules are private you need to set up ssh access to github for this to work - see https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account . Note you can check your ssh is working by running ssh -T git@github.com.
Main branch on WCTE/analysis_tools is protected - please open a pull request (either from your own branch or fork) to push changes to main branch
Waveform processing contains a copy of the CFD used in the test stand repository
WaveformProcessingTeststand.cfd_teststand_method() processes the CFD using that method returning
the charge and the time for a pulse in that waveform - including non-linearity corrections
for both
Additionally the same CFD and charge calculation method used online by the mPMT is included in WaveformProcessingmPMT the versions to run on single waveforms and vectorised versions to run on arrays of waveforms are given
Finds pulses in the waveforms using the same method as run online on the mPMT.
do_pulse_finding is a slower version processing one waveform at a time and returning a list of found pulses
do_pulse_finding_vect is a mostly vectorised version, returning a list of lists of found pulses for each waveform
do_pulse_finding_fast is a more optimised, fully-vectorised version that returns results as arrays of rows (waveforms)
and columns (samples) of all pulses found in all waveforms
Interfaces with the calibration database see more instruction here
https://wcte.hyperk.ca/documents/calibration-db-apis/v1-api-endpoints-documentation
Currently processed for the test database - to be updated when the production database is ready. The authentication requires a credential text file ./.wctecaldb.credential to be in the current working directory - more details in the database interface above
PMTMapping is a class containing the mapping of the WCTE PMTs slot and position ids (position in the detector) to the channel and mPMT card ids (electronics numbering) and vice versa. This is generally not needed as processed data already has the mPMTs mapped to their position and slot numbers. Usage:
mapping = PMTMapping()
mapping.get_slot_pmt_pos_from_card_pmt_chan(card_id,pmt_channel)
returns the slot and pmt position and
mapping.get_card_pmt_chan_from_slot_pmt_pos(slot_id,pmt_position)
returns the card and channel
The mapping json is located in the package
Class to load PMT positions, directions and calculate time of flight.
The earliest version of production script for production v_0 which added timing constants to self-trigger data
Self-trigger data production v 0_5 (see https://wcte.hyperk.ca/wg/simulation-and-analysis/data-production-2) for self-trigger data which includes timing constants and data quality flags
Production v_1 processes both self-trigger and hardware-trigger data through a multi-step pipeline.
A top-level orchestrator script (run_pipeline.py) reads run metadata from the run info JSON,
determines the trigger type and beam analysis mode automatically, and calls the appropriate
pipeline scripts. Intermediate outputs are written to step-specific subdirectories under
<output_base>/<run_number>/.
The recommended entry point for production. For a given run it:
- Reads trigger type and beam analysis mode from the run info JSON via
get_run_info(). - Runs the appropriate trigger-specific pipeline (
run_hw_trigger_pipeline.pyorrun_self_trigger_pipeline.py). - Conditionally runs beam monitor PID analysis (
WCTE_beam_analysis.py) depending on the beam configuration. - Runs the T5 beam monitor analysis
Usage:
python run_pipeline.py \
-r <run_number> \
-i <input_file(s)> \
-o <output_base_dir> \
[--steps wf calibrate dq beam] \
[--debug]--steps controls which parts of the pipeline run. If not provided all steps run by default.
If wf or calibrate is specified, the corresponding downstream steps are also re-run
(e.g. --steps calibrate also runs dq).
The decision to run VME beam processing is determined automatically from the run info JSON. If the run is in a tagged gamma configuration or downstream ACTs (act3–5) are missing, the VME processing will not run.
These can also be run standalone (e.g. to reprocess a single step).
Orchestrates self-trigger data processing through two steps:
Step 1 (calibrate) : calibrate_hits.py → calibrated_hits/
Step 2 (dq) : self_trigger_dq_flags.py → dq_flags/
Usage:
python run_self_trigger_pipeline.py \
-i <input_file(s)> -r <run_number> -o <output_base_dir> [--steps calibrate dq] [--debug]Orchestrates hardware-trigger data processing through three steps:
Step 1 (wf) : hw_trigger_wf_processing.py → processed_waveforms/
Step 2 (calibrate) : calibrate_hits.py → calibrated_hits/
Step 3 (dq) : hw_trigger_dq_flags.py → dq_flags/
Usage:
python run_hw_trigger_pipeline.py \
-i <input_file(s)> -r <run_number> -o <output_base_dir> [--steps wf calibrate dq] [--debug]Both pipeline scripts support --steps (see above). The dependency expansion is applied
automatically — requesting calibrate will also run dq.
Runs pulse finding and charge/time determination on hardware-trigger raw waveform data.
Reads WCTEReadoutWindows trees and writes a ProcessedWaveforms ROOT tree.
Applies timing constants from the calibration database to hit times.
Works on both self-trigger (WCTEReadoutWindows) and waveform-processed (ProcessedWaveforms) input files.
Writes a CalibratedHits ROOT tree and a Configuration tree recording the git hash,
timing constant revision, and list of PMTs with timing constants.
Applies data quality flags for self-trigger runs. Determines the good channel list from the intersection of slow-control stable channels and channels with calibration constants. Applies trigger-level bitmask flags for slow-control excluded periods and the 67 ms periodic issue. Applies hit-level bitmask flags for missing timing constants and slow-control unstable channels.
Applies data quality flags for hardware-trigger runs. In addition to the channel-level flags above, applies trigger-level flags for missing waveforms, missing trigger signals, and mismatched waveform lengths.
<output_base>/<run_number>/
processed_waveforms/ (hw-trigger only)
<base>_processed_waveforms.root
calibrated_hits/
<base>_calibrated_hits.root
dq_flags/
<base>_[self|hw]_trigger_dq_flags.root
<base>_[self|hw]_trigger_dq_flags_status.json
beam_data/ (normal beam runs only)
beam_analysis_output_R<run_number>.root
Common functions used across all production scripts are in analysis_tools/production_utils.py:
get_run_info— reads trigger type and beam analysis mode from the run info JSONget_git_descriptor— git provenance for output Configuration treesfile_sha256— hash of the slow-control input file usedget_run_database_data,get_stable_mpmt_list_slow_control— slow-control data accessget_slow_control_trigger_mask,get_67ms_mask— trigger-level DQ masksslot_pos_from_card_chan_list— PMT channel mapping
scripts/WCTE_beam_analysis.py performs particle identification (PID) and momentum estimation for beamline runs, using the VME beam-monitor detectors (T0, T1, T4, T5/TOF, and the ACT Cherenkov counters). It calibrates the ACT PMTs (1 p.e. calibration), tags particle species event-by-event, and estimates the momentum of each particle species both just after the CERN beam pipe and at the WCTE tank window.
It is a template PID: reasonable defaults are used throughout, but the selection is not tuned or optimised for any specific physics analysis. Treat it as a starting point to build your own beam PID on top of, not as a final analysis tool.
The logic lives in BeamAnalysis (analysis_tools/beam_monitors_pid.py); the script is a thin CLI wrapper around it.
| Detector | Channels | Role |
|---|---|---|
| T0 | 0–3 | Upstream timing reference (start of TOF) |
| T1 | 4–7 | Downstream-of-T0 timing (T0–T1 TOF used for proton/deuteron/helium-3/triton tagging) |
| Hole counter (HC) | 9–10 | Vetoes triggers with an off-axis/halo hit |
| ACT0–2 ("e-veto") | 12–17 | Cherenkov counters used to tag electrons/positrons |
| ACT3–5 ("tagger") | 18–23 | Cherenkov counters used to separate muons/pions (ACT5 optional — some runs don't have it) |
| T4 | 42–43 | Further downstream timing reference |
| T5 / TOF bars | 48–63 (8 bars × 2 SiPMs) | End-of-line scintillator bars; also used as a "reached the tank" requirement |
| Muon tagger | subset of ACT3–5 channels | Auxiliary muon tag, used when the ACT3-5 charge distribution doesn't show a clean muon/pion minimum |
Reference TDC channels 31 and 46 are used to correct all other times.
Run the script from the scripts/ directory — it resolves two paths relative to its own location:
../analysis_tools(the package, viasys.path.append("../"))../include/1pe_calibration.json(ACT PMT gain/pedestal calibration)
It also reads, from fixed EOS paths (not configurable via CLI):
/eos/experiment/wcte/configuration/run_info/google_sheet_beam_data.json— run metadata (beam momentum, ACT refractive indices, beam configuration, whether ACT5 is installed, etc.), compiled by Laurence from the Google sheet/eos/experiment/wcte/configuration/run_info/beamline_equipment_settings.csv— collimator/acceptance jaw settings
Input ROOT file(s) must contain a WCTEReadoutWindows tree with the beamline_pmt_tdc_times, beamline_pmt_tdc_ids, beamline_pmt_qdc_charges, beamline_pmt_qdc_ids, and spill_counter branches — i.e. VME-merged offline data, e.g. from /eos/experiment/wcte/data/2025_commissioning/offline_data_vme_match/.
The run must satisfy preconditions read from the run-info JSON, or the script raises and exits:
- ACT0 must be in the beamline; lead glass must be out of the beamline.
- Unless
--no_actsis passed: ACT0/1/2 must all share the same refractive index, ACT3 and ACT4 must be in the beamline, and ACT3/4(/5) must share the same refractive index.
python3 WCTE_beam_analysis.py -r <run_number> -i <input_file(s)> -o <output_dir> [options]| Flag | Required | Description |
|---|---|---|
-r, --run_number |
yes | Run number; must appear as R<run_number> in every input filename |
-i, --input_files |
yes | One or more WCTEReadoutWindows ROOT files (space-separated) |
-o, --output_dir |
yes | Directory for the output ROOT file and PDF (created if missing) |
--debug |
no | Only process the first 5000 events |
--no_acts |
no | Minimum-bias mode: relax the requirement that upstream/downstream ACTs share a refractive index (for runs without a full ACT lineup) |
--is_kaon_run |
no | Relabels identified muons as kaons, uses kaon dE/dx tables for their momentum estimate, and records the flag in run_info |
--use_emupi_TOF_separation |
no | Use time-of-flight instead of ACT3-5 charge to separate muons/pions (needed below the ~300 MeV/c muon Cherenkov threshold). Automatically forced on for runs before 1441 with |momentum| < 300 MeV/c |
Example:
python3 WCTE_beam_analysis.py -r 2285 \
-i /eos/experiment/wcte/data/2025_commissioning/offline_data_vme_match/WCTE_offline_R2285S0_VME_matched.root \
-o ./beam_data_outMultiple input files for the same run can be passed at once; each is processed independently and produces its own pair of output files.
For each input file, in order:
- 1 p.e. calibration (
adjust_1pe_calibration) — refines the ACT PMT gain calibration. - Proton/deuteron/helium-3/triton tagging (
tag_protons_TOF) — via the T0–T1 time of flight, using theoretical TOF cuts computed from the nominal beam momentum. Done first to avoid double-counting these particles as something else. Protons are only tagged above 250 MeV/c; deuterons/helium-3/tritons only above 350 MeV/c. - Electron/positron tagging (
tag_electrons_ACT02) — from a charge threshold on the ACT0–2 ("e-veto") sum. - Sanity-check plots (
plot_ACT35_left_vs_right,plot_ACT02_left_vs_right) — visualise that the proton/electron removal looks correct. - Muon/pion tagging (
tag_muons_pions_ACT35) — from the ACT3–5 ("tagger") charge, or from TOF instead if--use_emupi_TOF_separationapplies. Falls back to an auxiliary muon-tagger cut if the ACT3-5 muon/pion populations don't show a clean minimum. - TOF offset correction (
measure_particle_TOF) — corrects for cable-length-type offsets so later momentum estimates are meaningful. - Momentum estimation (
estimate_particle_momentum) — mean momentum per particle species and per-trigger (not output to production 1), both just after the CERN beam pipe and at the WCTE tank window. The per-trigger error is taken from the electron TOF resolution (std of a Gaussian fit), so it is large for slow/heavy particles. - POT-normalised yield plot (
plot_number_particles_per_POT). - Event-quality plot (
plot_event_quality_bitmask). - ROOT output (
output_to_root).
If no triggers pass the basic event-quality requirements, the analysis is skipped for that file (the PDF is still closed cleanly and a message is printed).
For each input file <base>.root, two files are written to -o:
-
<base>_PID.pdf— every diagnostic plot produced above (ACT/TOF distributions, cut lines, per-particle momentum plots, event-quality histograms, etc.). Always check these to confirm the selection behaved sensibly for that run. -
<base>_beam_analysis.root— three trees:beam_analysis— one entry per trigger:event_id,spill_number, per-channel TDC-corrected times and QDC charges/p.e. for T0/T1/T4/ACT0-5/muon tagger,tof_t0t1and other TOF combinations,evt_quality_bitmask,digi_issues_bitmask.evt_quality_bitmaskbits:0T0/T1 TDC missing,1T4 TDC missing,2T5 TDC missing,3hole-counter hit,4T4 QDC missing,5more than one T5 bar hit.digi_issues_bitmaskbits:0QDC failure,1missing digitiser times.
run_info— one entry: run number, nominal beam momentum, ACT refractive indices, whether ACT5 is present, jaw positions,is_kaon_runflag.scalar_results— one entry: the cut values used (TOF/ACT cut lines), number of triggers by particle species, per-species TOF and momentum means/errors (near the CERN pipe and at the WCTE window), and nominal (theoretical) TOF per species.
Per-trigger PID/momentum branches (
is_muon,is_electron, ...,final_momentum, ...) are only written whenis_beam_paper_analysis=True, which this CLI never sets — so only the aggregate PID counts and per-species momentum summaries inscalar_resultsare available from a standalone run. Note also thatis_kept(whether a trigger passed the basic beam requirements) is used internally to count total triggers but is dropped before writing, so it is not present in the outputbeam_analysistree.
- Helium-3 nuclei are tagged by TOF the same way as protons/deuterons/tritons, but the magnetic-rigidity/charge correction used for their theoretical TOF cut has not been as thoroughly validated — treat with care.
- No cut is applied on the T5 (TOF detector) total charge; it is saved in the output for reference only.
- Per-trigger momentum estimates carry large errors for slow/heavy particles, since the TOF resolution is derived from the (fast) electron peak.
- See the beam PID write-up and collaboration meeting slides below for the full physics rationale.
- The event bitmask is != 0 if:
- The TOF is too short (< 10ns)
- The particle does not reach T5 (though this is also checked more directly by Frantisek's BRB analysis)
- The TDC or QDC failed
Email Alie if you have any questions.