go-hep.org/x/hep@v0.38.1/hbook/h2d.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 "math" 12 "strings" 13 ) 14 15 // H2D is a 2-dim histogram with weighted entries. 16 type H2D struct { 17 Binning Binning2D 18 Ann Annotation 19 } 20 21 // NewH2D creates a new 2-dim histogram. 22 func NewH2D(nx int, xlow, xhigh float64, ny int, ylow, yhigh float64) *H2D { 23 return &H2D{ 24 Binning: newBinning2D(nx, xlow, xhigh, ny, ylow, yhigh), 25 Ann: make(Annotation), 26 } 27 } 28 29 // NewH2DFromEdges creates a new 2-dim histogram from slices 30 // of edges in x and y. 31 // The number of bins in x and y is thus len(edges)-1. 32 // It panics if the length of edges is <=1 (in any dimension.) 33 // It panics if the edges are not sorted (in any dimension.) 34 // It panics if there are duplicate edge values (in any dimension.) 35 func NewH2DFromEdges(xedges []float64, yedges []float64) *H2D { 36 return &H2D{ 37 Binning: newBinning2DFromEdges(xedges, yedges), 38 Ann: make(Annotation), 39 } 40 } 41 42 // Name returns the name of this histogram, if any 43 func (h *H2D) Name() string { 44 v, ok := h.Ann["name"] 45 if !ok { 46 return "" 47 } 48 n, ok := v.(string) 49 if !ok { 50 return "" 51 } 52 return n 53 } 54 55 // Annotation returns the annotations attached to this histogram 56 func (h *H2D) Annotation() Annotation { 57 return h.Ann 58 } 59 60 // Rank returns the number of dimensions for this histogram 61 func (h *H2D) Rank() int { 62 return 2 63 } 64 65 // Entries returns the number of entries in this histogram 66 func (h *H2D) Entries() int64 { 67 return h.Binning.entries() 68 } 69 70 // EffEntries returns the number of effective entries in this histogram 71 func (h *H2D) EffEntries() float64 { 72 return h.Binning.effEntries() 73 } 74 75 // SumW returns the sum of weights in this histogram. 76 // Overflows are included in the computation. 77 func (h *H2D) SumW() float64 { 78 return h.Binning.Dist.SumW() 79 } 80 81 // SumW2 returns the sum of squared weights in this histogram. 82 // Overflows are included in the computation. 83 func (h *H2D) SumW2() float64 { 84 return h.Binning.Dist.SumW2() 85 } 86 87 // SumWX returns the 1st order weighted x moment 88 // Overflows are included in the computation. 89 func (h *H2D) SumWX() float64 { 90 return h.Binning.Dist.SumWX() 91 } 92 93 // SumWX2 returns the 2nd order weighted x moment 94 // Overflows are included in the computation. 95 func (h *H2D) SumWX2() float64 { 96 return h.Binning.Dist.SumWX2() 97 } 98 99 // SumWY returns the 1st order weighted y moment 100 // Overflows are included in the computation. 101 func (h *H2D) SumWY() float64 { 102 return h.Binning.Dist.SumWY() 103 } 104 105 // SumWY2 returns the 2nd order weighted y moment 106 // Overflows are included in the computation. 107 func (h *H2D) SumWY2() float64 { 108 return h.Binning.Dist.SumWY2() 109 } 110 111 // SumWXY returns the 1st order weighted x*y moment 112 // Overflows are included in the computation. 113 func (h *H2D) SumWXY() float64 { 114 return h.Binning.Dist.SumWXY() 115 } 116 117 // XMean returns the mean X. 118 // Overflows are included in the computation. 119 func (h *H2D) XMean() float64 { 120 return h.Binning.Dist.xMean() 121 } 122 123 // YMean returns the mean Y. 124 // Overflows are included in the computation. 125 func (h *H2D) YMean() float64 { 126 return h.Binning.Dist.yMean() 127 } 128 129 // XVariance returns the variance in X. 130 // Overflows are included in the computation. 131 func (h *H2D) XVariance() float64 { 132 return h.Binning.Dist.xVariance() 133 } 134 135 // YVariance returns the variance in Y. 136 // Overflows are included in the computation. 137 func (h *H2D) YVariance() float64 { 138 return h.Binning.Dist.yVariance() 139 } 140 141 // XStdDev returns the standard deviation in X. 142 // Overflows are included in the computation. 143 func (h *H2D) XStdDev() float64 { 144 return h.Binning.Dist.xStdDev() 145 } 146 147 // YStdDev returns the standard deviation in Y. 148 // Overflows are included in the computation. 149 func (h *H2D) YStdDev() float64 { 150 return h.Binning.Dist.yStdDev() 151 } 152 153 // XStdErr returns the standard error in X. 154 // Overflows are included in the computation. 155 func (h *H2D) XStdErr() float64 { 156 return h.Binning.Dist.xStdErr() 157 } 158 159 // YStdErr returns the standard error in Y. 160 // Overflows are included in the computation. 161 func (h *H2D) YStdErr() float64 { 162 return h.Binning.Dist.yStdErr() 163 } 164 165 // XRMS returns the RMS in X. 166 // Overflows are included in the computation. 167 func (h *H2D) XRMS() float64 { 168 return h.Binning.Dist.xRMS() 169 } 170 171 // YRMS returns the RMS in Y. 172 // Overflows are included in the computation. 173 func (h *H2D) YRMS() float64 { 174 return h.Binning.Dist.yRMS() 175 } 176 177 // Fill fills this histogram with (x,y) and weight w. 178 func (h *H2D) Fill(x, y, w float64) { 179 h.Binning.fill(x, y, w) 180 } 181 182 // FillN fills this histogram with the provided slices (xs,ys) and weights ws. 183 // if ws is nil, the histogram will be filled with entries of weight 1. 184 // Otherwise, FillN panics if the slices lengths differ. 185 func (h *H2D) FillN(xs, ys, ws []float64) { 186 switch ws { 187 case nil: 188 if len(xs) != len(ys) { 189 panic(fmt.Errorf("hbook: lengths mismatch")) 190 } 191 for i := range xs { 192 x := xs[i] 193 y := ys[i] 194 h.Binning.fill(x, y, 1) 195 } 196 default: 197 if len(xs) != len(ys) { 198 panic(fmt.Errorf("hbook: lengths mismatch")) 199 } 200 if len(xs) != len(ws) { 201 panic(fmt.Errorf("hbook: lengths mismatch")) 202 } 203 for i := range xs { 204 x := xs[i] 205 y := ys[i] 206 w := ws[i] 207 h.Binning.fill(x, y, w) 208 } 209 } 210 } 211 212 // Bin returns the bin at coordinates (x,y) for this 2-dim histogram. 213 // Bin returns nil for under/over flow bins. 214 func (h *H2D) Bin(x, y float64) *Bin2D { 215 idx := h.Binning.coordToIndex(x, y) 216 if idx < 0 { 217 return nil 218 } 219 return &h.Binning.Bins[idx] 220 } 221 222 // XMin returns the low edge of the X-axis of this histogram. 223 func (h *H2D) XMin() float64 { 224 return h.Binning.xMin() 225 } 226 227 // XMax returns the high edge of the X-axis of this histogram. 228 func (h *H2D) XMax() float64 { 229 return h.Binning.xMax() 230 } 231 232 // YMin returns the low edge of the Y-axis of this histogram. 233 func (h *H2D) YMin() float64 { 234 return h.Binning.yMin() 235 } 236 237 // YMax returns the high edge of the Y-axis of this histogram. 238 func (h *H2D) YMax() float64 { 239 return h.Binning.yMax() 240 } 241 242 // Integral computes the integral of the histogram. 243 // 244 // Overflows are included in the computation. 245 func (h *H2D) Integral() float64 { 246 return h.SumW() 247 } 248 249 // GridXYZ returns an anonymous struct value that implements 250 // gonum/plot/plotter.GridXYZ and is ready to plot. 251 func (h *H2D) GridXYZ() h2dGridXYZ { 252 return h2dGridXYZ{h} 253 } 254 255 type h2dGridXYZ struct { 256 h *H2D 257 } 258 259 func (g h2dGridXYZ) Dims() (c, r int) { 260 return g.h.Binning.Nx, g.h.Binning.Ny 261 } 262 263 func (g h2dGridXYZ) Z(c, r int) float64 { 264 idx := r*g.h.Binning.Nx + c 265 return g.h.Binning.Bins[idx].SumW() 266 } 267 268 func (g h2dGridXYZ) X(c int) float64 { 269 return g.h.Binning.Bins[c].XMid() 270 } 271 272 func (g h2dGridXYZ) Y(r int) float64 { 273 idx := r * g.h.Binning.Nx 274 return g.h.Binning.Bins[idx].YMid() 275 } 276 277 // check various interfaces 278 var _ Object = (*H2D)(nil) 279 var _ Histogram = (*H2D)(nil) 280 281 // annToYODA creates a new Annotation with fields compatible with YODA 282 func (h *H2D) annToYODA() Annotation { 283 ann := make(Annotation, len(h.Ann)) 284 ann["Type"] = "Histo2D" 285 ann["Path"] = "/" + h.Name() 286 ann["Title"] = "" 287 for k, v := range h.Ann { 288 if k == "name" { 289 continue 290 } 291 if k == "title" { 292 ann["Title"] = v 293 continue 294 } 295 ann[k] = v 296 } 297 return ann 298 } 299 300 // annFromYODA creates a new Annotation from YODA compatible fields 301 func (h *H2D) annFromYODA(ann Annotation) { 302 if len(h.Ann) == 0 { 303 h.Ann = make(Annotation, len(ann)) 304 } 305 for k, v := range ann { 306 switch k { 307 case "Type": 308 // noop 309 case "Path": 310 name := v.(string) 311 name = strings.TrimPrefix(name, "/") 312 h.Ann["name"] = name 313 case "Title": 314 h.Ann["title"] = v 315 default: 316 h.Ann[k] = v 317 } 318 } 319 } 320 321 // MarshalYODA implements the YODAMarshaler interface. 322 func (h *H2D) MarshalYODA() ([]byte, error) { 323 return h.marshalYODAv2() 324 } 325 326 func (h *H2D) marshalYODAv1() ([]byte, error) { 327 buf := new(bytes.Buffer) 328 ann := h.annToYODA() 329 fmt.Fprintf(buf, "BEGIN YODA_HISTO2D %s\n", ann["Path"]) 330 data, err := ann.marshalYODAv1() 331 if err != nil { 332 return nil, err 333 } 334 buf.Write(data) 335 336 fmt.Fprintf(buf, "# Mean: (%e, %e)\n", h.XMean(), h.YMean()) 337 fmt.Fprintf(buf, "# Volume: %e\n", h.Integral()) 338 339 fmt.Fprintf(buf, "# ID\t ID\t sumw\t sumw2\t sumwx\t sumwx2\t sumwy\t sumwy2\t sumwxy\t numEntries\n") 340 d := h.Binning.Dist 341 fmt.Fprintf( 342 buf, 343 "Total \tTotal \t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%d\n", 344 d.SumW(), d.SumW2(), d.SumWX(), d.SumWX2(), d.SumWY(), d.SumWY2(), d.SumWXY(), d.Entries(), 345 ) 346 347 // outflows 348 fmt.Fprintf(buf, "# 2D outflow persistency not currently supported until API is stable\n") 349 350 // bins 351 fmt.Fprintf(buf, "# xlow\t xhigh\t ylow\t yhigh\t sumw\t sumw2\t sumwx\t sumwx2\t sumwy\t sumwy2\t sumwxy\t numEntries\n") 352 for ix := range h.Binning.Nx { 353 for iy := range h.Binning.Ny { 354 bin := h.Binning.Bins[iy*h.Binning.Nx+ix] 355 d := bin.Dist 356 fmt.Fprintf( 357 buf, 358 "%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%d\n", 359 bin.XRange.Min, bin.XRange.Max, bin.YRange.Min, bin.YRange.Max, 360 d.SumW(), d.SumW2(), d.SumWX(), d.SumWX2(), d.SumWY(), d.SumWY2(), d.SumWXY(), d.Entries(), 361 ) 362 } 363 } 364 fmt.Fprintf(buf, "END YODA_HISTO2D\n\n") 365 return buf.Bytes(), err 366 } 367 368 func (h *H2D) marshalYODAv2() ([]byte, error) { 369 buf := new(bytes.Buffer) 370 ann := h.annToYODA() 371 fmt.Fprintf(buf, "BEGIN YODA_HISTO2D_V2 %s\n", ann["Path"]) 372 data, err := ann.marshalYODAv2() 373 if err != nil { 374 return nil, err 375 } 376 buf.Write(data) 377 buf.Write([]byte("---\n")) 378 379 fmt.Fprintf(buf, "# Mean: (%e, %e)\n", h.XMean(), h.YMean()) 380 fmt.Fprintf(buf, "# Volume: %e\n", h.Integral()) 381 382 fmt.Fprintf(buf, "# ID\t ID\t sumw\t sumw2\t sumwx\t sumwx2\t sumwy\t sumwy2\t sumwxy\t numEntries\n") 383 d := h.Binning.Dist 384 fmt.Fprintf( 385 buf, 386 "Total \tTotal \t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\n", 387 d.SumW(), d.SumW2(), d.SumWX(), d.SumWX2(), d.SumWY(), d.SumWY2(), d.SumWXY(), float64(d.Entries()), 388 ) 389 390 // outflows 391 fmt.Fprintf(buf, "# 2D outflow persistency not currently supported until API is stable\n") 392 393 // bins 394 fmt.Fprintf(buf, "# xlow\t xhigh\t ylow\t yhigh\t sumw\t sumw2\t sumwx\t sumwx2\t sumwy\t sumwy2\t sumwxy\t numEntries\n") 395 for ix := range h.Binning.Nx { 396 for iy := range h.Binning.Ny { 397 bin := h.Binning.Bins[iy*h.Binning.Nx+ix] 398 d := bin.Dist 399 fmt.Fprintf( 400 buf, 401 "%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\n", 402 bin.XRange.Min, bin.XRange.Max, bin.YRange.Min, bin.YRange.Max, 403 d.SumW(), d.SumW2(), d.SumWX(), d.SumWX2(), d.SumWY(), d.SumWY2(), d.SumWXY(), float64(d.Entries()), 404 ) 405 } 406 } 407 fmt.Fprintf(buf, "END YODA_HISTO2D_V2\n\n") 408 return buf.Bytes(), err 409 } 410 411 // UnmarshalYODA implements the YODAUnmarshaler interface. 412 func (h *H2D) UnmarshalYODA(data []byte) error { 413 r := newRBuffer(data) 414 _, vers, err := readYODAHeader(r, "BEGIN YODA_HISTO2D") 415 if err != nil { 416 return err 417 } 418 switch vers { 419 case 1: 420 return h.unmarshalYODAv1(r) 421 case 2: 422 return h.unmarshalYODAv2(r) 423 default: 424 return fmt.Errorf("hbook: invalid YODA version %v", vers) 425 } 426 } 427 428 func (h *H2D) unmarshalYODAv1(r *rbuffer) error { 429 ann := make(Annotation) 430 431 // pos of end of annotations 432 pos := bytes.Index(r.Bytes(), []byte("\n# Mean:")) 433 if pos < 0 { 434 return fmt.Errorf("hbook: invalid H2D-YODA data") 435 } 436 err := ann.unmarshalYODAv1(r.Bytes()[:pos+1]) 437 if err != nil { 438 return fmt.Errorf("hbook: %q\nhbook: %w", string(r.Bytes()[:pos+1]), err) 439 } 440 h.annFromYODA(ann) 441 r.next(pos) 442 443 var ctx struct { 444 dist bool 445 bins bool 446 } 447 448 // sets of xlow and ylow values, to infer number of bins in X and Y. 449 xset := make(map[float64]int) 450 yset := make(map[float64]int) 451 452 var ( 453 dist Dist2D 454 bins []Bin2D 455 xmin = math.Inf(+1) 456 xmax = math.Inf(-1) 457 ymin = math.Inf(+1) 458 ymax = math.Inf(-1) 459 ) 460 s := bufio.NewScanner(r) 461 scanLoop: 462 for s.Scan() { 463 buf := s.Bytes() 464 if len(buf) == 0 || buf[0] == '#' { 465 continue 466 } 467 rbuf := bytes.NewReader(buf) 468 switch { 469 case bytes.HasPrefix(buf, []byte("END YODA_HISTO2D")): 470 break scanLoop 471 case !ctx.dist && bytes.HasPrefix(buf, []byte("Total \t")): 472 ctx.dist = true 473 d := &dist 474 _, err = fmt.Fscanf( 475 rbuf, 476 "Total \tTotal \t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%d\n", 477 &d.X.Dist.SumW, &d.X.Dist.SumW2, 478 &d.X.Stats.SumWX, &d.X.Stats.SumWX2, 479 &d.Y.Stats.SumWX, &d.Y.Stats.SumWX2, 480 &d.Stats.SumWXY, &d.X.Dist.N, 481 ) 482 if err != nil { 483 return fmt.Errorf("hbook: %q\nhbook: %w", string(buf), err) 484 } 485 d.Y.Dist = d.X.Dist 486 ctx.bins = true 487 case ctx.bins: 488 var bin Bin2D 489 d := &bin.Dist 490 _, err = fmt.Fscanf( 491 rbuf, 492 "%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%d\n", 493 &bin.XRange.Min, &bin.XRange.Max, &bin.YRange.Min, &bin.YRange.Max, 494 &d.X.Dist.SumW, &d.X.Dist.SumW2, 495 &d.X.Stats.SumWX, &d.X.Stats.SumWX2, 496 &d.Y.Stats.SumWX, &d.Y.Stats.SumWX2, 497 &d.Stats.SumWXY, &d.X.Dist.N, 498 ) 499 if err != nil { 500 return fmt.Errorf("hbook: %q\nhbook: %w", string(buf), err) 501 } 502 d.Y.Dist = d.X.Dist 503 xset[bin.XRange.Min] = 1 504 yset[bin.YRange.Min] = 1 505 xmin = math.Min(xmin, bin.XRange.Min) 506 xmax = math.Max(xmax, bin.XRange.Max) 507 ymin = math.Min(ymin, bin.YRange.Min) 508 ymax = math.Max(ymax, bin.YRange.Max) 509 bins = append(bins, bin) 510 511 default: 512 return fmt.Errorf("hbook: invalid H2D-YODA data: %q", string(buf)) 513 } 514 } 515 h.Binning = newBinning2D(len(xset), xmin, xmax, len(yset), ymin, ymax) 516 h.Binning.Dist = dist 517 // YODA bins are transposed wrt ours 518 for ix := range h.Binning.Nx { 519 for iy := range h.Binning.Ny { 520 h.Binning.Bins[iy*h.Binning.Nx+ix] = bins[ix*h.Binning.Ny+iy] 521 } 522 } 523 return err 524 } 525 526 func (h *H2D) unmarshalYODAv2(r *rbuffer) error { 527 ann := make(Annotation) 528 529 // pos of end of annotations 530 pos := bytes.Index(r.Bytes(), []byte("\n# Mean:")) 531 if pos < 0 { 532 return fmt.Errorf("hbook: invalid H2D-YODA data") 533 } 534 err := ann.unmarshalYODAv2(r.Bytes()[:pos+1]) 535 if err != nil { 536 return fmt.Errorf("hbook: %q\nhbook: %w", string(r.Bytes()[:pos+1]), err) 537 } 538 h.annFromYODA(ann) 539 r.next(pos) 540 541 var ctx struct { 542 dist bool 543 bins bool 544 } 545 546 // sets of xlow and ylow values, to infer number of bins in X and Y. 547 xset := make(map[float64]int) 548 yset := make(map[float64]int) 549 550 var ( 551 dist Dist2D 552 bins []Bin2D 553 xmin = math.Inf(+1) 554 xmax = math.Inf(-1) 555 ymin = math.Inf(+1) 556 ymax = math.Inf(-1) 557 ) 558 s := bufio.NewScanner(r) 559 scanLoop: 560 for s.Scan() { 561 buf := s.Bytes() 562 if len(buf) == 0 || buf[0] == '#' { 563 continue 564 } 565 rbuf := bytes.NewReader(buf) 566 switch { 567 case bytes.HasPrefix(buf, []byte("END YODA_HISTO2D_V2")): 568 break scanLoop 569 case !ctx.dist && bytes.HasPrefix(buf, []byte("Total \t")): 570 ctx.dist = true 571 d := &dist 572 var n float64 573 _, err = fmt.Fscanf( 574 rbuf, 575 "Total \tTotal \t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\n", 576 &d.X.Dist.SumW, &d.X.Dist.SumW2, 577 &d.X.Stats.SumWX, &d.X.Stats.SumWX2, 578 &d.Y.Stats.SumWX, &d.Y.Stats.SumWX2, 579 &d.Stats.SumWXY, &n, 580 ) 581 if err != nil { 582 return fmt.Errorf("hbook: %q\nhbook: %w", string(buf), err) 583 } 584 d.X.Dist.N = int64(n) 585 d.Y.Dist = d.X.Dist 586 ctx.bins = true 587 case ctx.bins: 588 var bin Bin2D 589 d := &bin.Dist 590 var n float64 591 _, err = fmt.Fscanf( 592 rbuf, 593 "%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\t%e\n", 594 &bin.XRange.Min, &bin.XRange.Max, &bin.YRange.Min, &bin.YRange.Max, 595 &d.X.Dist.SumW, &d.X.Dist.SumW2, 596 &d.X.Stats.SumWX, &d.X.Stats.SumWX2, 597 &d.Y.Stats.SumWX, &d.Y.Stats.SumWX2, 598 &d.Stats.SumWXY, &n, 599 ) 600 if err != nil { 601 return fmt.Errorf("hbook: %q\nhbook: %w", string(buf), err) 602 } 603 d.X.Dist.N = int64(n) 604 d.Y.Dist = d.X.Dist 605 xset[bin.XRange.Min] = 1 606 yset[bin.YRange.Min] = 1 607 xmin = math.Min(xmin, bin.XRange.Min) 608 xmax = math.Max(xmax, bin.XRange.Max) 609 ymin = math.Min(ymin, bin.YRange.Min) 610 ymax = math.Max(ymax, bin.YRange.Max) 611 bins = append(bins, bin) 612 613 default: 614 return fmt.Errorf("hbook: invalid H2D-YODA data: %q", string(buf)) 615 } 616 } 617 h.Binning = newBinning2D(len(xset), xmin, xmax, len(yset), ymin, ymax) 618 h.Binning.Dist = dist 619 // YODA bins are transposed wrt ours 620 for ix := range h.Binning.Nx { 621 for iy := range h.Binning.Ny { 622 h.Binning.Bins[iy*h.Binning.Nx+ix] = bins[ix*h.Binning.Ny+iy] 623 } 624 } 625 return err 626 }