## \file
## \ingroup tutorial_roofit_main
## \notebook -js
## Implementing the Barlow-Beeston method for taking into account the statistical
## uncertainty of a Monte-Carlo fit template.
##
## \macro_image
## \macro_code
## \macro_output
##
## Based on a demo by Wouter Verkerke
## \date June 2021
## \author Harshal Shende, Stephan Hageboeck (C++ version)


import ROOT

# First, construct a likelihood model with a Gaussian signal on top of a uniform background
x = ROOT.RooRealVar("x", "x", -20, 20)
x.setBins(25)

meanG = ROOT.RooRealVar("meanG", "meanG", 1, -10, 10)
sigG = ROOT.RooRealVar("sigG", "sigG", 1.5, -10, 10)
g = ROOT.RooGaussian("g", "Gauss", x, meanG, sigG)
u = ROOT.RooUniform("u", "Uniform", x)


# Generate the data to be fitted
sigData = g.generate(x, 50)
bkgData = u.generate(x, 1000)

sumData = ROOT.RooDataSet("sumData", "Gauss + Uniform", x)
sumData.append(sigData)
sumData.append(bkgData)


# Make histogram templates for signal and background.
# Let's take a signal distribution with low statistics and a more accurate
# background distribution.
# Normally, these come from Monte Carlo simulations, but we will just generate them.
dh_sig = g.generateBinned(x, 50)
dh_bkg = u.generateBinned(x, 10000)


#  Case 0 - 'Rigid templates'

# Construct histogram shapes for signal and background
p_h_sig = ROOT.RooHistFunc("p_h_sig", "p_h_sig", x, dh_sig)
p_h_bkg = ROOT.RooHistFunc("p_h_bkg", "p_h_bkg", x, dh_bkg)

# Construct scale factors for adding the two distributions
Asig0 = ROOT.RooRealVar("Asig", "Asig", 1, 0.01, 5000)
Abkg0 = ROOT.RooRealVar("Abkg", "Abkg", 1, 0.01, 5000)

# Construct the sum model
model0 = ROOT.RooRealSumPdf("model0", "model0", [p_h_sig, p_h_bkg], [Asig0, Abkg0], True)


#  Case 1 - 'Barlow Beeston'

# Construct parameterized histogram shapes for signal and background
p_ph_sig1 = ROOT.RooParamHistFunc("p_ph_sig", "p_ph_sig", dh_sig, x)
p_ph_bkg1 = ROOT.RooParamHistFunc("p_ph_bkg", "p_ph_bkg", dh_bkg, x)

Asig1 = ROOT.RooRealVar("Asig", "Asig", 1, 0.01, 5000)
Abkg1 = ROOT.RooRealVar("Abkg", "Abkg", 1, 0.01, 5000)

# Construct the sum of these
model_tmp = ROOT.RooRealSumPdf("sp_ph", "sp_ph", [p_ph_sig1, p_ph_bkg1], [Asig1, Abkg1], True)

# Construct the subsidiary poisson measurements constraining the histogram parameters
# These ensure that the bin contents of the histograms are only allowed to vary within
# the statistical uncertainty of the Monte Carlo.
hc_sig = ROOT.RooHistConstraint("hc_sig", "hc_sig", p_ph_sig1)
hc_bkg = ROOT.RooHistConstraint("hc_bkg", "hc_bkg", p_ph_bkg1)

# Construct the joint model with template PDFs and constraints
model1 = ROOT.RooProdPdf("model1", "model1", {hc_sig, hc_bkg}, Conditional=(model_tmp, x))


#  Case 2 - 'Barlow Beeston' light (one parameter per bin for all samples)

# Construct the histogram shapes, using the same parameters for signal and background
# This requires passing the first histogram to the second, so that their common parameters
# can be re-used.
# The first ParamHistFunc will create one parameter per bin, such as `p_ph_sig2_gamma_bin_0`.
# This allows bin 0 to fluctuate up and down.
# Then, the SAME parameters are connected to the background histogram, so the bins fluctuate
# synchronously. This reduces the number of parameters.
p_ph_sig2 = ROOT.RooParamHistFunc("p_ph_sig2", "p_ph_sig2", dh_sig, x)
p_ph_bkg2 = ROOT.RooParamHistFunc("p_ph_bkg2", "p_ph_bkg2", dh_bkg, x, p_ph_sig2, True)

Asig2 = ROOT.RooRealVar("Asig", "Asig", 1, 0.01, 5000)
Abkg2 = ROOT.RooRealVar("Abkg", "Abkg", 1, 0.01, 5000)

# As before, construct the sum of signal2 and background2
model2_tmp = ROOT.RooRealSumPdf("sp_ph", "sp_ph", [p_ph_sig2, p_ph_bkg2], [Asig2, Abkg2], True)

# Construct the subsidiary poisson measurements constraining the statistical fluctuations
hc_sigbkg = ROOT.RooHistConstraint("hc_sigbkg", "hc_sigbkg", {p_ph_sig2, p_ph_bkg2})

# Construct the joint model
model2 = ROOT.RooProdPdf("model2", "model2", hc_sigbkg, Conditional=(model2_tmp, x))


# ************ Fit all models to data and plot *********************

result0 = model0.fitTo(sumData, PrintLevel=0, Save=True)
result1 = model1.fitTo(sumData, PrintLevel=0, Save=True)
result2 = model2.fitTo(sumData, PrintLevel=0, Save=True)


can = ROOT.TCanvas("can", "", 1500, 600)
can.Divide(3, 1)

pt = ROOT.TPaveText(-19.5, 1, -2, 25)
pt.SetFillStyle(0)
pt.SetBorderSize(0)


can.cd(1)
frame = x.frame(Title="No template uncertainties")
# Plot data to enable automatic determination of model0 normalisation:
sumData.plotOn(frame)
model0.plotOn(frame, LineColor="b", VisualizeError=result0)
# Plot data again to show it on top of model0 error bands:
sumData.plotOn(frame)
# Plot model components
model0.plotOn(frame, LineColor="b")
p_ph_sig_set = {p_h_sig}
p_ph_bkg_set = {p_h_bkg}
model0.plotOn(frame, Components=p_ph_sig_set, LineColor="kAzure")
model0.plotOn(frame, Components=p_ph_bkg_set, LineColor="r")
model0.paramOn(frame)

sigData.plotOn(frame, MarkerColor="b")
frame.Draw()

pt_text1 = [
    "No template uncertainties",
    "are taken into account.",
    "This leads to low errors",
    "for the parameters A, since",
    "the only source of errors",
    "are the data statistics.",
]
for text in pt_text1:
    pt.AddText(text)

pt.DrawClone()


can.cd(2)
frame = x.frame(Title="Barlow Beeston for Sig & Bkg separately")
sumData.plotOn(frame)
model1.plotOn(frame, LineColor="b", VisualizeError=result1)
# Plot data again to show it on top of error bands:
sumData.plotOn(frame)
model1.plotOn(frame, LineColor="b")
p_ph_sig1_set = {p_ph_sig1}
p_ph_bkg1_set = {p_ph_bkg1}
model1.plotOn(frame, Components=p_ph_sig1_set, LineColor="kAzure")
model1.plotOn(frame, Components=p_ph_bkg1_set, LineColor="r")
model1.paramOn(frame, Parameters={Asig1, Abkg1})

sigData.plotOn(frame, MarkerColor="b")
frame.Draw()

pt.Clear()
pt_text2 = [
    "With gamma parameters, the",
    "signal & background templates",
    "can adapt to the data.",
    "Note how the blue signal",
    "template changes its shape.",
    "This leads to higher errors",
    "of the scale parameters A.",
]

for text in pt_text2:
    pt.AddText(text)

pt.DrawClone()

can.cd(3)
frame = x.frame(Title="Barlow Beeston light for (Sig+Bkg)")
sumData.plotOn(frame)
model2.plotOn(frame, LineColor="b", VisualizeError=result2)
# Plot data again to show it on top of model0 error bands:
sumData.plotOn(frame)
model2.plotOn(frame, LineColor="b")
p_ph_sig2_set = {p_ph_sig2}
p_ph_bkg2_set = {p_ph_bkg2}
model2.plotOn(frame, Components=p_ph_sig2_set, LineColor="kAzure")
model2.plotOn(frame, Components=p_ph_bkg2_set, LineColor="r")
model2.paramOn(frame, Parameters={Asig2, Abkg2})

sigData.plotOn(frame, MarkerColor="b")
frame.Draw()

pt.Clear()
pt_text3 = [
    "When signal and background",
    "template share one gamma para-",
    "meter per bin, they adapt less.",
    "The errors of the A parameters",
    "also shrink slightly.",
]
for text in pt_text3:
    pt.AddText(text)
pt.DrawClone()


print("Asig [normal ] = {} +/- {}".format(Asig0.getVal(), Asig0.getError()))
print("Asig [BB     ] = {} +/- {}".format(Asig1.getVal(), Asig1.getError()))
print("Asig [BBlight] = {} +/- {}".format(Asig2.getVal(), Asig2.getError()))

can.SaveAs("rf709_BarlowBeeston.png")
