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  }