go-hep.org/x/hep@v0.38.1/hbook/s2d.go (about)

     1  // Copyright ©2016 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  	"bufio"
     9  	"bytes"
    10  	"fmt"
    11  	"io"
    12  	"maps"
    13  	"math"
    14  	"sort"
    15  	"strings"
    16  )
    17  
    18  // S2D is a collection of 2-dim data points with errors.
    19  type S2D struct {
    20  	pts []Point2D
    21  	ann Annotation
    22  }
    23  
    24  // NewS2D creates a new 2-dim scatter with pts as an optional
    25  // initial set of data points.
    26  //
    27  // If n <= 0, the initial capacity is 0.
    28  func NewS2D(pts ...Point2D) *S2D {
    29  	s := &S2D{
    30  		pts: make([]Point2D, len(pts)),
    31  		ann: make(Annotation),
    32  	}
    33  	copy(s.pts, pts)
    34  	return s
    35  }
    36  
    37  // NewS2DFrom creates a new 2-dim scatter with x,y data slices.
    38  //
    39  // It panics if the lengths of the 2 slices don't match.
    40  func NewS2DFrom(x, y []float64) *S2D {
    41  	if len(x) != len(y) {
    42  		panic("hbook: len differ")
    43  	}
    44  
    45  	s := &S2D{
    46  		pts: make([]Point2D, len(x)),
    47  		ann: make(Annotation),
    48  	}
    49  	for i := range s.pts {
    50  		pt := &s.pts[i]
    51  		pt.X = x[i]
    52  		pt.Y = y[i]
    53  	}
    54  	return s
    55  }
    56  
    57  // S2DOpts controls how S2D scatters are created from H1D and P1D.
    58  type S2DOpts struct {
    59  	UseFocus  bool
    60  	UseStdDev bool
    61  }
    62  
    63  // NewS2DFromH1D creates a new 2-dim scatter from the given H1D.
    64  // NewS2DFromH1D optionally takes a S2DOpts slice:
    65  // only the first element is considered.
    66  func NewS2DFromH1D(h *H1D, opts ...S2DOpts) *S2D {
    67  	s := NewS2D()
    68  	maps.Copy(s.ann, h.Ann)
    69  	var opt S2DOpts
    70  	if len(opts) > 0 {
    71  		opt = opts[0]
    72  	}
    73  	// YODA support
    74  	if _, ok := s.ann["Type"]; ok {
    75  		s.ann["Type"] = "Scatter2D"
    76  	}
    77  	for _, bin := range h.Binning.Bins {
    78  		var x float64
    79  		if opt.UseFocus {
    80  			x = bin.XFocus()
    81  		} else {
    82  			x = bin.XMid()
    83  		}
    84  		exm := x - bin.XMin()
    85  		exp := bin.XMax() - x
    86  		var y, ey float64
    87  		if w := bin.XWidth(); w != 0 {
    88  			ww := 1 / w
    89  			y = bin.SumW() * ww
    90  			ey = math.Sqrt(bin.SumW2()) * ww
    91  		} else {
    92  			y = math.NaN()  // FIXME(sbinet): use quiet-NaN ?
    93  			ey = math.NaN() // FIXME(sbinet): use quiet-NaN ?
    94  		}
    95  		s.Fill(Point2D{X: x, Y: y, ErrX: Range{exm, exp}, ErrY: Range{ey, ey}})
    96  	}
    97  	return s
    98  }
    99  
   100  // NewS2DFromP1D creates a new 2-dim scatter from the given P1D.
   101  // NewS2DFromP1D optionally takes a S2DOpts slice:
   102  // only the first element is considered.
   103  func NewS2DFromP1D(p *P1D, opts ...S2DOpts) *S2D {
   104  	s := NewS2D()
   105  	maps.Copy(s.ann, p.ann)
   106  	var opt S2DOpts
   107  	if len(opts) > 0 {
   108  		opt = opts[0]
   109  	}
   110  	// YODA support
   111  	if _, ok := s.ann["Type"]; ok {
   112  		s.ann["Type"] = "Scatter2D"
   113  	}
   114  	for _, bin := range p.bng.bins {
   115  		var x float64
   116  		if opt.UseFocus {
   117  			x = bin.XFocus()
   118  		} else {
   119  			x = bin.XMid()
   120  		}
   121  		exm := x - bin.XMin()
   122  		exp := bin.XMax() - x
   123  		var y, ey float64
   124  		if w := bin.XWidth(); w != 0 {
   125  			ww := 1 / w
   126  			y = bin.SumW() * ww
   127  			if opt.UseStdDev {
   128  				ey = bin.XStdDev()
   129  			} else {
   130  				ey = bin.XStdErr()
   131  			}
   132  		} else {
   133  			y = math.NaN()  // FIXME(sbinet): use quiet-NaN ?
   134  			ey = math.NaN() // FIXME(sbinet): use quiet-NaN ?
   135  		}
   136  		s.Fill(Point2D{X: x, Y: y, ErrX: Range{exm, exp}, ErrY: Range{ey, ey}})
   137  	}
   138  	return s
   139  }
   140  
   141  // Annotation returns the annotations attached to the
   142  // scatter. (e.g. name, title, ...)
   143  func (s *S2D) Annotation() Annotation {
   144  	return s.ann
   145  }
   146  
   147  // Name returns the name of this scatter
   148  func (s *S2D) Name() string {
   149  	v, ok := s.ann["name"]
   150  	if !ok {
   151  		return ""
   152  	}
   153  	n, ok := v.(string)
   154  	if !ok {
   155  		return ""
   156  	}
   157  	return n
   158  }
   159  
   160  // Rank returns the number of dimensions of this scatter.
   161  func (*S2D) Rank() int {
   162  	return 2
   163  }
   164  
   165  // Entries returns the number of entries of this histogram.
   166  func (s *S2D) Entries() int64 {
   167  	return int64(len(s.pts))
   168  }
   169  
   170  // Fill adds new points to the scatter.
   171  func (s *S2D) Fill(pts ...Point2D) {
   172  	if len(pts) == 0 {
   173  		return
   174  	}
   175  
   176  	i := len(s.pts)
   177  	s.pts = append(s.pts, make([]Point2D, len(pts))...)
   178  	copy(s.pts[i:], pts)
   179  }
   180  
   181  // Sort sorts the data points by x,y and x-err,y-err.
   182  func (s *S2D) Sort() {
   183  	sort.Sort(points2D(s.pts))
   184  }
   185  
   186  // Points returns the points of the scatter.
   187  //
   188  // Users may not modify the returned slice.
   189  // Users may not rely on the stability of the indices as the slice of points
   190  // may be re-sorted at any point in time.
   191  func (s *S2D) Points() []Point2D {
   192  	return s.pts
   193  }
   194  
   195  // Point returns the point at index i.
   196  //
   197  // Point panics if i is out of bounds.
   198  func (s *S2D) Point(i int) Point2D {
   199  	return s.pts[i]
   200  }
   201  
   202  // ScaleX rescales the X values by a factor f.
   203  func (s *S2D) ScaleX(f float64) {
   204  	for i := range s.pts {
   205  		p := &s.pts[i]
   206  		p.ScaleX(f)
   207  	}
   208  }
   209  
   210  // ScaleY rescales the Y values by a factor f.
   211  func (s *S2D) ScaleY(f float64) {
   212  	for i := range s.pts {
   213  		p := &s.pts[i]
   214  		p.ScaleY(f)
   215  	}
   216  }
   217  
   218  // ScaleXY rescales the X and Y values by a factor f.
   219  func (s *S2D) ScaleXY(f float64) {
   220  	for i := range s.pts {
   221  		p := &s.pts[i]
   222  		p.ScaleX(f)
   223  		p.ScaleY(f)
   224  	}
   225  }
   226  
   227  // Len returns the number of points in the scatter.
   228  //
   229  // Len implements the gonum/plot/plotter.XYer interface.
   230  func (s *S2D) Len() int {
   231  	return len(s.pts)
   232  }
   233  
   234  // XY returns the x, y pair at index i.
   235  //
   236  // XY panics if i is out of bounds.
   237  // XY implements the gonum/plot/plotter.XYer interface.
   238  func (s *S2D) XY(i int) (x, y float64) {
   239  	pt := s.pts[i]
   240  	x = pt.X
   241  	y = pt.Y
   242  	return
   243  }
   244  
   245  // XError returns the two error values for X data.
   246  //
   247  // XError implements the gonum/plot/plotter.XErrorer interface.
   248  func (s *S2D) XError(i int) (float64, float64) {
   249  	pt := s.pts[i]
   250  	return pt.ErrX.Min, pt.ErrX.Max
   251  }
   252  
   253  // YError returns the two error values for Y data.
   254  //
   255  // YError implements the gonum/plot/plotter.YErrorer interface.
   256  func (s *S2D) YError(i int) (float64, float64) {
   257  	pt := s.pts[i]
   258  	return pt.ErrY.Min, pt.ErrY.Max
   259  }
   260  
   261  // DataRange returns the minimum and maximum
   262  // x and y values, implementing the gonum/plot.DataRanger
   263  // interface.
   264  func (s *S2D) DataRange() (xmin, xmax, ymin, ymax float64) {
   265  	xmin = math.Inf(+1)
   266  	ymin = math.Inf(+1)
   267  	xmax = math.Inf(-1)
   268  	ymax = math.Inf(-1)
   269  	for _, p := range s.pts {
   270  		xmin = math.Min(p.XMin(), xmin)
   271  		xmax = math.Max(p.XMax(), xmax)
   272  		ymin = math.Min(p.YMin(), ymin)
   273  		ymax = math.Max(p.YMax(), ymax)
   274  	}
   275  	return
   276  }
   277  
   278  // annToYODA creates a new Annotation with fields compatible with YODA
   279  func (s *S2D) annToYODA() Annotation {
   280  	ann := make(Annotation, len(s.ann))
   281  	ann["Type"] = "Scatter2D"
   282  	ann["Path"] = "/" + s.Name()
   283  	ann["Title"] = ""
   284  	for k, v := range s.ann {
   285  		if k == "name" {
   286  			continue
   287  		}
   288  		if k == "title" {
   289  			ann["Title"] = v
   290  			continue
   291  		}
   292  		ann[k] = v
   293  	}
   294  	return ann
   295  }
   296  
   297  // annFromYODA creates a new Annotation from YODA compatible fields
   298  func (s *S2D) annFromYODA(ann Annotation) {
   299  	if len(s.ann) == 0 {
   300  		s.ann = make(Annotation, len(ann))
   301  	}
   302  	for k, v := range ann {
   303  		switch k {
   304  		case "Type":
   305  			// noop
   306  		case "Path":
   307  			name := v.(string)
   308  			name = strings.TrimPrefix(name, "/")
   309  			s.ann["name"] = name
   310  		case "Title":
   311  			s.ann["title"] = v
   312  		default:
   313  			s.ann[k] = v
   314  		}
   315  	}
   316  }
   317  
   318  // MarshalYODA implements the YODAMarshaler interface.
   319  func (s *S2D) MarshalYODA() ([]byte, error) {
   320  	return s.marshalYODAv2()
   321  }
   322  
   323  func (s *S2D) marshalYODAv1() ([]byte, error) {
   324  	buf := new(bytes.Buffer)
   325  	ann := s.annToYODA()
   326  	fmt.Fprintf(buf, "BEGIN YODA_SCATTER2D %s\n", ann["Path"])
   327  	data, err := ann.marshalYODAv1()
   328  	if err != nil {
   329  		return nil, err
   330  	}
   331  	buf.Write(data)
   332  
   333  	// TODO: change ordering to {vals} {errs} {errs} ...
   334  	fmt.Fprintf(buf, "# xval\t xerr-\t xerr+\t yval\t yerr-\t yerr+\n")
   335  	s.Sort()
   336  	for _, pt := range s.pts {
   337  		fmt.Fprintf(
   338  			buf,
   339  			"%e\t%e\t%e\t%e\t%e\t%e\n",
   340  			pt.X, pt.ErrX.Min, pt.ErrX.Max, pt.Y, pt.ErrY.Min, pt.ErrY.Max,
   341  		)
   342  	}
   343  	fmt.Fprintf(buf, "END YODA_SCATTER2D\n\n")
   344  	return buf.Bytes(), err
   345  }
   346  
   347  func (s *S2D) marshalYODAv2() ([]byte, error) {
   348  	buf := new(bytes.Buffer)
   349  	ann := s.annToYODA()
   350  	fmt.Fprintf(buf, "BEGIN YODA_SCATTER2D_V2 %s\n", ann["Path"])
   351  	data, err := ann.marshalYODAv2()
   352  	if err != nil {
   353  		return nil, err
   354  	}
   355  	buf.Write(data)
   356  	buf.Write([]byte("---\n"))
   357  
   358  	// TODO: change ordering to {vals} {errs} {errs} ...
   359  	fmt.Fprintf(buf, "# xval\t xerr-\t xerr+\t yval\t yerr-\t yerr+\t\n")
   360  	s.Sort()
   361  	for _, pt := range s.pts {
   362  		fmt.Fprintf(
   363  			buf,
   364  			"%e\t%e\t%e\t%e\t%e\t%e\n",
   365  			pt.X, pt.ErrX.Min, pt.ErrX.Max, pt.Y, pt.ErrY.Min, pt.ErrY.Max,
   366  		)
   367  	}
   368  	fmt.Fprintf(buf, "END YODA_SCATTER2D_V2\n\n")
   369  	return buf.Bytes(), err
   370  }
   371  
   372  // UnmarshalYODA implements the YODAUnmarshaler interface.
   373  func (s *S2D) UnmarshalYODA(data []byte) error {
   374  	r := newRBuffer(data)
   375  	_, vers, err := readYODAHeader(r, "BEGIN YODA_SCATTER2D")
   376  	if err != nil {
   377  		return err
   378  	}
   379  	switch vers {
   380  	case 1:
   381  		return s.unmarshalYODAv1(r)
   382  	case 2:
   383  		return s.unmarshalYODAv2(r)
   384  	default:
   385  		return fmt.Errorf("hbook: invalid YODA version %v", vers)
   386  	}
   387  }
   388  
   389  func (s *S2D) unmarshalYODAv1(r *rbuffer) error {
   390  	ann := make(Annotation)
   391  
   392  	// pos of end of annotations
   393  	pos := bytes.Index(r.Bytes(), []byte("\n# xval\t xerr-\t"))
   394  	if pos < 0 {
   395  		return fmt.Errorf("hbook: invalid Scatter2D-YODA data")
   396  	}
   397  	err := ann.unmarshalYODAv1(r.Bytes()[:pos+1])
   398  	if err != nil {
   399  		return fmt.Errorf("hbook: %q\nhbook: %w", string(r.Bytes()[:pos+1]), err)
   400  	}
   401  	s.annFromYODA(ann)
   402  	r.next(pos)
   403  
   404  	sc := bufio.NewScanner(r)
   405  scanLoop:
   406  	for sc.Scan() {
   407  		buf := sc.Bytes()
   408  		if len(buf) == 0 || buf[0] == '#' {
   409  			continue
   410  		}
   411  		rbuf := bytes.NewReader(buf)
   412  		switch {
   413  		case bytes.HasPrefix(buf, []byte("END YODA_SCATTER2D")):
   414  			break scanLoop
   415  		default:
   416  			var pt Point2D
   417  			fmt.Fscanf(
   418  				rbuf,
   419  				"%e\t%e\t%e\t%e\t%e\t%e\n",
   420  				&pt.X, &pt.ErrX.Min, &pt.ErrX.Max, &pt.Y, &pt.ErrY.Min, &pt.ErrY.Max,
   421  			)
   422  			if err != nil {
   423  				return fmt.Errorf("hbook: %q\nhbook: %w", string(buf), err)
   424  			}
   425  			s.Fill(pt)
   426  		}
   427  	}
   428  	err = sc.Err()
   429  	if err == io.EOF {
   430  		err = nil
   431  	}
   432  	s.Sort()
   433  	return err
   434  }
   435  
   436  func (s *S2D) unmarshalYODAv2(r *rbuffer) error {
   437  	ann := make(Annotation)
   438  
   439  	// pos of end of annotations
   440  	pos := bytes.Index(r.Bytes(), []byte("\n# xval\t xerr-\t"))
   441  	if pos < 0 {
   442  		return fmt.Errorf("hbook: invalid Scatter2D-YODA data")
   443  	}
   444  	err := ann.unmarshalYODAv2(r.Bytes()[:pos+1])
   445  	if err != nil {
   446  		return fmt.Errorf("hbook: %q\nhbook: %w", string(r.Bytes()[:pos+1]), err)
   447  	}
   448  	s.annFromYODA(ann)
   449  	r.next(pos)
   450  
   451  	sc := bufio.NewScanner(r)
   452  scanLoop:
   453  	for sc.Scan() {
   454  		buf := sc.Bytes()
   455  		if len(buf) == 0 || buf[0] == '#' {
   456  			continue
   457  		}
   458  		rbuf := bytes.NewReader(buf)
   459  		switch {
   460  		case bytes.HasPrefix(buf, []byte("END YODA_SCATTER2D_V2")):
   461  			break scanLoop
   462  		default:
   463  			var pt Point2D
   464  			fmt.Fscanf(
   465  				rbuf,
   466  				"%e\t%e\t%e\t%e\t%e\t%e\n",
   467  				&pt.X, &pt.ErrX.Min, &pt.ErrX.Max, &pt.Y, &pt.ErrY.Min, &pt.ErrY.Max,
   468  			)
   469  			if err != nil {
   470  				return fmt.Errorf("hbook: %q\nhbook: %w", string(buf), err)
   471  			}
   472  			s.Fill(pt)
   473  		}
   474  	}
   475  	err = sc.Err()
   476  	if err == io.EOF {
   477  		err = nil
   478  	}
   479  	s.Sort()
   480  	return err
   481  }