23.5 Avoiding Hard-Coded Values
Table of Contents
Why Hard-Coded Values Are a Problem
In ROOT analysis code, it is very tempting to write numerical constants directly into your macros. For instance, you might type a mass window, a number of bins, or a file path right where it is needed in the code. This is called “hard-coding” a value.
A small example looks like this:
TH1F *hMass = new TH1F("hMass", "Invariant mass", 100, 0.0, 2.0);
tree->Draw("mass >> hMass", "mass > 0.8 && mass < 1.0");Here, the histogram range and the selection cut are hard-coded. At first, this seems harmless, but as the analysis grows, many such numbers spread across your macros. Changing them later becomes error-prone and time consuming. It is also hard for someone else to understand which values are important physics choices and which are just technical details.
The goal of this chapter is to show how to avoid hard-coded values in ROOT analysis code, and to introduce more robust and maintainable alternatives.
Important rule: Avoid scattering “magic numbers” and literal strings across your ROOT analysis code. Centralize important values and make them configurable.
Typical “Magic Numbers” in ROOT Analyses
In ROOT based data analysis, hard-coded values usually appear in similar places. Recognizing these patterns is the first step toward replacing them.
Common examples include:
Selection cuts. Values such as pt > 0.5, nHits >= 10, or abs(eta) < 2.5 often appear directly in TTree::Draw strings, Filter expressions, or if statements inside event loops.
Histogram definitions. Numbers of bins and axis ranges like TH1F("hE", "Energy", 200, 0.0, 10.0) are often typed inline, and sometimes duplicated in multiple places for the same observable.
File paths. Input and output files such as "data/run123.root" or "results/histograms.root" are frequently embedded directly in the code. When directory structures change, all of these strings must be found and updated.
Physics constants and calibration parameters. Values such as particle masses, detector calibration factors, or timing offsets might be typed as literal numbers instead of read from a dedicated source.
Bin edges. When analyses use variable bin widths, long arrays of numbers can end up hard-coded in macros, which makes visual inspection and modification difficult.
Table of typical magic numbers:
| Category | Example |
|---|---|
| Kinematic cuts | pt > 0.5, abs(eta) < 2.4 |
| Quality cuts | chi2 < 5.0, nHits >= 12 |
| Histogram settings | 100 bins, 0.0 to 200.0 GeV |
| File paths | "data/input.root", "plots/output.pdf" |
| Calibration factors | energy *= 1.05; |
| Physics constants | 0.13957 (pion mass), 3.0e8 (speed of light) |
When such values are repeated or hidden inside strings, they become easy to forget, misinterpret, or misuse.
Using Constants and Named Variables
The first and simplest method to avoid hard-coded values is to replace literal numbers with named variables or constants. This directly improves readability and reduces duplication.
Named constants in C++
For values that are fixed for a given version of your analysis, you can use const variables:
const int kNBinsMass = 100;
const double kMassMin = 0.0;
const double kMassMax = 2.0;
const double kMassCutLow = 0.8;
const double kMassCutHigh = 1.0;
TH1F *hMass = new TH1F("hMass", "Invariant mass",
kNBinsMass, kMassMin, kMassMax);
This approach has several advantages. The purpose of each number is explicit, changing binning or ranges affects all usages at once, and names can follow a consistent pattern. A common C++ style is to use a prefix like k for constants.
For physics constants, define them once, for example in a small header file that you include:
// PhysicsConstants.h
#ifndef PHYSICSCONSTANTS_H
#define PHYSICSCONSTANTS_H
const double kMassPionGeV = 0.13957;
const double kMassKaonGeV = 0.49367;
#endifThen include them in your macros or source files:
#include "PhysicsConstants.h"
// Use kMassPionGeV instead of 0.13957This makes the physics content of your analysis clear and keeps important values in one place.
Important rule: Use named const variables or constants instead of repeating literal numbers. This is especially important for physics parameters, histogram definitions, and selection thresholds.
Named variables for run-time configuration
Sometimes a value is not fixed forever, but you still want to avoid search and replace throughout the code. In that case, use regular variables that can be set at the beginning of your macro or in a configuration function.
int nBinsEnergy = 200;
double eMin = 0.0;
double eMax = 10.0;
TH1F *hE = new TH1F("hE", "Energy", nBinsEnergy, eMin, eMax);Later, if you want to change the binning, you only need to modify these initial definitions.
Building Selection Strings Systematically
ROOT often uses selection cuts passed as strings, for example in TTree::Draw and in various RDataFrame methods. These strings are a common place where hard-coded values hide.
Instead of writing:
tree->Draw("mass >> hMass", "pt > 0.5 && abs(eta) < 2.4");you can construct the selection string using named values:
const double kPtMin = 0.5;
const double kAbsEtaMax = 2.4;
const double kMassWindowL = 0.8;
const double kMassWindowH = 1.0;
TString baseCut;
baseCut.Form("pt > %f && abs(eta) < %f", kPtMin, kAbsEtaMax);
TString massCut;
massCut.Form("mass > %f && mass < %f", kMassWindowL, kMassWindowH);
TString fullCut = baseCut + " && " + massCut;
tree->Draw("mass >> hMass", fullCut);This pattern isolates the actual numbers and prepares you for further extensions, for instance reading these values from a configuration file instead of defining them directly in the macro.
With RDataFrame, you can use C++ variables directly, without constructing strings manually:
const double kPtMin = 0.5;
const double kAbsEtaMax = 2.4;
ROOT::RDataFrame df("tree", "data.root");
auto dfFiltered = df.Filter(
[=](double pt, double eta) {
return pt > kPtMin && std::abs(eta) < kAbsEtaMax;
},
{"pt", "eta"}
);
Here the filter is a C++ lambda function. The values kPtMin and kAbsEtaMax are no longer hidden inside a string, which helps both readability and maintainability.
Important rule: Avoid embedding important physics cuts as anonymous numbers inside selection strings. Instead, use named constants and systematic string construction or C++ expressions.
Centralizing Configuration Parameters
As soon as your analysis has more than a few tunable values, it is useful to gather them into a dedicated place. Instead of spreading constants throughout many macros, group them into a configuration section, struct, or file.
A simple configuration struct
You can define a struct that contains all adjustable parameters for a particular analysis:
struct AnalysisConfig {
int nBinsMass = 100;
double massMin = 0.0;
double massMax = 2.0;
double ptMin = 0.5;
double absEtaMax = 2.4;
double signalMassWindowLow = 0.8;
double signalMassWindowHigh = 1.0;
TString inputFileName = "data/input.root";
TString outputFileName = "results/output.root";
};
Then pass an AnalysisConfig object to your analysis functions:
void RunAnalysis(const AnalysisConfig &cfg) {
TFile inputFile(cfg.inputFileName, "READ");
TTree *tree = (TTree*)inputFile.Get("tree");
TH1F *hMass = new TH1F("hMass", "Invariant mass",
cfg.nBinsMass, cfg.massMin, cfg.massMax);
TString cut;
cut.Form("pt > %f && abs(eta) < %f && mass > %f && mass < %f",
cfg.ptMin, cfg.absEtaMax,
cfg.signalMassWindowLow, cfg.signalMassWindowHigh);
tree->Draw("mass >> hMass", cut);
}
All key parameters now reside in one place. When you need to run a different configuration, you simply modify or create another instance of AnalysisConfig.
Using configuration files
For more flexibility, you can store configuration in an external file instead of compiling it into the code. Even a plain text file with key=value pairs can be useful. You can read it at the beginning of the macro and fill an AnalysisConfig object.
For example, a simple configuration text:
nBinsMass = 120
massMin = 0.5
massMax = 1.5
ptMin = 0.6
absEtaMax = 2.1
inputFileName = data/new_sample.root
outputFileName = results/new_output.rootYou can parse these values with standard C++ or with any simple parsing logic. The important point for this chapter is that the values are no longer scattered through the analysis code, and it becomes easy to run the same code with different parameters.
In some projects you may use more sophisticated configuration formats such as JSON, YAML, or XML, possibly parsed via external libraries. The principle remains the same: configuration is separated from the analysis logic.
Important rule: Centralize configuration such as cuts, binning, and file paths. Keep analysis logic separate from the choice of specific parameter values.
Avoiding Hard-Coded File and Directory Names
File and directory paths are another common source of hard-coded values. This often shows up in analysis code as:
TFile *inputFile = TFile::Open("data/run001.root");
TFile *outputFile = new TFile("output/hists_run001.root", "RECREATE");If you change directory names, move to a new computer, or run over many input files, you must manually adjust the code each time.
Instead, treat file paths as configuration parameters. Use variables or configuration structs:
TString inputDir = "data/";
TString outputDir = "results/";
TString runName = "run001";
TString inputFileName = inputDir + runName + ".root";
TString outputFileName = outputDir + "hists_" + runName + ".root";
TFile *inputFile = TFile::Open(inputFileName);
TFile *outputFile = new TFile(outputFileName, "RECREATE");If you have many runs, you can loop over a list of run names instead of editing the code each time.
When your analysis is more advanced, you might pass the input and output paths as command line arguments to compiled programs or macros, or read them from a configuration file. That is discussed in more detail in related chapters about organizing projects and writing reusable macros.
Making Plot Styling Configurable
Styling choices in ROOT, such as colors, marker styles, and line widths, are also often hard-coded. For instance:
hMass->SetLineColor(kRed);
hMass->SetLineWidth(2);
hMass->SetMarkerStyle(20);While these are not physics parameters, they are still decisions that you may want to adjust without editing many lines.
You can centralize plotting styles in helper functions or small configuration objects:
struct StyleConfig {
Color_t signalColor = kRed;
Width_t lineWidth = 2;
Style_t markerStyle = 20;
};
void ApplySignalStyle(TH1 *h, const StyleConfig &style) {
h->SetLineColor(style.signalColor);
h->SetLineWidth(style.lineWidth);
h->SetMarkerStyle(style.markerStyle);
}Then in your analysis:
StyleConfig style;
TH1F *hMass = new TH1F("hMass", "Invariant mass", 100, 0.0, 2.0);
ApplySignalStyle(hMass, style);With this approach, you can adjust styling in a single place, and you can even have different style configurations for publications, internal notes, or quick debugging plots.
Summary of Good Practices
Avoiding hard-coded values in ROOT analysis code is mainly about clarity, maintainability, and flexibility. By using constants, named variables, centralized configuration, and systematic construction of selection expressions, you can reduce errors and make your analysis easier to understand and modify.
The key ideas in this chapter are:
Use named constants instead of anonymous numbers, especially for physics quantities, cuts, and histogram definitions.
Do not hide important values inside selection strings. Construct them from variables or use C++ expressions where possible.
Collect configuration parameters such as binning, cuts, and file paths into dedicated structs or configuration files, separated from the analysis logic.
Treat file names, directory paths, and plot styles as configurable options, not as fixed properties of the code.
These habits work together with other practices from this section of the course to help you write ROOT analysis code that is robust, reusable, and easier for collaborators and for your future self to work with.
Views: 14
KAHIBARO