go-hep.org/x/hep@v0.38.1/hbook/ops.go (about) 1 // Copyright ©2017 The go-hep Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package hbook 6 7 import ( 8 "fmt" 9 "math" 10 ) 11 12 // DivideH1D divides 2 1D-histograms and returns a 2D scatter. 13 // DivideH1D returns an error if the binning of the 1D histograms are not compatible. 14 // If no DivOptions is passed, NaN raised during division are kept. 15 func DivideH1D(num, den *H1D, opts ...DivOptions) (*S2D, error) { 16 17 cfg := newDivConfig() 18 for _, opt := range opts { 19 opt(cfg) 20 } 21 22 var s2d S2D 23 24 bins1 := num.Binning.Bins 25 bins2 := den.Binning.Bins 26 27 for i := range bins1 { 28 b1 := bins1[i] 29 b2 := bins2[i] 30 31 if !fuzzyEq(b1.XMin(), b2.XMin()) || !fuzzyEq(b1.XMax(), b2.XMax()) { 32 return nil, fmt.Errorf("hbook: x binnings are not equivalent in %v / %v", num.Name(), den.Name()) 33 } 34 35 // assemble the x value and error 36 // use the midpoint of the "bin" for the new central value 37 x := b1.XMid() 38 exm := x - b1.XMin() 39 exp := b1.XMax() - x 40 41 // assemble the y value and error 42 var y, ey float64 43 b2h := b2.SumW() / b2.XWidth() // height of the bin 44 b1h := b1.SumW() / b1.XWidth() // ditto 45 b2herr := math.Sqrt(b2.SumW2()) / b2.XWidth() 46 b1herr := math.Sqrt(b1.SumW2()) / b1.XWidth() 47 48 switch { 49 case b2h == 0 || (b1h == 0 && b1herr != 0): // TODO(sbinet): is it OK? 50 if cfg.ignoreNaN { 51 continue 52 } else { 53 y = cfg.replaceNaN 54 ey = 0.0 // TODO(rmadar): I guess this is the most sensitive case 55 // but another field could be added to divConfig 56 } 57 default: 58 y = b1h / b2h 59 // TODO(sbinet): is this the exact error treatment for all (uncorrelated) cases? 60 // What should be the behaviour around 0? +1 and -1 fills? 61 relerr1 := 0.0 62 if b1herr != 0 { 63 relerr1 = math.Sqrt(b1.SumW2()) / b1.SumW() // TODO(sbinet) refactor as bin1d.RelErr() ? 64 } 65 relerr2 := 0.0 66 if b2herr != 0 { 67 relerr2 = math.Sqrt(b2.SumW2()) / b2.SumW() 68 } 69 ey = y * math.Sqrt(relerr1*relerr1+relerr2*relerr2) 70 } 71 72 // deal with +/- errors separately, inverted for the denominator contributions: 73 // TODO(sbinet): check correctness with different signed numerator and denominator. 74 75 s2d.Fill(Point2D{X: x, Y: y, ErrX: Range{Min: exm, Max: exp}, ErrY: Range{Min: ey, Max: ey}}) 76 } 77 return &s2d, nil 78 } 79 80 // DivOptions allows to customize the behaviour of DivideH1D 81 type DivOptions func(c *divConfig) 82 83 // divConfig type specifies the possible configurations 84 // passed as DivOptions. 85 type divConfig struct { 86 ignoreNaN bool 87 replaceNaN float64 88 } 89 90 // newDivConfig function builds the default configuration 91 // for DivideH1D() option. 92 func newDivConfig() *divConfig { 93 return &divConfig{replaceNaN: math.NaN()} 94 } 95 96 // DivIgnoreNaNs function configures DivideH1D to 97 // ignore data points with NaNs. 98 func DivIgnoreNaNs() DivOptions { 99 return func(c *divConfig) { 100 c.ignoreNaN = true 101 } 102 } 103 104 // DivReplaceNaNs function configures DivideH1D to replace 105 // NaN raised during divisions with the provided value. 106 func DivReplaceNaNs(v float64) DivOptions { 107 return func(c *divConfig) { 108 c.replaceNaN = v 109 } 110 } 111 112 // fuzzyEq returns true if a and b are equal with a degree of fuzziness 113 func fuzzyEq(a, b float64) bool { 114 const tol = 1e-5 115 aa := math.Abs(a) 116 bb := math.Abs(b) 117 absavg := 0.5 * (aa + bb) 118 absdiff := math.Abs(a - b) 119 return (aa < 1e-8 && bb < 1e-8) || absdiff < tol*absavg 120 } 121 122 // AddScaledH1D returns the histogram with the bin-by-bin h1+alpha*h2 123 // operation, assuming statistical uncertainties are uncorrelated. 124 func AddScaledH1D(h1 *H1D, alpha float64, h2 *H1D) *H1D { 125 if h1.Len() != h2.Len() { 126 panic(fmt.Errorf("hbook: h1 and h2 have different number of bins")) 127 } 128 129 if h1.XMin() != h2.XMin() || h1.XMax() != h2.XMax() { 130 panic(fmt.Errorf("hbook: h1 and h2 have different range")) 131 } 132 133 var ( 134 o = h1.Clone() 135 a2 = alpha * alpha 136 ) 137 138 for i := range o.Binning.Bins { 139 o := &o.Binning.Bins[i] 140 o.addScaled(alpha, a2, h2.Binning.Bins[i]) 141 } 142 143 o.Binning.Dist.addScaled(alpha, a2, h2.Binning.Dist) 144 o.Binning.Outflows[0].addScaled(alpha, a2, h2.Binning.Outflows[0]) 145 o.Binning.Outflows[1].addScaled(alpha, a2, h2.Binning.Outflows[1]) 146 return o 147 } 148 149 // AddH1D returns the bin-by-bin summed histogram of h1 and h2 150 // assuming their statistical uncertainties are uncorrelated. 151 func AddH1D(h1, h2 *H1D) *H1D { 152 return AddScaledH1D(h1, 1, h2) 153 } 154 155 // SubH1D returns the bin-by-bin subtracted histogram of h1 and h2 156 // assuming their statistical uncertainties are uncorrelated. 157 func SubH1D(h1, h2 *H1D) *H1D { 158 return AddScaledH1D(h1, -1, h2) 159 }