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