Bugs and Hacks

Over the years, I faced a lot of weird and interesting bugs, figured out neat workarounds, and picked up a bunch of programming tricks. Instead of keeping them scattered or forgetting them entirely, I decided to put them all in one place. This is basically my personal cheat sheet. I am sure I forgot to include a large part of it here, but still this page might be useful for somebody.

Regarding the NanoAOD analysis framework

Here are some issues I faced/fixed while working with the TSelector based NanoAOD analysis frameworks, histograms etc.

Avoiding segmentation fault while booking histograms

This is a common issue people typically face when the array of histogram is initialized with a fixed size in the header file, but the actual number of histograms booked in the source file is larger than that. The code compiles perfectly, but during runtime, it tries to access memory beyond the allocated array size, leading to a segmentation fault, without any clear error message.

To avoid this, I prefer to declare a vector of pointers to histograms instead of a fixed-size array as shown below.


struct Hists {
  //Histograms are declared here as a collection of vectors.
  //These need to be dynamicaly expanded in BookHistograms() function.
  vector<TH1F *> hist;
  vector<TH1F *> dnn;
};
            

Then, in the BookHistograms() function, I can dynamically resize these vectors and add histograms to them as needed, without worrying about exceeding a fixed size.


h.hist.resize(3);
//initiate 3 histograms here with index 0,1,2
for(int i = 0; i < (int)h.hist.size(); i++) h.hist[i]->Sumw2();
              

This is a safer approach, since adding a new histogram only requires a change in the BookHistograms() function. There is no need to find and update the array size in the header file. The loop can also use h.hist.size() instead of a fixed value like 3, so it updates automatically when new histograms are added.


The missing pointer reference bug

This was one of those bugs where everything looked fine, but the code was quietly doing something different from what I expected. I wanted to avoid sorting every physics object array separately, so I wrote a function that takes the entire object and sorts it by decreasing \(p_T\) as follows.


void nanoAna::SortPt(vector<Particle> &objarr){
  // Sort an object array in the decreasing order of pT
  for(int i=0; i<(int)objarr.size()-1; i++){
    for(int j=i+1; j<(int)objarr.size(); j++){
      if( objarr[i].v.Pt() < objarr[j].v.Pt() ) swap(objarr.at(i),objarr.at(j));
    }
  }
}
            

The catch was the tiny & in the argument. I had forgotten it in the earlier version of the function. Without the reference, objarr was a copy of the original object. The sorting algorithm itself worked perfectly, but it was only modifying this copy. Once the function finished, the copy went out of scope and was destroyed, while the original object remained unchanged. The worst part was that there was no error. The code compiled and ran normally, but the objects were simply not being sorted. It took me quite some time to figure out what was happening.

The simple fix was to pass the object by reference with & . Another option would be to change the function to return the modified copy as a vector<Particle> and assign the returned object back at the calling point.

Lesson learned: Be careful when passing objects or values to void functions. Use a reference or pointer if the original needs to be modified.

Plotting aesthetics

For aesthetics, I use the cmsstyle package and Python scripts to generate standardized plots. The official CMS plotting guidelines page contains some basic examples of cmsCanvas() and cmsDiCanvas() , along with some standard color palettes used in CMS plots. However, customizing the plots to suit specific needs is a bit complicated. Some of the useful tricks are described below.

Customizing the axes and labels

While using the cmsstyle package, the getCMSStyle() function is called to modify the objects in the figure instead of directly modifying the object. This is because the canvas decorations are applied globally using setCMSStyle() . Manually overriding the style of the objects before drawing on the canvas does not work.

For example, the axes parameters are customized after calling the style function as follows:


cmsstyle.setCMSStyle()
# Set lumi, energy etc labels here ...
cmsstyle.getCMSStyle().SetNdivisions(505, "XY") # applies to both X and Y
cmsstyle.getCMSStyle().SetNdivisions(505, "X")  # applies to only X
cmsstyle.getCMSStyle().SetLabelSize(0.04, "XY") # Sets the label size for both X and Y axes
# Similarly, the title size, offset, etc. can be customized for both axes or individually for X and Y axes.
            

The extra text on the top carrying luminosity and COM energy are designed to display some predefined texts. These can be customized as follows:


# SetLumi options:
cmsstyle.SetLumi(value)                  # Sets the luminosity value in fb^-1. 
cmsstyle.SetLumi(-1)                     # Prevents the luminosity value and the fb^-1 text, but still displays the "Run2" text.
cmsstyle.SetLumi(-1, run="Custom label") # Displays the custom label instead of "Run2" text. (Example: 2018_UL)

# SetEnergy options:
cmsstyle.SetEnergy(13)         # Sets the COM energy value in TeV.
cmsstyle.SetEnergy(0)          # Prevents the COM energy value, but still displays the "TeV" unit.
cmsstyle.SetEnergy(0, unit="") # Prevents the COM energy value and the "TeV" unit.

# SetExtraText options:
cmsstyle.SetExtraText("s")            # "s" = Simulation, "p" = Preliminary; use "" for unblinded plots.
cmsstyle.SetExtraText("Custom text")  # Allows a custom text to be displayed. 
            

Handling overflow and underflow bins

The standard TH1::Integral() function does not include the underflow and overflow bins by default. Therefore, before plotting, the underflow and overflow should be added to the first and last visible bins, respectively. This keeps the histogram integral consistent with the total number of selected events across the full variable range.

There is, however, one small thing to be careful about. Once the events are moved into the visible bins, the underflow and overflow bins should be reset to zero . Otherwise, if the function is called again, the same events will be moved again and counted twice. I learned this the hard way after accidentally calling the function twice and getting unexpectedly large integrals in my plots.

The following function performs the migration and resets the two bins afterwards. Pay attention to the last part.


def fixUnderflowOverflow(h):
    lastBin = h.GetNbinsX()

    # Handle overflow
    content  = h.GetBinContent(lastBin)
    error    = h.GetBinError(lastBin)
    overflow = h.GetBinContent(lastBin + 1)
    overflow_err = h.GetBinError(lastBin + 1)
    updated_content = content + overflow
    updated_error   = (error*error + overflow_err*overflow_err) ** 0.5
    h.SetBinContent(lastBin, updated_content)
    h.SetBinError(lastBin, updated_error)

    # Handle underflow
    content_first = h.GetBinContent(1)
    error_first   = h.GetBinError(1)
    underflow     = h.GetBinContent(0)
    underflow_err = h.GetBinError(0)
    updated_content_first = content_first + underflow
    updated_error_first   = (error_first*error_first + underflow_err*underflow_err) ** 0.5
    h.SetBinContent(1, updated_content_first)
    h.SetBinError(1, updated_error_first)

    # Reset underflow / overflow bins
    h.SetBinContent(lastBin + 1, 0.0)
    h.SetBinError(lastBin + 1, 0.0)
    h.SetBinContent(0, 0.0)
    h.SetBinError(0, 0.0)
            

Plotting data with Poisson errors

Data are drawn as points with asymmetric Poisson uncertainties rather than as a histogram because the observed event count is discrete and the corresponding statistical uncertainty is asymmetric, particularly for bins with a small number of events. A TGraphAsymmErrors object allows these lower and upper uncertainties to be displayed explicitly, while empty bins can simply be omitted from the graph. The CMS Poisson error bars prescription is used for the data uncertainties.

The Poisson uncertainties are assigned using ROOT's built-in kPoisson option.


h.Sumw2(False)
h.SetBinErrorOption(ROOT.TH1.kPoisson)

The data histogram is then converted to a TGraphAsymmErrors (empty bins can be omitted) as follows, so that the asymmetric lower and upper uncertainties can be displayed.


def makeDataGraph(h_data):
    g_data = ROOT.TGraphAsymmErrors()
    pt_idx = 0
    for i in range(1, h_data.GetNbinsX()+1):
        y = h_data.GetBinContent(i)
        if y == 0: continue # Safe to skip empty bins

        # Extract information from the histogram 
        x = h_data.GetBinCenter(i)
        ey_low = h_data.GetBinErrorLow(i)
        ey_high = h_data.GetBinErrorUp(i)
        ex = h_data.GetBinWidth(i) / 2.0

        # Set the point in the graph
        g_data.SetPoint(pt_idx, x, y)
        g_data.SetPointError(pt_idx, ex, ex, ey_low, ey_high)
        pt_idx += 1

    # Decoration
    g_data.SetMarkerStyle(ROOT.kFullCircle)
    g_data.SetMarkerSize(1.1)
    g_data.SetLineColor(ROOT.kBlack)
    g_data.SetMarkerColor(ROOT.kBlack)
    return g_data
            

The points are drawn as filled black circles with horizontal uncertainties corresponding to half the bin width. The graph is drawn with PZ0 , which displays the points and their error bars without connecting them.


cmsstyle.cmsObjectDraw(g_data, "PZ0")
            

Making the ratio plot

The ratio panel shows the observed data divided by the total expected background. A horizontal reference line at y = 1 is drawn to make it easy to identify agreement between data and prediction. I use a dummy histogram to define the ratio-panel axis and extract its x-range before drawing the reference line.


# Inherit the binning and x-range from the total background histogram
h_ratio_dummy = h_tot.Clone("ratio_dummy")
h_ratio_dummy.Reset()
h_ratio_dummy.SetYTitle(yratiotitle)
cmsstyle.cmsObjectDraw(h_ratio_dummy, "AXIS") # Draw only the axes, no data

# Draw a horizontal reference line at y=1, using the x-range of the dummy histogram
x_min = h_ratio_dummy.GetXaxis().GetXmin()
x_max = h_ratio_dummy.GetXaxis().GetXmax()
line = ROOT.TLine(x_min, 1.0, x_max, 1.0)
line.SetLineStyle(2)
line.Draw()
            

The uncertainty on the total background is shown as a band around unity. For each bin, the absolute background uncertainty is divided by the total background yield, giving the corresponding relative uncertainty in the ratio panel. The band is therefore centred at y = 1 .


def makeUncertaintyBand(h_tot):
    # inherit the binning and x-range
    h_band = h_tot.Clone("ratio_band")
    h_band.SetDirectory(0)
    h_band.Reset()

    # Fill the band with relative uncertainties
    for i in range(1, h_tot.GetNbinsX()+1):
        B   = h_tot.GetBinContent(i)
        err = h_tot.GetBinError(i)
        h_band.SetBinContent(i, 1.0)             # Center the band at 1.0
        if B > 0: h_band.SetBinError(i, err / B) # Relative uncertainty = bin error / bin content
        else:     h_band.SetBinError(i, 0.0)
    
    # Decoration
    h_band.SetFillStyle(3345) # dashed fill pattern
    h_band.SetFillColor(12)   # light gray fill color
    h_band.SetMarkerSize(0)   # No markers for the band
    return h_band
            

The data-to-background ratio is constructed as a TGraphAsymmErrors object so that the asymmetric Poisson uncertainties of the data are retained. The data yield and its lower and upper uncertainties are divided by the expected background yield in each bin. Empty data bins and bins with zero expected background are omitted.


def makeRatioGraph(h_data, h_tot):
    g_ratio = ROOT.TGraphAsymmErrors()
    pt_idx = 0
    for i in range(1, h_data.GetNbinsX()+1):
        y = h_data.GetBinContent(i)
        if y == 0: continue # Safe to skip empty bins

        # Extract information from the histogram
        x = h_data.GetBinCenter(i)
        ey_low = h_data.GetBinErrorLow(i)
        ey_high = h_data.GetBinErrorUp(i)
        ex = h_data.GetBinWidth(i) / 2.0
        bkg_val = h_tot.GetBinContent(i)
        if bkg_val > 0: # Only include bins with non-zero expected background
            g_ratio.SetPoint(pt_idx, x, y / bkg_val)
            g_ratio.SetPointError(pt_idx, ex, ex, ey_low / bkg_val, ey_high / bkg_val)
            pt_idx += 1

    # Decoration
    g_ratio.SetMarkerStyle(20)
    g_ratio.SetMarkerSize(0.9)
    g_ratio.SetLineColor(ROOT.kBlack)
    g_ratio.SetMarkerColor(ROOT.kBlack)
    return g_ratio
            

Finally, the ratio points are drawn without connecting them.


cmsstyle.cmsObjectDraw(g_ratio, "PZ0")