ra4_stats  0341147a0dc35f80f4e12c6003afb76a38e2ed6e
abcd_sensitivity.py
Go to the documentation of this file.
1 #! /usr/bin/env python
2 
3 from __future__ import print_function
4 
5 import argparse
6 import numpy
7 import scipy.stats
8 import math
9 import ROOT
10 
11 def poisson(N,mu):
12  if mu>0.:
13  return N*math.log(mu)-mu
14  elif mu==0. and N==0.:
15  return 0.
16  else:
17  raise ValueError("Bad poisson params: N={}, mu={}".format(N,mu))
18 
19 def eval(x, coeffs):
20  y = 0.
21  for c in coeffs:
22  y = y*x+c
23  return y
24 
25 class ABCDModel():
26  def __init__(self, B, R):
27  self.B = B
28  self.R = R
29 
30  def fit(self,S,Shat = None):
31  if Shat is None:
32  return (self.B, self.R, S)
33  elif Shat == 0.:
34  T = self.B*(1.+self.R)**2+S
35  Rhat = self.B*self.R*(1.+self.R)/(self.B*(1.+self.R)+S)
36  Bhat = T/(1.+Rhat)**2
37  return (Bhat, Rhat, Shat)
38  else:
39  c3 = S+self.B*(self.R+1.)**2
40  c2 = -(S*S + 2.*self.B*self.R*S + 2.*self.B*S - Shat*S + self.B*self.B*self.R*self.R - 2.*Shat*self.B*self.R*self.R + 2.*self.B*self.B*self.R - 4.*Shat*self.B*self.R + self.B*self.B - Shat*self.B)
41  c1 = -Shat*self.B*self.R*(2.*S + 2.*self.B*self.R - Shat*self.R + 2.*self.B - 2.*Shat)
42  c0 = -Shat*Shat*self.B*self.B*self.R*self.R
43 
44  coeffs = (c3,c2,c1,c0)
45 
46  lower = self.B*self.R/(self.R+2.)
47  upper = (self.B*(self.R+1.)+S)**2/(self.B*(self.R+1.)**2+S)
48  Bhat = lower+0.5*(upper-lower)
49  while upper>Bhat and Bhat>lower:
50  if eval(Bhat, coeffs)>0.:
51  upper=Bhat
52  else:
53  lower=Bhat
54  Bhat = lower+0.5*(upper-lower)
55 
56  Rhat = (math.sqrt(Bhat*(4.*self.B*self.R*self.R+4.*self.B*self.R+Bhat))-Bhat)/(2.*Bhat)
57  return (Bhat, Rhat, Shat)
58 
59  def nll(self, S, Bhat, Rhat, Shat):
60  return -(poisson(self.B+S, Bhat+Shat)
61  +2.*poisson(self.B*self.R, Bhat*Rhat)
62  +poisson(self.B*self.R*self.R, Bhat*Rhat*Rhat))
63 
64  def q0(self, S):
65  Bfix, Rfix, Sfix = self.fit(S,0.)
66  Bfloat, Rfloat, Sfloat = self.fit(S)
67  return 2.*(self.nll(S, Bfix, Rfix, Sfix)-self.nll(S, Bfloat, Rfloat, Sfloat))
68 
69  def qtilde(self, Stest):
70  Bfix, Rfix, Sfix = self.fit(0.,Stest)
71  Bfloat, Rfloat, Sfloat = self.fit(0.)
72  if Sfloat > Stest:
73  return 0.
74  return 2.*(self.nll(0., Bfix, Rfix, Sfix)-self.nll(0., Bfloat, Rfloat, Sfloat))
75 
76  def clb(self):
77  return 0.5
78 
79  def clsb(self, Stest):
80  qtilde = max(self.qtilde(Stest), 0.)
81  return scipy.stats.norm.sf(math.sqrt(qtilde))
82 
83  def cls(self, Stest):
84  return self.clsb(Stest)/self.clb()
85 
86  def canDiscover(self, S):
87  return self.q0(S) > 25.
88 
89  def canExclude(self, S):
90  return self.cls(S) < 0.05
91 
93  lower = 0.
94  upper = 1.
95  while not self.canDiscover(upper):
96  upper *= 2.
97  if upper >= 1000000000.:
98  return upper
99 
100  mid = lower+0.5*(upper-lower)
101  while upper>mid and mid>lower:
102  if self.canDiscover(mid):
103  upper = mid
104  else:
105  lower = mid
106  mid = lower + 0.5*(upper-lower)
107  return mid
108 
109  def upperLimit(self):
110  lower = 0.
111  upper = 1.
112  while not self.canExclude(upper):
113  upper *= 2.
114 
115  mid = lower+0.5*(upper-lower)
116  while upper>mid and mid>lower:
117  if self.canExclude(mid):
118  upper = mid
119  else:
120  lower = mid
121  mid = lower + 0.5*(upper-lower)
122  return mid
123 
124 def makeHist(bins_B, bins_R, line_style):
125  h = ROOT.TH2D("",
126  ";Background Yield in Signal Region;Sideband-to-Signal Ratio;Signal Events for Exclusion/Discovery",
127  len(bins_B)-1, bins_B, len(bins_R)-1, bins_R)
128  h.SetStats(False)
129  h.SetMinimum(1.)
130  h.SetLineWidth(5)
131  h.SetLineStyle(line_style)
132  h.SetLineColor(1)
133  h.SetTitleOffset(1.05, "XYZ")
134  h.SetTitleSize(0.045, "XYZ")
135  return h
136 
137 def getContours(the_max):
138  one_max = math.floor(math.log(the_max,10.))
139  three_max = math.floor(math.log(the_max/3.,10.))
140 
141  one_list = [ 10.**p for p in range(one_max+1) ]
142  three_list = [ 3.*10.**p for p in range(one_max+1) ]
143 
144  full_list = one_list + three_list
145  full_list.sort()
146 
147  return numpy.array(full_list)
148 
149 def makePlot(range_B, range_R, num_bins_B, num_bins_R):
150  ROOT.gStyle.SetPalette(ROOT.kRainBow)
151  ROOT.gStyle.SetNumberContours(999)
152 
153  abcd = ABCDModel(1., 10.)
154  bins_B = numpy.logspace(math.log(range_B[0],2.),
155  math.log(range_B[1],2.),
156  num_bins_B+1, base=2.)
157  bins_R = numpy.logspace(math.log(range_R[0],2.),
158  math.log(range_R[1],2.),
159  num_bins_R+1, base=2.)
160  h_disc = makeHist(bins_B, bins_R, 1)
161  h_excl = makeHist(bins_B, bins_R, 3)
162  h_dumb = makeHist(bins_B, bins_R, 1)
163 
164  for ib in range(num_bins_B):
165  for ir in range(num_bins_R):
166  abcd.B = math.sqrt(bins_B[ib]*bins_B[ib+1])
167  abcd.R = math.sqrt(bins_R[ir]*bins_R[ir+1])
168  h_disc.SetBinContent(ib+1, ir+1, abcd.discoverySensitivity())
169  h_excl.SetBinContent(ib+1, ir+1, abcd.upperLimit())
170 
171  left_margin = 0.1
172  right_margin = 0.15
173  top_margin = 0.1
174  bottom_margin = 0.1
175 
176  canvas = ROOT.TCanvas("canvas", "canvas", 800, 750)
177  canvas.SetLogx()
178  canvas.SetLogy()
179  canvas.SetLogz()
180  canvas.SetMargin(left_margin, right_margin, bottom_margin, top_margin)
181 
182  the_max = h_excl.GetBinContent(h_excl.GetMaximumBin())*1.001
183  h_dumb.Fill(-1.,-1.)
184  h_disc.SetMaximum(the_max)
185  h_excl.SetMaximum(the_max)
186  h_dumb.SetMaximum(the_max)
187 
188  contours = getContours(the_max)
189 
190  h_disc.SetContour(len(contours), contours)
191  h_excl.SetContour(len(contours), contours)
192 
193  ROOT.gStyle.SetNumberContours(999)
194  h_excl.Draw("cont1z")
195  h_disc.Draw("cont1 same")
196 
197  legend = ROOT.TLegend(left_margin, 1.-top_margin, 1.-right_margin, 1.)
198  legend.SetNColumns(2)
199  legend.SetBorderSize(0)
200  legend.AddEntry(h_disc, "5#sigma Discovery", "l")
201  legend.AddEntry(h_excl, "95% C.L. Exclusion", "l")
202  legend.Draw()
203 
204  canvas.Print("abcd_sensitivity_thresholds.pdf")
205 
206 if __name__ == "__main__":
207  parser = argparse.ArgumentParser(description="Computes minimum number of signal events needed for ABCD sensistivity.",
208  formatter_class=argparse.ArgumentDefaultsHelpFormatter)
209  parser.add_argument("--rangeB", type=float, nargs=2, default=[0.1,1000.], help="Range of number of background events to test")
210  parser.add_argument("--rangeR", type=float, nargs=2, default=[0.1,1000.], help="Range of sideband-to-signal ratios to test")
211  parser.add_argument("--binsB", type=int, default=100, help="Number of bins to use along background axis")
212  parser.add_argument("--binsR", type=int, default=100, help="Number of bins to use along ratio axis")
213  args = parser.parse_args()
214 
215  makePlot(args.rangeB, args.rangeR, args.binsB, args.binsR)
def getContours(the_max)
def makeHist(bins_B, bins_R, line_style)
def fit(self, S, Shat=None)
def nll(self, S, Bhat, Rhat, Shat)
def eval(x, coeffs)
def makePlot(range_B, range_R, num_bins_B, num_bins_R)