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 }