1cimport util 2 3# cdef class Binned(AnalysisObject): 4# pass 5# cdef class Fillable(AnalysisObject): 6# pass 7 8cdef class Histo1D(AnalysisObject): 9 """ 10 1D histogram, with distinction between bin areas and heights. 11 12 Complete histogram binning is supported, including uniform/regular binning, 13 variable-width binning, unbinned gaps in the covered range, and 14 under/overflows. Rebinning by integer factors, or by explicit merging of 15 contiguous bins is also supported. 16 17 Rescaling of weights and/or the x axis is permitted in-place: the result is 18 still a valid Histo1D. Binning-compatible 1D histograms may be divided, 19 resulting in a Scatter2D since further fills would not be meaningful. 20 21 Several sets of arguments are tried by the constructor in the 22 following order. 23 24 Histo1D(path="", title=""). 25 Construct a histogram with optional path and title but no bins. 26 27 Histo1D(nbins, low, high, path="", title="") 28 Construct a histogram with optional path and title, and nbins bins 29 uniformly distributed between low and high. 30 31 Histo1D(B, path="", title=""). 32 Construct a histogram with optional path and title, from an 33 iterator of bins, B. 34 """ 35 36 cdef inline c.Histo1D* h1ptr(self) except NULL: 37 return <c.Histo1D*> self.ptr() 38 39 def __init__(self, *args, **kwargs): 40 util.try_loop([self.__init2, self.__init5, self.__init3], *args, **kwargs) 41 42 def __init2(self, path="", title=""): 43 path = path.encode('utf-8') 44 title = title.encode('utf-8') 45 cutil.set_owned_ptr(self, new c.Histo1D(<string>path, <string>title)) 46 47 # TODO: Is Cython clever enough that we can make 3a and 3b versions and let it do the type inference? 48 def __init3(self, bins_or_edges, path="", title=""): 49 # TODO: Do this type-checking better 50 cdef vector[double] edges 51 try: 52 path = path.encode('utf-8') 53 title = title.encode('utf-8') 54 ## If float conversions work for all elements, it's a list of edges: 55 edges = list(float(x) for x in bins_or_edges) 56 cutil.set_owned_ptr(self, new c.Histo1D(edges, <string>path, <string>title)) 57 except: 58 ## Assume it's a list of HistoBin1D 59 bins = bins_or_edges 60 self.__init2(path, title) 61 self.addBins(bins) 62 63 def __init5(self, nbins, low, high, path="", title=""): 64 path = path.encode('utf-8') 65 title = title.encode('utf-8') 66 cutil.set_owned_ptr(self, new c.Histo1D(nbins, low, high, <string>path, <string>title)) 67 68 69 def __len__(self): 70 "Number of bins" 71 return self.numBins() 72 73 def __getitem__(self, i): 74 "Direct access to bins" 75 cdef size_t ii = cutil.pythonic_index(i, self.numBins()) 76 return cutil.new_borrowed_cls(HistoBin1D, & self.h1ptr().bin(ii), self) 77 78 79 def __repr__(self): 80 xmean = None 81 if self.sumW() != 0: 82 xmean = "%0.2e" % self.xMean() 83 return "<%s '%s' %d bins, sumw=%0.2g, xmean=%s>" % \ 84 (self.__class__.__name__, self.path(), 85 len(self.bins()), self.sumW(), xmean) 86 87 88 def reset(self): 89 """None -> None. 90 Reset the histogram but leave the bin structure.""" 91 self.h1ptr().reset() 92 93 94 def clone(self): 95 """None -> Histo1D. 96 Clone this Histo1D.""" 97 return cutil.new_owned_cls(Histo1D, self.h1ptr().newclone()) 98 99 100 def fill(self, x, weight=1.0, fraction=1.0): 101 """(x,[w]) -> None. 102 Fill with given x value and optional weight.""" 103 self.h1ptr().fill(x, weight, fraction) 104 105 106 def fillBin(self, size_t ix, weight=1.0, fraction=1.0): 107 """(ix,[w]) -> None. 108 Fill bin ix and optional weight.""" 109 self.h1ptr().fillBin(ix, weight, fraction) 110 111 112 def totalDbn(self): 113 """None -> Dbn1D 114 The Dbn1D representing the total distribution.""" 115 return cutil.new_borrowed_cls(Dbn1D, &self.h1ptr().totalDbn(), self) 116 117 def underflow(self): 118 """None -> Dbn1D 119 The Dbn1D representing the underflow distribution.""" 120 return cutil.new_borrowed_cls(Dbn1D, &self.h1ptr().underflow(), self) 121 122 def overflow(self): 123 """None -> Dbn1D 124 The Dbn1D representing the overflow distribution.""" 125 return cutil.new_borrowed_cls(Dbn1D, &self.h1ptr().overflow(), self) 126 127 128 def integral(self, includeoverflows=True): 129 """([bool]) -> float 130 Histogram integral, optionally excluding the overflows.""" 131 return self.h1ptr().integral(includeoverflows) 132 133 def integralRange(self, int ia, int ib): 134 """(int, int) -> float 135 Integral between bins ia..ib inclusive""" 136 return self.h1ptr().integralRange(ia, ib) 137 138 def integralTo(self, int ia, includeunderflow=True): 139 """(int, [bool]) -> float 140 Integral up to bin ia inclusive, optionally excluding the underflow""" 141 return self.h1ptr().integralRange(ia, includeunderflow) 142 143 144 def numEntries(self, includeoverflows=True): 145 """([bool]) -> float 146 Number of times this histogram was filled, optionally excluding the overflows.""" 147 return self.h1ptr().numEntries(includeoverflows) 148 149 def effNumEntries(self, includeoverflows=True): 150 """([bool]) -> float 151 Effective number of times this histogram was filled, computed from weights, and optionally excluding the overflows.""" 152 return self.h1ptr().effNumEntries(includeoverflows) 153 154 def sumW(self, includeoverflows=True): 155 """([bool]) -> float 156 Sum of weights filled into this histogram, optionally excluding the overflows.""" 157 return self.h1ptr().sumW(includeoverflows) 158 159 def sumW2(self, includeoverflows=True): 160 """([bool]) -> float 161 Sum of weights filled into this histogram, optionally excluding the overflows.""" 162 return self.h1ptr().sumW2(includeoverflows) 163 164 165 def xMean(self, includeoverflows=True): 166 """([bool]) -> float 167 Mean x of the histogram, optionally excluding the overflows.""" 168 return self.h1ptr().xMean(includeoverflows) 169 170 def xVariance(self, includeoverflows=True): 171 """([bool]) -> float 172 Variance in x of the histogram, optionally excluding the overflows.""" 173 return self.h1ptr().xVariance(includeoverflows) 174 175 def xStdDev(self, includeoverflows=True): 176 """([bool]) -> float 177 Standard deviation in x of the histogram, optionally excluding the overflows.""" 178 return self.h1ptr().xStdDev(includeoverflows) 179 180 def xStdErr(self, includeoverflows=True): 181 """([bool]) -> float 182 Standard error on the mean x of the histogram, optionally excluding the overflows.""" 183 return self.h1ptr().xStdErr(includeoverflows) 184 185 def xRMS(self, includeoverflows=True): 186 """([bool]) -> float 187 RMS in x of the histogram, optionally excluding the overflows.""" 188 return self.h1ptr().xRMS(includeoverflows) 189 190 191 def scaleW(self, w): 192 """ (float) -> None. 193 Rescale the weights in this histogram by the factor w.""" 194 self.h1ptr().scaleW(w) 195 196 def normalize(self, normto=1.0, includeoverflows=True): 197 """ (float, bool) -> None. 198 Normalize the histogram.""" 199 self.h1ptr().normalize(normto, includeoverflows) 200 201 202 def xMin(self): 203 """Low x edge of the histo.""" 204 return self.h1ptr().xMin() 205 206 def xMax(self): 207 """High x edge of the histo.""" 208 return self.h1ptr().xMax() 209 210 def numBins(self): 211 """() -> int 212 Number of bins (not including overflows).""" 213 return self.h1ptr().numBins() 214 215 def numBinsX(self): 216 """() -> int 217 Number of x-axis bins (not including overflows).""" 218 return self.h1ptr().numBinsX() 219 220 def bins(self): 221 """Access the ordered bins list.""" 222 return list(self) 223 224 def bin(self, i): 225 """Get the i'th bin (equivalent to bins[i]""" 226 # cdef size_t ii = cutil.pythonic_index(i, self.h1ptr().numBins()) 227 return cutil.new_borrowed_cls(HistoBin1D, & self.h1ptr().bin(i), self) 228 229 def binIndexAt(self, x): 230 """Get the bin index containing position x""" 231 return self.h1ptr().binIndexAt(x) 232 233 def binAt(self, x): 234 """Get the bin containing position x""" 235 # TODO: what's the problem with this direct mapping? Produces compile error re. no default constructor... 236 #return cutil.new_borrowed_cls(HistoBin1D, & self.h1ptr().binAt(x), self) 237 # TODO: need out-of-range check to return None? 238 return self.bin(self.binIndexAt(x)) 239 240 def addBin(self, low, high): 241 """(low, high) -> None. 242 Add a bin.""" 243 self.h1ptr().addBin(low, high) 244 245 def addBins(self, edges_or_bins): 246 """Add several bins.""" 247 # TODO: simplify / make consistent 248 arg = list(edges_or_bins) 249 util.try_loop([self.__addBins_edges, 250 self.__addBins_tuples, 251 self.__addBins_points, 252 self.__addBins_bins], arg) 253 254 def __addBins_edges(self, edges): 255 cdef vector[double] cedges 256 for edge in edges: 257 cedges.push_back(edge) 258 if len(edges): 259 self.h1ptr().addBins(cedges) 260 261 def __addBins_bins(self, bins): 262 self.__addBins_tuples([ b.xEdges for b in bins ]) 263 264 def __addBins_points(self, points): 265 self.__addBins_tuples([ p.xWidth for p in points ]) 266 267 def __addBins_tuples(self, tuples): 268 cdef double a, b 269 for a, b in tuples: 270 self.h1ptr().addBin(a, b) 271 272 273 def mergeBins(self, ia, ib): 274 """mergeBins(ia, ib) -> None. 275 Merge bins from indices ia through ib.""" 276 self.h1ptr().mergeBins(ia, ib) 277 278 def rebinBy(self, n, begin=0, end=None): 279 """(n) -> None. 280 Merge every group of n bins together (between begin and end, if specified).""" 281 if end is None: 282 end = self.numBins() 283 self.h1ptr().rebinBy(int(n), begin, end) 284 285 def rebinTo(self, edges): 286 """([edges]) -> None. 287 Merge bins to produce the given new edges... which must be a subset of the current ones.""" 288 self.h1ptr().rebinTo(edges) 289 290 def rebin(self, arg, **kwargs): 291 """(n) -> None or ([edges]) -> None 292 Merge bins, like rebinBy if an int argument is given; like rebinTo if an iterable is given.""" 293 if hasattr(arg, "__iter__"): 294 self.rebinTo(arg, **kwargs) 295 else: 296 self.rebinBy(arg, **kwargs) 297 298 299 def mkScatter(self, usefocus=False): 300 """None -> Scatter2D. 301 Convert this Histo1D to a Scatter2D, with y representing bin heights 302 (not sumW) and height errors.""" 303 cdef c.Scatter2D s2 = c.mkScatter_Histo1D(deref(self.h1ptr()), usefocus) 304 return cutil.new_owned_cls(Scatter2D, s2.newclone()) 305 306 307 def toIntegral(self, efficiency=False, includeunderflow=True, includeoverflow=True): 308 """None -> Scatter2D. 309 310 Convert this Histo1D to a Scatter2D representing an integral (i.e. cumulative) 311 histogram constructed from this differential one. 312 313 The efficiency argument is used to construct an 'efficiency integral' histogram 314 and the includeXXXflow bools determine whether under and overflows are included 315 in computing the (efficiency) integral. 316 """ 317 cdef c.Scatter2D s 318 if not efficiency: 319 s = c.Histo1D_toIntegral(deref(self.h1ptr()), includeunderflow) 320 else: 321 s = c.Histo1D_toIntegralEff(deref(self.h1ptr()), includeunderflow, includeoverflow) 322 return cutil.new_owned_cls(Scatter2D, s.newclone()) 323 324 def divideBy(self, Histo1D h, efficiency=False): 325 """Histo1D -> Scatter2D 326 327 Divide this histogram by h, returning a Scatter2D. The optional 'efficiency' 328 argument, if set True, will use a binomial efficiency treatment of the errors. 329 """ 330 # if type(h) is not Histo1D: 331 # raise ValueError("Histograms must be of the same type to be divided") 332 cdef c.Scatter2D s 333 if not efficiency: 334 s = c.Histo1D_div_Histo1D(deref(self.h1ptr()), deref(h.h1ptr())) 335 else: 336 s = c.Histo1D_eff_Histo1D(deref(self.h1ptr()), deref(h.h1ptr())) 337 return cutil.new_owned_cls(Scatter2D, s.newclone()) 338 339 340 ## In-place special methods 341 342 def __iadd__(Histo1D self, Histo1D other): 343 c.Histo1D_iadd_Histo1D(self.h1ptr(), other.h1ptr()) 344 return self 345 346 def __isub__(Histo1D self, Histo1D other): 347 c.Histo1D_isub_Histo1D(self.h1ptr(), other.h1ptr()) 348 return self 349 350 # def __imul__(Histo1D self, double x): 351 # c.Histo1D_imul_dbl(self.h1ptr(), x) 352 # return self 353 354 # def __idiv__(Histo1D self, double x): 355 # c.Histo1D_idiv_dbl(self.h1ptr(), x) 356 # return self 357 358 359 ## Unbound special methods 360 361 # # TODO: only to bootstrap sum(), but doesn't work properly? Seems to treat *self* as the int... 362 # def __radd__(Histo1D self, zero): 363 # #assert zero == 0 364 # print "FOO" 365 # return self.clone() 366 367 def __add__(Histo1D self, Histo1D other): 368 # print "BAR" 369 h = Histo1D() 370 cutil.set_owned_ptr(h, c.Histo1D_add_Histo1D(self.h1ptr(), other.h1ptr())) 371 return h 372 373 # TODO: Cython doesn't support type overloading for special functions? 374 # def __add__(Histo1D self, int x): 375 # """ 376 # Special operator support to allow use of sum(histos) which starts from 0. 377 # """ 378 # assert(x == 0) 379 # return self 380 381 # TODO: Cython doesn't support type overloading for special functions? 382 # def __radd__(Histo1D self, int x): 383 # """ 384 # Special operator support to allow use of sum(histos) which starts from 0. 385 # """ 386 # assert(x == 0) 387 # return self 388 389 def __sub__(Histo1D self, Histo1D other): 390 h = Histo1D() 391 cutil.set_owned_ptr(h, c.Histo1D_sub_Histo1D(self.h1ptr(), other.h1ptr())) 392 return h 393 394 # def __mul__(Histo1D self, double x): 395 # h = c.Histo1D_mul_dbl(self.h1ptr(), x) 396 # return h 397 # def __rmul__(Histo1D self, double x): 398 # h = c.Histo1D_mul_dbl(self.h1ptr(), x) 399 # return h 400 401 def __div__(Histo1D self, Histo1D other): 402 return self.divideBy(other) 403 404 def __truediv__(Histo1D self, Histo1D other): 405 return self.divideBy(other) 406 407 408 409 ## Functions for array-based plotting, chi2 calculations, etc. 410 411 # def sumWs(self): 412 # """All sumWs of the histo.""" 413 # return [b.sumW for b in self.bins] 414 415 def _mknp(self, xs): 416 try: 417 import numpy 418 return numpy.array(xs) 419 except ImportError: 420 return xs 421 422 def xEdges(self): 423 """All x edges of the histo.""" 424 return self._mknp(self.h1ptr().xEdges()) 425 426 def xWidths(self): 427 """All x widths of the histo.""" 428 return self._mknp(self.h1ptr().xWidths()) 429 430 def xMins(self): 431 """All x low edges of the histo.""" 432 return self._mknp([b.xMin() for b in self.bins()]) 433 434 def xMaxs(self): 435 """All x high edges of the histo.""" 436 return self._mknp([b.xMax() for b in self.bins()]) 437 438 def xMids(self): 439 """All x bin midpoints of the histo.""" 440 return self._mknp([b.xMid() for b in self.bins()]) 441 442 def xFoci(self): 443 """All x bin foci of the histo.""" 444 return self._mknp([b.xFocus() for b in self.bins()]) 445 446 def xVals(self, foci=False): 447 return self.xFoci() if foci else self.xMids() 448 449 def xErrs(self, foci=False): 450 if foci: 451 return [(b.xFocus()-b.xMin(), b.xMax()-b.xFocus()) for b in self.bins()] 452 else: 453 return [(b.xMid()-b.xMin(), b.xMax()-b.xMid()) for b in self.bins()] 454 455 def xMin(self): 456 """Lowest x value.""" 457 return min(self.xMins()) 458 459 def xMax(self): 460 """Highest x value.""" 461 return max(self.xMaxs()) 462 463 464 def sumWs(self): 465 """All sumW values of the histo.""" 466 rtn = self._mknp([b.sumW() for b in self.bins()]) 467 return rtn 468 469 def heights(self): 470 """All y heights of the histo.""" 471 return self._mknp([b.height() for b in self.bins()]) 472 473 def areas(self): 474 """All areas of the histo.""" 475 return self._mknp([b.area() for b in self.bins()]) 476 477 def yVals(self, area=False): 478 return self.areas() if area else self.heights() 479 480 481 def heightErrs(self): #, asymm=False): 482 """All height errors of the histo. 483 484 TODO: asymm arg / heightErrsMinus/Plus? 485 """ 486 return self._mknp([b.heightErr() for b in self.bins()]) 487 488 def areaErrs(self): #, asymm=False): 489 """All area errors of the histo. 490 491 TODO: asymm arg / areaErrsMinus/Plus? 492 """ 493 # Use symmetrised errors by default, or return a list of (-,+) pairs if asymm is requested.""" 494 # if asymm: 495 # pass 496 #else: 497 return self._mknp([b.areaErr() for b in self.bins()]) 498 499 def relErrs(self): #, asymm=False): 500 """All relative errors of the histo. 501 502 TODO: asymm arg / areaErrsMinus/Plus? 503 """ 504 return self._mknp([b.relErr() for b in self.bins()]) 505 506 def yErrs(self, area=False): 507 return self.areaErrs() if area else self.heightErrs() 508 509 510 def yMins(self, area=False): 511 ys = self.yVals(area) 512 es = self.yErrs(area) 513 return self._mknp([y-e for (y,e) in zip(ys,es)]) 514 515 def yMaxs(self, area=False): 516 ys = self.yVals(area) 517 es = self.yErrs(area) 518 return self._mknp([y+e for (y,e) in zip(ys,es)]) 519 520 def yMin(self): 521 """Lowest x value.""" 522 return min(self.yMins()) 523 524 def yMax(self): 525 """Highest y value.""" 526 return max(self.yMaxs()) 527 528 529## Convenience alias 530H1D = Histo1D 531