From ad8bed72c7e3d7720a48ffe4618bcae5f6482694 Mon Sep 17 00:00:00 2001 From: Olivier Delaune Date: Tue, 17 Mar 2020 13:50:02 +0100 Subject: [PATCH 01/10] Formatting --- hist/hist/src/TH2.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hist/hist/src/TH2.cxx b/hist/hist/src/TH2.cxx index dfd5df93f51be..9bcce99c439dc 100644 --- a/hist/hist/src/TH2.cxx +++ b/hist/hist/src/TH2.cxx @@ -1632,7 +1632,7 @@ TH2 *TH2::RebinY(Int_t ngroup, const char *newname) /// If a non-null pointer is given an error is flagged /// see RebinX and Rebin2D -TH2 * TH2::Rebin( Int_t ngroup, const char*newname, const Double_t *xbins) +TH2 *TH2::Rebin(Int_t ngroup, const char *newname, const Double_t *xbins) { if (xbins != nullptr) { Error("Rebin","Rebinning a 2-d histogram into variable bins is not supported (it is possible only for 1-d histograms). Return a nullptr"); From 80cdba3236f171bb362496fe2ad72d6b07dca096 Mon Sep 17 00:00:00 2001 From: Olivier Delaune Date: Mon, 30 Mar 2020 23:08:38 +0200 Subject: [PATCH 02/10] Implement variable rebinning 2-D histogram classes (ROOT-5224) Implemented for TProfile2D as well. --- hist/hist/inc/TH2.h | 2 +- hist/hist/inc/TProfile2D.h | 2 +- hist/hist/src/TH2.cxx | 127 ++++++++++++++++++++++++++++++----- hist/hist/src/TProfile2D.cxx | 89 ++++++++++++++++++------ 4 files changed, 180 insertions(+), 40 deletions(-) diff --git a/hist/hist/inc/TH2.h b/hist/hist/inc/TH2.h index 40ef814dde7fb..fad7abac7384d 100644 --- a/hist/hist/inc/TH2.h +++ b/hist/hist/inc/TH2.h @@ -115,7 +115,7 @@ class TH2 : public TH1 { TH2 *RebinX(Int_t ngroup=2, const char *newname="") override; // *MENU* virtual TH2 *RebinY(Int_t ngroup=2, const char *newname=""); // *MENU* TH2 *Rebin(Int_t ngroup=2, const char*newname="", const Double_t *xbins = nullptr) override; // re-implementation of the TH1 function using RebinX - virtual TH2 *Rebin2D(Int_t nxgroup=2, Int_t nygroup=2, const char *newname=""); // *MENU* + virtual TH2 *Rebin2D(Int_t nxgroup=2, Int_t nygroup=2, const char *newname="", const Double_t *xbins=0, const Double_t *ybins=0); // *MENU* TProfile *ProfileX(const char *name="_pfx", Int_t firstybin=1, Int_t lastybin=-1, Option_t *option="") const; // *MENU* TProfile *ProfileY(const char *name="_pfy", Int_t firstxbin=1, Int_t lastxbin=-1, Option_t *option="") const; // *MENU* TH1D *ProjectionX(const char *name="_px", Int_t firstybin=0, Int_t lastybin=-1, Option_t *option="") const; // *MENU* diff --git a/hist/hist/inc/TProfile2D.h b/hist/hist/inc/TProfile2D.h index 564141207c4f3..c25cfebfb2a81 100644 --- a/hist/hist/inc/TProfile2D.h +++ b/hist/hist/inc/TProfile2D.h @@ -137,7 +137,7 @@ class TProfile2D : public TH2D { TProfile *ProfileY(const char *name="_pfy", Int_t firstxbin=0, Int_t lastxbin=-1, Option_t *option="") const; // *MENU* void PutStats(Double_t *stats) override; void Reset(Option_t *option="") override; - TProfile2D *Rebin2D(Int_t nxgroup=2, Int_t nygroup=2, const char *newname="") override; + TProfile2D *Rebin2D(Int_t nxgroup=2, Int_t nygroup=2, const char *newname="", const Double_t *xbins=0, const Double_t *ybins=0) override; TProfile2D *RebinX(Int_t ngroup=2, const char *newname="") override; TProfile2D *RebinY(Int_t ngroup=2, const char *newname="") override; void SavePrimitive(std::ostream &out, Option_t *option = "") override; diff --git a/hist/hist/src/TH2.cxx b/hist/hist/src/TH2.cxx index 9bcce99c439dc..fabeb8456dbbc 100644 --- a/hist/hist/src/TH2.cxx +++ b/hist/hist/src/TH2.cxx @@ -1641,9 +1641,12 @@ TH2 *TH2::Rebin(Int_t ngroup, const char *newname, const Double_t *xbins) Info("Rebin","Rebinning only the x-axis. Use Rebin2D for rebinning both axes"); return RebinX(ngroup, newname); } + //////////////////////////////////////////////////////////////////////////////// /// Rebin this histogram grouping nxgroup/nygroup bins along the xaxis/yaxis together. /// +/// #### case 1 xbins=0 || ybins=0 +/// /// if newname is not blank a new temporary histogram hnew is created. /// else the current histogram is modified (default) /// The parameter nxgroup/nygroup indicate how many bins along the xaxis/yaxis of this @@ -1666,8 +1669,35 @@ TH2 *TH2::Rebin(Int_t ngroup, const char *newname, const Double_t *xbins) /// ybin=newybins*nygroup and the corresponding bins are added to /// the overflow bin. /// Statistics will be recomputed from the new bin contents. +/// +/// #### case 2 xbins!=0 && ybins!=0 +/// +/// A new histogram is created (you should specify newname). +/// The parameter nxgroup (nygroup) is the number of variable size bins for the x-axis +/// (y-axis) in the created histogram. +/// The arrays xbins and ybins must contain nxgroup+1 and nygroups+1 elements +/// that represent the low-edges of the x and y bins respectively. +/// If the original histogram has errors stored (via Sumw2), the resulting +/// histograms has new errors correctly calculated. +/// +/// NOTE: The bin edges specified in xbins and ybins should correspond +/// to bin edges in the original histogram. If a bin edge in the new histogram +/// is in the middle of a bin in the original histogram, all entries in +/// the split bin in the original histogram will be transfered to the +/// lower of the two possible bins in the new histogram. This is +/// probably not what you want. A warning message is emitted in this +/// case +/// +/// examples: if h2 is an existing TH2F histogram with 100 bins on x-axis +/// and 100 bins y-axis +/// +/// ~~~ {.cpp} +/// Double_t xbins[25] = {...} array of low-edges for x-axis (xbins[25] is the upper edge of last bin) +/// Double_t ybins[25] = {...} array of low-edges for y-axis (ybins[25] is the upper edge of last bin) +/// h1->Rebin(24,24,"hnew",xbins,ybins); //creates a new variable bin size histogram hnew +/// ~~~ -TH2 *TH2::Rebin2D(Int_t nxgroup, Int_t nygroup, const char *newname) +TH2 *TH2::Rebin2D(Int_t nxgroup, Int_t nygroup, const char *newname, const Double_t *xbins, const Double_t *ybins) { Int_t nxbins = fXaxis.GetNbins(); Int_t nybins = fYaxis.GetNbins(); @@ -1690,37 +1720,94 @@ TH2 *TH2::Rebin2D(Int_t nxgroup, Int_t nygroup, const char *newname) Error("Rebin2D", "Illegal value of nygroup=%d",nygroup); return nullptr; } + if (!newname && xbins) { + Error("Rebin","if xbins is specified, newname must be given"); + return 0; + } + if (!newname && ybins) { + Error("Rebin","if ybins is specified, newname must be given"); + return 0; + } Int_t newxbins = nxbins / nxgroup; - Int_t newybins = nybins / nygroup; Int_t newnx = newxbins + 2; // regular bins + overflow / underflow + if (!xbins) { + Int_t nbgx = nxbins/nxgroup; + if (nbgx*nxgroup != nxbins) { + Warning("Rebin2D", "nxgroup=%d is not an exact divider of nxbins=%d.",nxgroup,nxbins); + } + } + else { + // in the case that xbins is given (rebinning in variable bins), nxgroup is + // the new number of bins and number of grouped bins is not constant. + // when looping for setting the contents for the new histogram we + // need to loop on all bins of original histogram. Then set nxgroup=nxbins + newxbins = nxgroup; + nxgroup = nxbins; + } + + Int_t newybins = nybins / nygroup; Int_t newny = newybins + 2; // regular bins + overflow / underflow + if (!ybins) { + Int_t nbgy = nybins/nygroup; + if (nbgy*nygroup != nybins) { + Warning("Rebin2D", "nygroup=%d is not an exact divider of nybins=%d.",nygroup,nybins); + } + } + else { + // in the case that ybins is given (rebinning in variable bins), nygroup is + // the new number of bins and number of grouped bins is not constant. + // when looping for setting the contents for the new histogram we + // need to loop on all bins of original histogram. Then set nygroup=nybins + newybins = nygroup; + nygroup = nybins; + } // Save old bin contents into a new array + Double_t entries = fEntries; Double_t *oldBins = new Double_t[fNcells]; for (Int_t i = 0; i < fNcells; ++i) oldBins[i] = RetrieveBinContent(i); Double_t* oldErrors = nullptr; - if (fSumw2.fN) { + if (fSumw2.fN != 0) { oldErrors = new Double_t[fNcells]; for (Int_t i = 0; i < fNcells; ++i) oldErrors[i] = GetBinErrorSqUnchecked(i); } + // rebin will not include underflow/overflow if new axis range is larger than old axis range + if (xbins) { + if (xbins[0] < fXaxis.GetXmin() && oldBins[0] != 0 ) + Warning("Rebin2D","underflow entries for X axis will not be used when rebinning"); + if (xbins[newxbins] > fXaxis.GetXmax() && oldBins[nxbins+1] != 0 ) + Warning("Rebin2D","overflow entries for X axis will not be used when rebinning"); + } + if (ybins) { + if (ybins[0] < fYaxis.GetXmin() && oldBins[0] != 0 ) + Warning("Rebin2D","underflow entries for Y axis will not be used when rebinning"); + if (ybins[newybins] > fYaxis.GetXmax() && oldBins[nybins+1] != 0 ) + Warning("Rebin2D","overflow entries for Y axis will not be used when rebinning"); + } + // create a clone of the old histogram if newname is specified TH2* hnew = this; - if (newname && strlen(newname)) { - hnew = (TH2*)Clone(); - hnew->SetName(newname); + if ((newname && strlen(newname)) || xbins || ybins) { + hnew = (TH2*)Clone(newname); } + //reset can extend bit to avoid an axis extension in SetBinContent + UInt_t oldExtendBitMask = hnew->SetCanExtend(kNoAxis); + + // save original statistics + Double_t stat[kNstat]; + GetStats(stat); bool resetStat = false; // change axis specs and rebuild bin contents array - if(newxbins * nxgroup != nxbins) { + if(!xbins && (newxbins * nxgroup != nxbins)) { xmax = fXaxis.GetBinUpEdge(newxbins * nxgroup); resetStat = true; // stats must be reset because top bins will be moved to overflow bin } - if(newybins * nygroup != nybins) { + if(!ybins && (newybins * nygroup != nybins)) { ymax = fYaxis.GetBinUpEdge(newybins * nygroup); resetStat = true; // stats must be reset because top bins will be moved to overflow bin } @@ -1753,15 +1840,17 @@ TH2 *TH2::Rebin2D(Int_t nxgroup, Int_t nygroup, const char *newname) // copy merged bin contents (ignore under/overflows) if (nxgroup != 1 || nygroup != 1) { - if(fXaxis.GetXbins()->GetSize() > 0 || fYaxis.GetXbins()->GetSize() > 0){ + if((!xbins && fXaxis.GetXbins()->GetSize() > 0) || (!ybins && fYaxis.GetXbins()->GetSize() > 0)){ // variable bin sizes in x or y, don't treat both cases separately - Double_t *xbins = new Double_t[newxbins + 1]; - for(Int_t i = 0; i <= newxbins; ++i) xbins[i] = fXaxis.GetBinLowEdge(1 + i * nxgroup); - Double_t *ybins = new Double_t[newybins + 1]; - for(Int_t i = 0; i <= newybins; ++i) ybins[i] = fYaxis.GetBinLowEdge(1 + i * nygroup); - hnew->SetBins(newxbins, xbins, newybins, ybins); // changes also errors array (if any) - delete [] xbins; - delete [] ybins; + Double_t *xbinsTmp = new Double_t[newxbins + 1]; + for(Int_t i = 0; i <= newxbins; ++i) xbinsTmp[i] = fXaxis.GetBinLowEdge(1 + i * nxgroup); + Double_t *ybinsTmp = new Double_t[newybins + 1]; + for(Int_t i = 0; i <= newybins; ++i) ybinsTmp[i] = fYaxis.GetBinLowEdge(1 + i * nygroup); + hnew->SetBins(newxbins, xbinsTmp, newybins, ybinsTmp); // changes also errors array (if any) + delete [] xbinsTmp; + delete [] ybinsTmp; + } else if(xbins && ybins) { + hnew->SetBins(newxbins,xbins, newybins, ybins); } else { hnew->SetBins(newxbins, xmin, xmax, newybins, ymin, ymax); //changes also errors array } @@ -1839,7 +1928,11 @@ TH2 *TH2::Rebin2D(Int_t nxgroup, Int_t nygroup, const char *newname) fYaxis.SetTitleColor(yTitleColor); fYaxis.SetTitleFont(yTitleFont); - if (resetStat) hnew->ResetStats(); + hnew->SetCanExtend(oldExtendBitMask); // restore previous state + + // restore statistics and entries modified by SetBinContent + hnew->SetEntries(entries); + if (!resetStat) hnew->PutStats(stat); delete [] oldBins; if (oldErrors) delete [] oldErrors; diff --git a/hist/hist/src/TProfile2D.cxx b/hist/hist/src/TProfile2D.cxx index 09e6700e8989e..d4ccda6e19838 100644 --- a/hist/hist/src/TProfile2D.cxx +++ b/hist/hist/src/TProfile2D.cxx @@ -1551,6 +1551,8 @@ void TProfile2D::ExtendAxis(Double_t x, TAxis *axis) //////////////////////////////////////////////////////////////////////////////// /// Rebin this histogram grouping nxgroup/nygroup bins along the xaxis/yaxis together. /// +/// ## case 1 xbins=0 || ybins=0 +/// /// if newname is not blank a new profile hnew is created. /// else the current histogram is modified (default) /// The parameter nxgroup/nygroup indicate how many bins along the xaxis/yaxis of this @@ -1575,8 +1577,27 @@ void TProfile2D::ExtendAxis(Double_t x, TAxis *axis) /// ybin=newybins*nygroup and the remaining bins are added to /// the overflow bin. /// Statistics will be recomputed from the new bin contents. +/// +/// ## case 2 xbins!=0 && ybins!=0 +/// a new profile is created (you should specify newname). +/// The parameter nxgroup (nygroup) is the number of variable size bins for the x-axis +/// (y-axis) in the created profile. +/// The arrays xbins and ybins must contain nxgroup+1 and nygroup+1 elements that +/// represent the low-edge of the x and y bins respectively. +/// The data of the old bins are added to the new bin which contains the bin center +/// of the old bins. It is possible that information from the old binning are attached +/// to the under-/overflow bins of the new binning. +/// +/// examples: if hp is an existing TProfile2D with 100 bins on x-axis +/// and 100 bins y-axis +/// +/// ~~~ {.cpp} +/// Double_t xbins[25] = {...} array of low-edges for x-axis (xbins[25] is the upper edge of last bin) +/// Double_t ybins[25] = {...} array of low-edges for y-axis (ybins[25] is the upper edge of last bin) +/// hp->Rebin(24,24,"hpnew",xbins,ybins); //creates a new variable bin size profile hpnew +/// ~~~ -TProfile2D * TProfile2D::Rebin2D(Int_t nxgroup ,Int_t nygroup,const char * newname ) { +TProfile2D * TProfile2D::Rebin2D(Int_t nxgroup ,Int_t nygroup,const char * newname, const Double_t *xbins, const Double_t *ybins) { //something to do? if((nxgroup != 1) || (nygroup != 1)){ Int_t nxbins = fXaxis.GetNbins(); @@ -1586,23 +1607,46 @@ TProfile2D * TProfile2D::Rebin2D(Int_t nxgroup ,Int_t nygroup,const char * newna Double_t ymin = fYaxis.GetXmin(); Double_t ymax = fYaxis.GetXmax(); if ((nxgroup <= 0) || (nxgroup > nxbins)) { - Error("Rebin", "Illegal value of nxgroup=%d",nxgroup); + Error("Rebin2D", "Illegal value of nxgroup=%d",nxgroup); return nullptr; } if ((nygroup <= 0) || (nygroup > nybins)) { - Error("Rebin", "Illegal value of nygroup=%d",nygroup); + Error("Rebin2D", "Illegal value of nygroup=%d",nygroup); return nullptr; } Int_t newxbins = nxbins/nxgroup; - Int_t newybins = nybins/nygroup; + if (!xbins) { + Int_t nbg = nxbins/nxgroup; + //warning if bins are added to the overflow bin + if (nbg*nxgroup != nxbins) { + Warning("Rebin2D", "nxgroup=%d should be an exact divider of nxbins=%d",nxgroup,nxbins); + } + } + else { + // in the case of xbins given (rebinning in variable bins) ngroup is the new number of bins. + // and number of grouped bins is not constant. + // when looping for setting the contents for the new histogram we + // need to loop on all bins of original histogram. Set then nxgroup=nxbins + newxbins = nxgroup; + nxgroup = nxbins; + } - //warning if bins are added to the overflow bin - if(newxbins*nxgroup != nxbins) { - Warning("Rebin", "nxgroup=%d should be an exact divider of nxbins=%d",nxgroup,nxbins); + Int_t newybins = nybins/nygroup; + if (!ybins) { + Int_t nbg = nybins/nygroup; + //warning if bins are added to the overflow bin + if (nbg*nygroup != nybins) { + Warning("Rebin2D", "nygroup=%d should be an exact divider of nybins=%d",nygroup,nybins); + } } - if(newybins*nygroup != nybins) { - Warning("Rebin", "nygroup=%d should be an exact divider of nybins=%d",nygroup,nybins); + else { + // in the case of ybins given (rebinning in variable bins) ngroup is the new number of bins. + // and number of grouped bins is not constant. + // when looping for setting the contents for the new histogram we + // need to loop on all bins of original histogram. Set then nygroup=nybins + newybins = nygroup; + nygroup = nybins; } //save old bin contents in new arrays @@ -1623,35 +1667,38 @@ TProfile2D * TProfile2D::Rebin2D(Int_t nxgroup ,Int_t nygroup,const char * newna // create a clone of the old profile if newname is specified TProfile2D *hnew = this; - if(newname && strlen(newname) > 0) { + if((newname && strlen(newname) > 0) || xbins || ybins) { hnew = (TProfile2D*)Clone(newname); } // in case of nxgroup/nygroup not an exact divider of nxbins/nybins, // top limit is changed (see NOTE in method comment) - if(newxbins*nxgroup != nxbins) { + if(!xbins && (newxbins*nxgroup != nxbins)) { xmax = fXaxis.GetBinUpEdge(newxbins*nxgroup); hnew->fTsumw = 0; //stats must be reset because top bins will be moved to overflow bin } - if(newybins*nygroup != nybins) { + if(!ybins && (newybins*nygroup != nybins)) { ymax = fYaxis.GetBinUpEdge(newybins*nygroup); hnew->fTsumw = 0; //stats must be reset because top bins will be moved to overflow bin } //rebin the axis - if((fXaxis.GetXbins()->GetSize() > 0) || (fYaxis.GetXbins()->GetSize() > 0)){ - Double_t* xbins = new Double_t[newxbins+1]; - Double_t* ybins = new Double_t[newybins+1]; + if((!xbins && (fXaxis.GetXbins()->GetSize() > 0)) || (!ybins && (fYaxis.GetXbins()->GetSize() > 0))){ + // for rebinning of variable bins in a constant group + Double_t* xbinsTmp = new Double_t[newxbins+1]; + Double_t* ybinsTmp = new Double_t[newybins+1]; for(Int_t i=0; i < newxbins+1; i++) - xbins[i] = fXaxis.GetBinLowEdge(1+i*nxgroup); + xbinsTmp[i] = fXaxis.GetBinLowEdge(1+i*nxgroup); for(Int_t j=0; j < newybins+1; j++) - ybins[j] = fYaxis.GetBinLowEdge(1+j*nygroup); + ybinsTmp[j] = fYaxis.GetBinLowEdge(1+j*nygroup); hnew->SetBins(newxbins,xbins,newybins,ybins); - delete [] xbins; - delete [] ybins; - } + delete [] xbinsTmp; + delete [] ybinsTmp; + // when rebinning in variable bins + } else if (xbins && ybins) { + hnew->SetBins(newxbins,xbins,newybins,ybins); //fixed bin size - else{ + } else{ hnew->SetBins(newxbins,xmin,xmax,newybins,ymin,ymax); } From b9667eb7d384ae03fde7b83d9e62db19c7cce76c Mon Sep 17 00:00:00 2001 From: Olivier Delaune Date: Fri, 3 Apr 2020 12:08:54 +0200 Subject: [PATCH 03/10] Format comments --- hist/hist/src/TH2.cxx | 20 ++++++++++---------- hist/hist/src/TProfile2D.cxx | 16 ++++++++-------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/hist/hist/src/TH2.cxx b/hist/hist/src/TH2.cxx index fabeb8456dbbc..3233156b5246d 100644 --- a/hist/hist/src/TH2.cxx +++ b/hist/hist/src/TH2.cxx @@ -1645,11 +1645,11 @@ TH2 *TH2::Rebin(Int_t ngroup, const char *newname, const Double_t *xbins) //////////////////////////////////////////////////////////////////////////////// /// Rebin this histogram grouping nxgroup/nygroup bins along the xaxis/yaxis together. /// -/// #### case 1 xbins=0 || ybins=0 +/// #### case 1 `xbins`=0 || `ybins`=0 /// -/// if newname is not blank a new temporary histogram hnew is created. +/// if `newname` is not blank a new temporary histogram hnew is created. /// else the current histogram is modified (default) -/// The parameter nxgroup/nygroup indicate how many bins along the xaxis/yaxis of this +/// The parameters `nxgroup`/`nygroup` indicate how many bins along the xaxis/yaxis of this /// have to me merged into one bin of hnew /// If the original histogram has errors stored (via Sumw2), the resulting /// histograms has new errors correctly calculated. @@ -1663,30 +1663,30 @@ TH2 *TH2::Rebin(Int_t ngroup, const char *newname, const Double_t *xbins) /// // merging 5 bins of h1 along the yaxis in one bin /// ~~~ /// -/// NOTE : If nxgroup/nygroup is not an exact divider of the number of bins, +/// \note : If `nxgroup`/`nygroup` is not an exact divider of the number of bins, /// along the xaxis/yaxis the top limit(s) of the rebinned histogram /// is changed to the upper edge of the xbin=newxbins*nxgroup resp. /// ybin=newybins*nygroup and the corresponding bins are added to /// the overflow bin. /// Statistics will be recomputed from the new bin contents. /// -/// #### case 2 xbins!=0 && ybins!=0 +/// #### case 2 `xbins`!=0 && `ybins`!=0 /// -/// A new histogram is created (you should specify newname). -/// The parameter nxgroup (nygroup) is the number of variable size bins for the x-axis +/// A new histogram is created (you should specify `newname`). +/// The parameter `nxgroup` (`nygroup`) is the number of variable size bins for the x-axis /// (y-axis) in the created histogram. -/// The arrays xbins and ybins must contain nxgroup+1 and nygroups+1 elements +/// The arrays `xbins` and `ybins` must contain `nxgroup+1` and `nygroups+1` elements /// that represent the low-edges of the x and y bins respectively. /// If the original histogram has errors stored (via Sumw2), the resulting /// histograms has new errors correctly calculated. /// -/// NOTE: The bin edges specified in xbins and ybins should correspond +/// \note The bin edges specified in xbins and ybins should correspond /// to bin edges in the original histogram. If a bin edge in the new histogram /// is in the middle of a bin in the original histogram, all entries in /// the split bin in the original histogram will be transfered to the /// lower of the two possible bins in the new histogram. This is /// probably not what you want. A warning message is emitted in this -/// case +/// case. /// /// examples: if h2 is an existing TH2F histogram with 100 bins on x-axis /// and 100 bins y-axis diff --git a/hist/hist/src/TProfile2D.cxx b/hist/hist/src/TProfile2D.cxx index d4ccda6e19838..a8004fdfaa11d 100644 --- a/hist/hist/src/TProfile2D.cxx +++ b/hist/hist/src/TProfile2D.cxx @@ -1551,11 +1551,11 @@ void TProfile2D::ExtendAxis(Double_t x, TAxis *axis) //////////////////////////////////////////////////////////////////////////////// /// Rebin this histogram grouping nxgroup/nygroup bins along the xaxis/yaxis together. /// -/// ## case 1 xbins=0 || ybins=0 +/// ## case 1 `xbins`=0 || `ybins`=0 /// -/// if newname is not blank a new profile hnew is created. +/// if `newname` is not blank a new profile hnew is created. /// else the current histogram is modified (default) -/// The parameter nxgroup/nygroup indicate how many bins along the xaxis/yaxis of this +/// The parameters `nxgroup`/`nygroup` indicate how many bins along the xaxis/yaxis of this /// have to be merged into one bin of hnew /// If the original profile has errors stored (via Sumw2), the resulting /// profile has new errors correctly calculated. @@ -1571,18 +1571,18 @@ void TProfile2D::ExtendAxis(Double_t x, TAxis *axis) /// // merging 5 bins of hpxpy along the yaxis in one bin /// ~~~ /// -/// NOTE : If nxgroup/nygroup is not an exact divider of the number of bins, +/// \note : If `nxgroup`/`nygroup` is not an exact divider of the number of bins, /// along the xaxis/yaxis the top limit(s) of the rebinned profile /// is changed to the upper edge of the xbin=newxbins*nxgroup resp. /// ybin=newybins*nygroup and the remaining bins are added to /// the overflow bin. /// Statistics will be recomputed from the new bin contents. /// -/// ## case 2 xbins!=0 && ybins!=0 -/// a new profile is created (you should specify newname). -/// The parameter nxgroup (nygroup) is the number of variable size bins for the x-axis +/// ## case 2 `xbins`!=0 && `ybins`!=0 +/// a new profile is created (you should specify `newname`). +/// The parameter `nxgroup` (`nygroup`) is the number of variable size bins for the x-axis /// (y-axis) in the created profile. -/// The arrays xbins and ybins must contain nxgroup+1 and nygroup+1 elements that +/// The arrays `xbins` and `ybins` must contain `nxgroup+1` and `nygroup+1` elements that /// represent the low-edge of the x and y bins respectively. /// The data of the old bins are added to the new bin which contains the bin center /// of the old bins. It is possible that information from the old binning are attached From 6ec54c840bbe8498b0071eaa792df0a7064bbe16 Mon Sep 17 00:00:00 2001 From: ferdymercury Date: Tue, 1 Apr 2025 09:09:47 +0200 Subject: [PATCH 04/10] [hist] styling corrections --- hist/hist/inc/TH2.h | 2 +- hist/hist/inc/TProfile2D.h | 2 +- hist/hist/src/TH2.cxx | 6 +++--- hist/hist/src/TProfile2D.cxx | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/hist/hist/inc/TH2.h b/hist/hist/inc/TH2.h index fad7abac7384d..c783048aaeebf 100644 --- a/hist/hist/inc/TH2.h +++ b/hist/hist/inc/TH2.h @@ -115,7 +115,7 @@ class TH2 : public TH1 { TH2 *RebinX(Int_t ngroup=2, const char *newname="") override; // *MENU* virtual TH2 *RebinY(Int_t ngroup=2, const char *newname=""); // *MENU* TH2 *Rebin(Int_t ngroup=2, const char*newname="", const Double_t *xbins = nullptr) override; // re-implementation of the TH1 function using RebinX - virtual TH2 *Rebin2D(Int_t nxgroup=2, Int_t nygroup=2, const char *newname="", const Double_t *xbins=0, const Double_t *ybins=0); // *MENU* + virtual TH2 *Rebin2D(Int_t nxgroup=2, Int_t nygroup=2, const char *newname="", const Double_t *xbins=nullptr, const Double_t *ybins=nullptr); // *MENU* TProfile *ProfileX(const char *name="_pfx", Int_t firstybin=1, Int_t lastybin=-1, Option_t *option="") const; // *MENU* TProfile *ProfileY(const char *name="_pfy", Int_t firstxbin=1, Int_t lastxbin=-1, Option_t *option="") const; // *MENU* TH1D *ProjectionX(const char *name="_px", Int_t firstybin=0, Int_t lastybin=-1, Option_t *option="") const; // *MENU* diff --git a/hist/hist/inc/TProfile2D.h b/hist/hist/inc/TProfile2D.h index c25cfebfb2a81..c7c6e8778e2b6 100644 --- a/hist/hist/inc/TProfile2D.h +++ b/hist/hist/inc/TProfile2D.h @@ -137,7 +137,7 @@ class TProfile2D : public TH2D { TProfile *ProfileY(const char *name="_pfy", Int_t firstxbin=0, Int_t lastxbin=-1, Option_t *option="") const; // *MENU* void PutStats(Double_t *stats) override; void Reset(Option_t *option="") override; - TProfile2D *Rebin2D(Int_t nxgroup=2, Int_t nygroup=2, const char *newname="", const Double_t *xbins=0, const Double_t *ybins=0) override; + TProfile2D *Rebin2D(Int_t nxgroup=2, Int_t nygroup=2, const char *newname="", const Double_t *xbins=nullptr, const Double_t *ybins=nullptr) override; TProfile2D *RebinX(Int_t ngroup=2, const char *newname="") override; TProfile2D *RebinY(Int_t ngroup=2, const char *newname="") override; void SavePrimitive(std::ostream &out, Option_t *option = "") override; diff --git a/hist/hist/src/TH2.cxx b/hist/hist/src/TH2.cxx index 3233156b5246d..eaca02ede09b0 100644 --- a/hist/hist/src/TH2.cxx +++ b/hist/hist/src/TH2.cxx @@ -1663,7 +1663,7 @@ TH2 *TH2::Rebin(Int_t ngroup, const char *newname, const Double_t *xbins) /// // merging 5 bins of h1 along the yaxis in one bin /// ~~~ /// -/// \note : If `nxgroup`/`nygroup` is not an exact divider of the number of bins, +/// \note If `nxgroup`/`nygroup` is not an exact divider of the number of bins, /// along the xaxis/yaxis the top limit(s) of the rebinned histogram /// is changed to the upper edge of the xbin=newxbins*nxgroup resp. /// ybin=newybins*nygroup and the corresponding bins are added to @@ -1721,11 +1721,11 @@ TH2 *TH2::Rebin2D(Int_t nxgroup, Int_t nygroup, const char *newname, const Doubl return nullptr; } if (!newname && xbins) { - Error("Rebin","if xbins is specified, newname must be given"); + Error("Rebin2D","if xbins is specified, newname must be given"); return 0; } if (!newname && ybins) { - Error("Rebin","if ybins is specified, newname must be given"); + Error("Rebin2D","if ybins is specified, newname must be given"); return 0; } diff --git a/hist/hist/src/TProfile2D.cxx b/hist/hist/src/TProfile2D.cxx index a8004fdfaa11d..95b765c0b22d1 100644 --- a/hist/hist/src/TProfile2D.cxx +++ b/hist/hist/src/TProfile2D.cxx @@ -1571,7 +1571,7 @@ void TProfile2D::ExtendAxis(Double_t x, TAxis *axis) /// // merging 5 bins of hpxpy along the yaxis in one bin /// ~~~ /// -/// \note : If `nxgroup`/`nygroup` is not an exact divider of the number of bins, +/// \note If `nxgroup`/`nygroup` is not an exact divider of the number of bins, /// along the xaxis/yaxis the top limit(s) of the rebinned profile /// is changed to the upper edge of the xbin=newxbins*nxgroup resp. /// ybin=newybins*nygroup and the remaining bins are added to From 8a45c4048f9dc2566bc46a36345f189c9792fffe Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Sun, 6 Sep 2026 19:14:10 +0000 Subject: [PATCH 05/10] [hist] Fix variable-bin rebinning in TH2::Rebin2D MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The variable-bin code path introduced for ROOT-5224 reinterpreted the group parameters but never adapted the content-filling loops, which still advanced through the old bins with a constant stride. As a result the entire histogram content was accumulated into a single bin, and because the new bin counts were computed before the parameter reinterpretation, the loops also indexed the new histogram with a stale stride, writing out of bounds. Rebuild the merging around per-axis old-to-new bin maps computed from the old bin centers with TAxis::FindFixBin, shared in a new internal header. This maps every old cell (including under- and overflows) to the new cell containing its center, works for constant groups and for user-provided edges alike, and therefore also supports passing bin edges for only one of the two axes, which previously fell through to the fixed-width code path and silently ignored the user's edges. Like TH1::Rebin, warn when a new bin edge does not line up with an old one. Also restore the ResetStats() call for the case where the group count does not divide the number of bins: the statistics tail copied from TH1::Rebin relies on SetBinContent zeroing fTsumw, but this code uses UpdateBinContent, so the rebinned histogram kept its stale pre-rebin statistics. 🤖 Done with the help of AI --- hist/hist/src/Rebin2DHelpers.h | 116 +++++++++++++++++++ hist/hist/src/TH2.cxx | 199 ++++++++++++--------------------- 2 files changed, 189 insertions(+), 126 deletions(-) create mode 100644 hist/hist/src/Rebin2DHelpers.h diff --git a/hist/hist/src/Rebin2DHelpers.h b/hist/hist/src/Rebin2DHelpers.h new file mode 100644 index 0000000000000..2797f0b14e02d --- /dev/null +++ b/hist/hist/src/Rebin2DHelpers.h @@ -0,0 +1,116 @@ +// @(#)root/hist:$Id$ +// Author: Jonas Rembser, CERN 09/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +#ifndef ROOT_Rebin2DHelpers +#define ROOT_Rebin2DHelpers + +// Internal helpers shared by TH2::Rebin2D and TProfile2D::Rebin2D. + +#include "TAxis.h" +#include "TH1.h" +#include "TMath.h" + +#include +#include +#include + +namespace ROOT { +namespace Internal { + +/// Define the axis of the rebinned histogram: either from the user-provided +/// bin edges, or by merging groups of ngroup bins of the old axis. The value +/// of xmax is the (possibly truncated) upper limit for the uniform-bin case. +inline void DefineRebinnedAxis(const TAxis &oldAxis, Int_t ngroup, Int_t nnew, const Double_t *userBins, Double_t xmin, + Double_t xmax, TAxis &newAxis) +{ + if (userBins) { + newAxis.Set(nnew, userBins); + } else if (oldAxis.GetXbins()->GetSize() > 0) { + std::vector edges(nnew + 1); + for (Int_t i = 0; i <= nnew; ++i) + edges[i] = oldAxis.GetBinLowEdge(1 + i * ngroup); + newAxis.Set(nnew, edges.data()); + } else { + newAxis.Set(nnew, xmin, xmax); + } +} + +/// Map each cell of the old axis (including underflow 0 and overflow n+1) to +/// the cell of the new axis that contains its bin center. Old bins outside +/// the new axis range are mapped to the new under-/overflow. If checkEdges is +/// true (variable rebinning with user-provided edges), emit a warning through +/// hist when a new bin edge does not line up with an old bin edge, because +/// the entries of the bin that is split cannot be distributed correctly. +inline std::vector +MakeRebinMap(const TAxis &oldAxis, const TAxis &newAxis, bool checkEdges, TH1 &hist, const char *where) +{ + const Int_t nOld = oldAxis.GetNbins(); + const Int_t nNew = newAxis.GetNbins(); + std::vector map(nOld + 2); + map[0] = 0; + map[nOld + 1] = nNew + 1; + Int_t prev = -1; + for (Int_t o = 1; o <= nOld; ++o) { + const Int_t b = newAxis.FindFixBin(oldAxis.GetBinCenter(o)); + if (checkEdges && b != prev && b >= 1 && b <= nNew && + !TMath::AreEqualAbs(oldAxis.GetBinLowEdge(o), newAxis.GetBinLowEdge(b), + TMath::Max(1.E-8 * oldAxis.GetBinWidth(o), 1.E-16))) { + hist.Warning(where, + "Bin edge %d of rebinned histogram does not match any bin edges of the old histogram. " + "Result can be inconsistent", + b); + } + map[o] = b; + prev = b; + } + return map; +} + +/// Apply the axes of the rebinned histogram, using explicit bin edges if any +/// of the two axes has non-uniform bins. +inline void SetRebinnedBins2D(TH1 &hnew, const TAxis &newXaxis, const TAxis &newYaxis) +{ + const Int_t nx = newXaxis.GetNbins(); + const Int_t ny = newYaxis.GetNbins(); + if (newXaxis.GetXbins()->GetSize() > 0 || newYaxis.GetXbins()->GetSize() > 0) { + std::vector xEdges(nx + 1); + std::vector yEdges(ny + 1); + for (Int_t i = 0; i <= nx; ++i) + xEdges[i] = newXaxis.GetBinUpEdge(i); + for (Int_t i = 0; i <= ny; ++i) + yEdges[i] = newYaxis.GetBinUpEdge(i); + hnew.SetBins(nx, xEdges.data(), ny, yEdges.data()); // changes also errors array (if any) + } else { + hnew.SetBins(nx, newXaxis.GetXmin(), newXaxis.GetXmax(), ny, newYaxis.GetXmin(), newYaxis.GetXmax()); + } +} + +/// Accumulate every old cell (including under- and overflow) into the new +/// cell given by the per-axis bin maps, for each (old array, new array) pair. +/// The new arrays must be zero-initialized by the caller. +inline void MergeRebinnedCells(Int_t nOldX, Int_t nOldY, Int_t nNewX, const std::vector &mapX, + const std::vector &mapY, + std::initializer_list> arrays) +{ + for (Int_t oy = 0; oy < nOldY + 2; ++oy) { + for (Int_t ox = 0; ox < nOldX + 2; ++ox) { + const Int_t oldBin = ox + (nOldX + 2) * oy; + const Int_t newBin = mapX[ox] + (nNewX + 2) * mapY[oy]; + for (auto const &arr : arrays) + arr.second[newBin] += arr.first[oldBin]; + } + } +} + +} // namespace Internal +} // namespace ROOT + +#endif diff --git a/hist/hist/src/TH2.cxx b/hist/hist/src/TH2.cxx index eaca02ede09b0..846b6468fc2e3 100644 --- a/hist/hist/src/TH2.cxx +++ b/hist/hist/src/TH2.cxx @@ -28,6 +28,9 @@ #include "TVirtualHistPainter.h" #include "snprintf.h" +#include "Rebin2DHelpers.h" + +#include /** \addtogroup Histograms @{ @@ -1627,17 +1630,15 @@ TH2 *TH2::RebinY(Int_t ngroup, const char *newname) } //////////////////////////////////////////////////////////////////////////////// -/// Override TH1::Rebin as TH2::RebinX -/// Rebinning in variable binning as for TH1 is not allowed -/// If a non-null pointer is given an error is flagged +/// Override TH1::Rebin, rebinning only the X axis with the same conventions +/// as the TH1 function (`ngroup` is the number of variable size bins when +/// `xbins` is given). /// see RebinX and Rebin2D TH2 *TH2::Rebin(Int_t ngroup, const char *newname, const Double_t *xbins) { - if (xbins != nullptr) { - Error("Rebin","Rebinning a 2-d histogram into variable bins is not supported (it is possible only for 1-d histograms). Return a nullptr"); - return nullptr; - } + if (xbins != nullptr) + return Rebin2D(ngroup, 1, newname, xbins, nullptr); Info("Rebin","Rebinning only the x-axis. Use Rebin2D for rebinning both axes"); return RebinX(ngroup, newname); } @@ -1670,13 +1671,18 @@ TH2 *TH2::Rebin(Int_t ngroup, const char *newname, const Double_t *xbins) /// the overflow bin. /// Statistics will be recomputed from the new bin contents. /// -/// #### case 2 `xbins`!=0 && `ybins`!=0 +/// #### case 2 `xbins`!=0 || `ybins`!=0 /// -/// A new histogram is created (you should specify `newname`). -/// The parameter `nxgroup` (`nygroup`) is the number of variable size bins for the x-axis -/// (y-axis) in the created histogram. -/// The arrays `xbins` and `ybins` must contain `nxgroup+1` and `nygroups+1` elements -/// that represent the low-edges of the x and y bins respectively. +/// A new histogram is created and `newname` must be specified. +/// For each axis with a non-null bin-edges array, the parameter `nxgroup` +/// (`nygroup`) is the number of variable size bins for the x-axis (y-axis) in +/// the created histogram, and the array `xbins` (`ybins`) must contain +/// `nxgroup+1` (`nygroup+1`) elements that represent the low-edges of the new +/// bins plus the upper edge of the last bin. An axis without a bin-edges +/// array is rebinned in constant groups as in case 1. +/// The content of an old bin is added to the new bin containing the old bin +/// center; old bins outside the range of the new axes are added to the +/// under-/overflow bins. /// If the original histogram has errors stored (via Sumw2), the resulting /// histograms has new errors correctly calculated. /// @@ -1692,17 +1698,15 @@ TH2 *TH2::Rebin(Int_t ngroup, const char *newname, const Double_t *xbins) /// and 100 bins y-axis /// /// ~~~ {.cpp} -/// Double_t xbins[25] = {...} array of low-edges for x-axis (xbins[25] is the upper edge of last bin) -/// Double_t ybins[25] = {...} array of low-edges for y-axis (ybins[25] is the upper edge of last bin) -/// h1->Rebin(24,24,"hnew",xbins,ybins); //creates a new variable bin size histogram hnew +/// Double_t xbins[25] = {...} array of low-edges for x-axis (xbins[24] is the upper edge of last bin) +/// Double_t ybins[25] = {...} array of low-edges for y-axis (ybins[24] is the upper edge of last bin) +/// h2->Rebin2D(24,24,"hnew",xbins,ybins); //creates a new variable bin size histogram hnew /// ~~~ TH2 *TH2::Rebin2D(Int_t nxgroup, Int_t nygroup, const char *newname, const Double_t *xbins, const Double_t *ybins) { Int_t nxbins = fXaxis.GetNbins(); - Int_t nybins = fYaxis.GetNbins(); - Int_t nx = nxbins + 2; // normal bins + underflow and overflow - Int_t ny = nybins + 2; + Int_t nybins = fYaxis.GetNbins(); Double_t xmin = fXaxis.GetXmin(); Double_t xmax = fXaxis.GetXmax(); Double_t ymin = fYaxis.GetXmin(); @@ -1720,57 +1724,31 @@ TH2 *TH2::Rebin2D(Int_t nxgroup, Int_t nygroup, const char *newname, const Doubl Error("Rebin2D", "Illegal value of nygroup=%d",nygroup); return nullptr; } - if (!newname && xbins) { - Error("Rebin2D","if xbins is specified, newname must be given"); - return 0; - } - if (!newname && ybins) { - Error("Rebin2D","if ybins is specified, newname must be given"); - return 0; - } - - Int_t newxbins = nxbins / nxgroup; - Int_t newnx = newxbins + 2; // regular bins + overflow / underflow - if (!xbins) { - Int_t nbgx = nxbins/nxgroup; - if (nbgx*nxgroup != nxbins) { - Warning("Rebin2D", "nxgroup=%d is not an exact divider of nxbins=%d.",nxgroup,nxbins); - } - } - else { - // in the case that xbins is given (rebinning in variable bins), nxgroup is - // the new number of bins and number of grouped bins is not constant. - // when looping for setting the contents for the new histogram we - // need to loop on all bins of original histogram. Then set nxgroup=nxbins - newxbins = nxgroup; - nxgroup = nxbins; + if (!newname && (xbins || ybins)) { + Error("Rebin2D", "if xbins or ybins are specified, newname must be given"); + return nullptr; } - Int_t newybins = nybins / nygroup; - Int_t newny = newybins + 2; // regular bins + overflow / underflow - if (!ybins) { - Int_t nbgy = nybins/nygroup; - if (nbgy*nygroup != nybins) { - Warning("Rebin2D", "nygroup=%d is not an exact divider of nybins=%d.",nygroup,nybins); - } + // number of bins of the rebinned histogram: for an axis with user-provided + // bin edges the group parameter is directly the new number of bins, + // otherwise the old bins are merged in groups + Int_t newxbins = xbins ? nxgroup : nxbins / nxgroup; + Int_t newybins = ybins ? nygroup : nybins / nygroup; + if (!xbins && newxbins * nxgroup != nxbins) { + Warning("Rebin2D", "nxgroup=%d is not an exact divider of nxbins=%d.", nxgroup, nxbins); } - else { - // in the case that ybins is given (rebinning in variable bins), nygroup is - // the new number of bins and number of grouped bins is not constant. - // when looping for setting the contents for the new histogram we - // need to loop on all bins of original histogram. Then set nygroup=nybins - newybins = nygroup; - nygroup = nybins; + if (!ybins && newybins * nygroup != nybins) { + Warning("Rebin2D", "nygroup=%d is not an exact divider of nybins=%d.", nygroup, nybins); } // Save old bin contents into a new array Double_t entries = fEntries; - Double_t *oldBins = new Double_t[fNcells]; + std::vector oldBins(fNcells); for (Int_t i = 0; i < fNcells; ++i) oldBins[i] = RetrieveBinContent(i); - Double_t* oldErrors = nullptr; + std::vector oldErrors; if (fSumw2.fN != 0) { - oldErrors = new Double_t[fNcells]; + oldErrors.resize(fNcells); for (Int_t i = 0; i < fNcells; ++i) oldErrors[i] = GetBinErrorSqUnchecked(i); } @@ -1837,69 +1815,35 @@ TH2 *TH2::Rebin2D(Int_t nxgroup, Int_t nygroup, const char *newname, const Doubl Color_t yTitleColor = fYaxis.GetTitleColor(); Style_t yTitleFont = fYaxis.GetTitleFont(); - - // copy merged bin contents (ignore under/overflows) - if (nxgroup != 1 || nygroup != 1) { - if((!xbins && fXaxis.GetXbins()->GetSize() > 0) || (!ybins && fYaxis.GetXbins()->GetSize() > 0)){ - // variable bin sizes in x or y, don't treat both cases separately - Double_t *xbinsTmp = new Double_t[newxbins + 1]; - for(Int_t i = 0; i <= newxbins; ++i) xbinsTmp[i] = fXaxis.GetBinLowEdge(1 + i * nxgroup); - Double_t *ybinsTmp = new Double_t[newybins + 1]; - for(Int_t i = 0; i <= newybins; ++i) ybinsTmp[i] = fYaxis.GetBinLowEdge(1 + i * nygroup); - hnew->SetBins(newxbins, xbinsTmp, newybins, ybinsTmp); // changes also errors array (if any) - delete [] xbinsTmp; - delete [] ybinsTmp; - } else if(xbins && ybins) { - hnew->SetBins(newxbins,xbins, newybins, ybins); + // define the axes of the rebinned histogram and the mapping of old to new + // bins, before hnew->SetBins() below possibly modifies fXaxis/fYaxis in + // the in-place case (hnew == this) + TAxis newXaxis, newYaxis; + ROOT::Internal::DefineRebinnedAxis(fXaxis, nxgroup, newxbins, xbins, xmin, xmax, newXaxis); + ROOT::Internal::DefineRebinnedAxis(fYaxis, nygroup, newybins, ybins, ymin, ymax, newYaxis); + std::vector binMapX = ROOT::Internal::MakeRebinMap(fXaxis, newXaxis, xbins != nullptr, *this, "Rebin2D"); + std::vector binMapY = ROOT::Internal::MakeRebinMap(fYaxis, newYaxis, ybins != nullptr, *this, "Rebin2D"); + + // copy merged bin contents, including under-/overflows + if (nxgroup != 1 || nygroup != 1 || xbins || ybins) { + ROOT::Internal::SetRebinnedBins2D(*hnew, newXaxis, newYaxis); // changes also errors array (if any) + + // add the content of each old cell to the new cell that contains its bin center + const Int_t newncells = (newxbins + 2) * (newybins + 2); + std::vector newBins(newncells, 0.); + std::vector newErrors; + if (oldErrors.empty()) { + ROOT::Internal::MergeRebinnedCells(nxbins, nybins, newxbins, binMapX, binMapY, + {{oldBins.data(), newBins.data()}}); } else { - hnew->SetBins(newxbins, xmin, xmax, newybins, ymin, ymax); //changes also errors array + newErrors.resize(newncells, 0.); + ROOT::Internal::MergeRebinnedCells(nxbins, nybins, newxbins, binMapX, binMapY, + {{oldBins.data(), newBins.data()}, {oldErrors.data(), newErrors.data()}}); } - - // (0, 0): x - underflow; y - underflow - hnew->UpdateBinContent(0, oldBins[0]); - if (oldErrors) hnew->fSumw2[0] = 0; - - // (x, 0): x - regular / overflow; y - underflow - for(Int_t binx = 1, oldbinx = 1; binx < newnx; ++binx, oldbinx += nxgroup){ - Double_t binContent = 0.0, binErrorSq = 0.0; - for (Int_t i = 0; i < nxgroup && (oldbinx + i) < nx; ++i) { - Int_t bin = oldbinx + i; - binContent += oldBins[bin]; - if(oldErrors) binErrorSq += oldErrors[bin]; - } - Int_t newbin = binx; - hnew->UpdateBinContent(newbin, binContent); - if (oldErrors) hnew->fSumw2[newbin] = binErrorSq; - } - - // (0, y): x - underflow; y - regular / overflow - for(Int_t biny = 1, oldbiny = 1; biny < newny; ++biny, oldbiny += nygroup){ - Double_t binContent = 0.0, binErrorSq = 0.0; - for (Int_t j = 0; j < nygroup && (oldbiny + j) < ny; ++j) { - Int_t bin = (oldbiny + j) * nx; - binContent += oldBins[bin]; - if(oldErrors) binErrorSq += oldErrors[bin]; - } - Int_t newbin = biny * newnx; - hnew->UpdateBinContent(newbin, binContent); - if (oldErrors) hnew->fSumw2[newbin] = binErrorSq; - } - - // (x, y): x - regular / overflow; y - regular / overflow - for (Int_t binx = 1, oldbinx = 1; binx < newnx; ++binx, oldbinx += nxgroup) { - for (Int_t biny = 1, oldbiny = 1; biny < newny; ++biny, oldbiny += nygroup) { - Double_t binContent = 0.0, binErrorSq = 0.0; - for (Int_t i = 0; i < nxgroup && (oldbinx + i) < nx; ++i) { - for (Int_t j = 0; j < nygroup && (oldbiny + j) < ny; ++j) { - Int_t bin = oldbinx + i + (oldbiny + j) * nx; - binContent += oldBins[bin]; - if (oldErrors) binErrorSq += oldErrors[bin]; - } - } - Int_t newbin = binx + biny * newnx; - hnew->UpdateBinContent(newbin, binContent); - if (oldErrors) hnew->fSumw2[newbin] = binErrorSq; - } + for (Int_t i = 0; i < newncells; ++i) { + hnew->UpdateBinContent(i, newBins[i]); + if (!oldErrors.empty()) + hnew->fSumw2[i] = newErrors[i]; } } @@ -1930,12 +1874,15 @@ TH2 *TH2::Rebin2D(Int_t nxgroup, Int_t nygroup, const char *newname, const Doubl hnew->SetCanExtend(oldExtendBitMask); // restore previous state - // restore statistics and entries modified by SetBinContent - hnew->SetEntries(entries); - if (!resetStat) hnew->PutStats(stat); + // restore the statistics and entries, or recompute them when bins were + // moved to the overflow + if (resetStat) { + hnew->ResetStats(); + } else { + hnew->SetEntries(entries); + hnew->PutStats(stat); + } - delete [] oldBins; - if (oldErrors) delete [] oldErrors; return hnew; } From 52fdbd63aa82e50082756553b0d1db580f85b95f Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Sun, 6 Sep 2026 19:14:17 +0000 Subject: [PATCH 06/10] [hist] Fix variable-bin rebinning in TProfile2D::Rebin2D MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The variable-bin code path had the same broken constant-stride merging loops as TH2::Rebin2D, and in addition the constant-group branch passed the null xbins/ybins function parameters instead of the computed edge arrays to SetBins, so rebinning any TProfile2D with variable-width axes crashed with a null-pointer dereference in TAxis::Set. Rewrite the merging with the shared per-axis bin-map helpers, which also replaces the hand-written under-/overflow bookkeeping and makes single-axis variable rebinning work. Validate that a new name is given when bin edges are passed, as in TH1::Rebin. 🤖 Done with the help of AI --- hist/hist/src/TProfile2D.cxx | 423 ++++++++++------------------------- 1 file changed, 113 insertions(+), 310 deletions(-) diff --git a/hist/hist/src/TProfile2D.cxx b/hist/hist/src/TProfile2D.cxx index 95b765c0b22d1..9749be20f45a4 100644 --- a/hist/hist/src/TProfile2D.cxx +++ b/hist/hist/src/TProfile2D.cxx @@ -16,7 +16,11 @@ #include "TError.h" #include "TClass.h" #include "TProfileHelper.h" +#include "Rebin2DHelpers.h" + +#include #include +#include Bool_t TProfile2D::fgApproximate = kFALSE; @@ -1578,12 +1582,14 @@ void TProfile2D::ExtendAxis(Double_t x, TAxis *axis) /// the overflow bin. /// Statistics will be recomputed from the new bin contents. /// -/// ## case 2 `xbins`!=0 && `ybins`!=0 -/// a new profile is created (you should specify `newname`). -/// The parameter `nxgroup` (`nygroup`) is the number of variable size bins for the x-axis -/// (y-axis) in the created profile. -/// The arrays `xbins` and `ybins` must contain `nxgroup+1` and `nygroup+1` elements that -/// represent the low-edge of the x and y bins respectively. +/// ## case 2 `xbins`!=0 || `ybins`!=0 +/// a new profile is created and `newname` must be specified. +/// For each axis with a non-null bin-edges array, the parameter `nxgroup` +/// (`nygroup`) is the number of variable size bins for the x-axis (y-axis) +/// in the created profile, and the array `xbins` (`ybins`) must contain +/// `nxgroup+1` (`nygroup+1`) elements that represent the low-edges of the +/// new bins plus the upper edge of the last bin. An axis without a bin-edges +/// array is rebinned in constant groups as in case 1. /// The data of the old bins are added to the new bin which contains the bin center /// of the old bins. It is possible that information from the old binning are attached /// to the under-/overflow bins of the new binning. @@ -1592,316 +1598,113 @@ void TProfile2D::ExtendAxis(Double_t x, TAxis *axis) /// and 100 bins y-axis /// /// ~~~ {.cpp} -/// Double_t xbins[25] = {...} array of low-edges for x-axis (xbins[25] is the upper edge of last bin) -/// Double_t ybins[25] = {...} array of low-edges for y-axis (ybins[25] is the upper edge of last bin) -/// hp->Rebin(24,24,"hpnew",xbins,ybins); //creates a new variable bin size profile hpnew +/// Double_t xbins[25] = {...} array of low-edges for x-axis (xbins[24] is the upper edge of last bin) +/// Double_t ybins[25] = {...} array of low-edges for y-axis (ybins[24] is the upper edge of last bin) +/// hp->Rebin2D(24,24,"hpnew",xbins,ybins); //creates a new variable bin size profile hpnew /// ~~~ -TProfile2D * TProfile2D::Rebin2D(Int_t nxgroup ,Int_t nygroup,const char * newname, const Double_t *xbins, const Double_t *ybins) { +TProfile2D * +TProfile2D::Rebin2D(Int_t nxgroup, Int_t nygroup, const char *newname, const Double_t *xbins, const Double_t *ybins) +{ //something to do? - if((nxgroup != 1) || (nygroup != 1)){ - Int_t nxbins = fXaxis.GetNbins(); - Int_t nybins = fYaxis.GetNbins(); - Double_t xmin = fXaxis.GetXmin(); - Double_t xmax = fXaxis.GetXmax(); - Double_t ymin = fYaxis.GetXmin(); - Double_t ymax = fYaxis.GetXmax(); - if ((nxgroup <= 0) || (nxgroup > nxbins)) { - Error("Rebin2D", "Illegal value of nxgroup=%d",nxgroup); - return nullptr; - } - if ((nygroup <= 0) || (nygroup > nybins)) { - Error("Rebin2D", "Illegal value of nygroup=%d",nygroup); - return nullptr; - } - - Int_t newxbins = nxbins/nxgroup; - if (!xbins) { - Int_t nbg = nxbins/nxgroup; - //warning if bins are added to the overflow bin - if (nbg*nxgroup != nxbins) { - Warning("Rebin2D", "nxgroup=%d should be an exact divider of nxbins=%d",nxgroup,nxbins); - } - } - else { - // in the case of xbins given (rebinning in variable bins) ngroup is the new number of bins. - // and number of grouped bins is not constant. - // when looping for setting the contents for the new histogram we - // need to loop on all bins of original histogram. Set then nxgroup=nxbins - newxbins = nxgroup; - nxgroup = nxbins; - } - - Int_t newybins = nybins/nygroup; - if (!ybins) { - Int_t nbg = nybins/nygroup; - //warning if bins are added to the overflow bin - if (nbg*nygroup != nybins) { - Warning("Rebin2D", "nygroup=%d should be an exact divider of nybins=%d",nygroup,nybins); - } - } - else { - // in the case of ybins given (rebinning in variable bins) ngroup is the new number of bins. - // and number of grouped bins is not constant. - // when looping for setting the contents for the new histogram we - // need to loop on all bins of original histogram. Set then nygroup=nybins - newybins = nygroup; - nygroup = nybins; - } - - //save old bin contents in new arrays - Double_t *oldBins = new Double_t[(nxbins+2)*(nybins+2)]; - Double_t *oldCount = new Double_t[(nxbins+2)*(nybins+2)]; - Double_t *oldErrors = new Double_t[(nxbins+2)*(nybins+2)]; - Double_t *oldBinw2 = (fBinSumw2.fN ? new Double_t[(nxbins+2)*(nybins+2)] : nullptr ); - Double_t *cu1 = GetW(); - Double_t *er1 = GetW2(); - Double_t *en1 = GetB(); - Double_t *ew1 = GetB2(); - for(Int_t ibin=0; ibin < (nxbins+2)*(nybins+2); ibin++){ - oldBins[ibin] = cu1[ibin]; - oldCount[ibin] = en1[ibin]; - oldErrors[ibin] = er1[ibin]; - if (ew1 && fBinSumw2.fN) oldBinw2[ibin] = ew1[ibin]; - } - - // create a clone of the old profile if newname is specified - TProfile2D *hnew = this; - if((newname && strlen(newname) > 0) || xbins || ybins) { - hnew = (TProfile2D*)Clone(newname); - } - - // in case of nxgroup/nygroup not an exact divider of nxbins/nybins, - // top limit is changed (see NOTE in method comment) - if(!xbins && (newxbins*nxgroup != nxbins)) { - xmax = fXaxis.GetBinUpEdge(newxbins*nxgroup); - hnew->fTsumw = 0; //stats must be reset because top bins will be moved to overflow bin - } - if(!ybins && (newybins*nygroup != nybins)) { - ymax = fYaxis.GetBinUpEdge(newybins*nygroup); - hnew->fTsumw = 0; //stats must be reset because top bins will be moved to overflow bin - } - - //rebin the axis - if((!xbins && (fXaxis.GetXbins()->GetSize() > 0)) || (!ybins && (fYaxis.GetXbins()->GetSize() > 0))){ - // for rebinning of variable bins in a constant group - Double_t* xbinsTmp = new Double_t[newxbins+1]; - Double_t* ybinsTmp = new Double_t[newybins+1]; - for(Int_t i=0; i < newxbins+1; i++) - xbinsTmp[i] = fXaxis.GetBinLowEdge(1+i*nxgroup); - for(Int_t j=0; j < newybins+1; j++) - ybinsTmp[j] = fYaxis.GetBinLowEdge(1+j*nygroup); - hnew->SetBins(newxbins,xbins,newybins,ybins); - delete [] xbinsTmp; - delete [] ybinsTmp; - // when rebinning in variable bins - } else if (xbins && ybins) { - hnew->SetBins(newxbins,xbins,newybins,ybins); - //fixed bin size - } else{ - hnew->SetBins(newxbins,xmin,xmax,newybins,ymin,ymax); - } - - //merge bins - Double_t *cu2 = hnew->GetW(); - Double_t *er2 = hnew->GetW2(); - Double_t *en2 = hnew->GetB(); - Double_t *ew2 = hnew->GetB2(); - Double_t binContent, binCount, binError, binSumw2; - //connection between x and y bin number and linear global bin number: - //global bin = xbin + (nxbins+2) * ybin - Int_t oldxbin = 1; - Int_t oldybin = 1; - //global bin number - Int_t bin; - for(Int_t xbin = 1; xbin <= newxbins; xbin++){ - oldybin = 1; - for(Int_t ybin = 1; ybin <= newybins; ybin++){ - binContent = 0; - binCount = 0; - binError = 0; - binSumw2 = 0; - for(Int_t i=0; i < nxgroup; i++){ - if(oldxbin + i > nxbins) break; - for(Int_t j=0; j < nygroup; j++){ - if(oldybin + j > nybins) break; - bin = oldxbin + i + (nxbins+2)*(oldybin+j); - binContent += oldBins[bin]; - binCount += oldCount[bin]; - binError += oldErrors[bin]; - if(fBinSumw2.fN) binSumw2 += oldBinw2[bin]; - } - } - bin = xbin + (newxbins + 2)*ybin; - cu2[bin] = binContent; - er2[bin] = binError; - en2[bin] = binCount; - if(fBinSumw2.fN) ew2[bin] = binSumw2; - oldybin += nygroup; - } - oldxbin += nxgroup; - } - - //copy the underflow bin in x and y (0,0) - cu2[0] = oldBins[0]; - er2[0] = oldErrors[0]; - en2[0] = oldCount[0]; - if(fBinSumw2.fN) ew2[0] = oldBinw2[0]; - //calculate overflow bin in x and y (newxbins+1,newybins+1) - //therefore the oldxbin and oldybin from above are needed! - binContent = 0; - binCount = 0; - binError = 0; - binSumw2 = 0; - for(Int_t i=oldxbin; i <= nxbins+1; i++){ - for(Int_t j=oldybin; j <= nybins+1; j++){ - //global bin number - bin = i + (nxbins+2)*j; - binContent += oldBins[bin]; - binCount += oldCount[bin]; - binError += oldErrors[bin]; - if(fBinSumw2.fN) binSumw2 += oldBinw2[bin]; - } - } - bin = (newxbins+2)*(newybins+2)-1; - cu2[bin] = binContent; - er2[bin] = binError; - en2[bin] = binCount; - if(fBinSumw2.fN) ew2[bin] = binSumw2; - //calculate overflow bin in x and underflow bin in y (newxbins+1,0) - binContent = 0; - binCount = 0; - binError = 0; - binSumw2 = 0; - for(Int_t i=oldxbin; i <= nxbins+1; i++){ - bin = i; - binContent += oldBins[bin]; - binCount += oldCount[bin]; - binError += oldErrors[bin]; - if(fBinSumw2.fN) binSumw2 += oldBinw2[bin]; - } - bin = newxbins + 1; - cu2[bin] = binContent; - er2[bin] = binError; - en2[bin] = binCount; - if(fBinSumw2.fN) ew2[bin] = binSumw2; - //calculate underflow bin in x and overflow bin in y (0,newybins+1) - binContent = 0; - binCount = 0; - binError = 0; - binSumw2 = 0; - for(Int_t i=oldybin; i <= nybins+1; i++){ - bin = i*(nxbins + 2); - binContent += oldBins[bin]; - binCount += oldCount[bin]; - binError += oldErrors[bin]; - if(fBinSumw2.fN) binSumw2 += oldBinw2[bin]; - } - bin = (newxbins + 2)*(newybins + 1); - cu2[bin] = binContent; - er2[bin] = binError; - en2[bin] = binCount; - if(fBinSumw2.fN) ew2[bin] = binSumw2; - //calculate under/overflow contents in y for the new x bins - Double_t binContentuf, binCountuf, binErroruf, binSumw2uf; - Double_t binContentof, binCountof, binErrorof, binSumw2of; - Int_t ufbin, ofbin; - Int_t oldxbin2 = 1; - for(Int_t xbin = 1; xbin <= newxbins; xbin++){ - binContentuf = 0; - binCountuf = 0; - binErroruf = 0; - binSumw2uf = 0; - binContentof = 0; - binCountof = 0; - binErrorof = 0; - binSumw2of = 0; - for(Int_t i = 0; i < nxgroup; i++){ - //index of under/overflow bin for y in old binning - ufbin = (oldxbin2 + i); - binContentuf += oldBins[ufbin]; - binCountuf += oldCount[ufbin]; - binErroruf += oldErrors[ufbin]; - if(fBinSumw2.fN) binSumw2uf += oldBinw2[ufbin]; - for(Int_t j = oldybin; j <= nybins+1; j++) - { - ofbin = ufbin + j*(nxbins + 2); - binContentof += oldBins[ofbin]; - binCountof += oldCount[ofbin]; - binErrorof += oldErrors[ofbin]; - if(fBinSumw2.fN) binSumw2of += oldBinw2[ofbin]; - } - } - //index of under/overflow bin for y in new binning - ufbin = xbin; - ofbin = ufbin + (newybins + 1)*(newxbins + 2); - cu2[ufbin] = binContentuf; - er2[ufbin] = binErroruf; - en2[ufbin] = binCountuf; - if(fBinSumw2.fN) ew2[ufbin] = binSumw2uf; - cu2[ofbin] = binContentof; - er2[ofbin] = binErrorof; - en2[ofbin] = binCountof; - if(fBinSumw2.fN) ew2[ofbin] = binSumw2of; - - oldxbin2 += nxgroup; - } - //calculate under/overflow contents in x for the new y bins - Int_t oldybin2 = 1; - for(Int_t ybin = 1; ybin <= newybins; ybin++){ - binContentuf = 0; - binCountuf = 0; - binErroruf = 0; - binSumw2uf = 0; - binContentof = 0; - binCountof = 0; - binErrorof = 0; - binSumw2of = 0; - for(Int_t i = 0; i < nygroup; i++){ - //index of under/overflow bin for x in old binning - ufbin = (oldybin2 + i)*(nxbins+2); - binContentuf += oldBins[ufbin]; - binCountuf += oldCount[ufbin]; - binErroruf += oldErrors[ufbin]; - if(fBinSumw2.fN) binSumw2uf += oldBinw2[ufbin]; - for(Int_t j = oldxbin; j <= nxbins+1; j++) - { - ofbin = j + ufbin; - binContentof += oldBins[ofbin]; - binCountof += oldCount[ofbin]; - binErrorof += oldErrors[ofbin]; - if(fBinSumw2.fN) binSumw2of += oldBinw2[ofbin]; - } - } - //index of under/overflow bin for x in new binning - ufbin = ybin * (newxbins + 2); - ofbin = newxbins + 1 + ufbin; - cu2[ufbin] = binContentuf; - er2[ufbin] = binErroruf; - en2[ufbin] = binCountuf; - if(fBinSumw2.fN) ew2[ufbin] = binSumw2uf; - cu2[ofbin] = binContentof; - er2[ofbin] = binErrorof; - en2[ofbin] = binCountof; - if(fBinSumw2.fN) ew2[ofbin] = binSumw2of; - - oldybin2 += nygroup; - } - - delete [] oldBins; - delete [] oldCount; - delete [] oldErrors; - if (oldBinw2) delete [] oldBinw2; - - return hnew; - } - //nxgroup == nygroup == 1 - else{ - if(newname && (strlen(newname) > 0)) - return (TProfile2D*)Clone(newname); + if ((nxgroup == 1) && (nygroup == 1) && !xbins && !ybins) { + if (newname && (strlen(newname) > 0)) + return (TProfile2D *)Clone(newname); else return this; } + + Int_t nxbins = fXaxis.GetNbins(); + Int_t nybins = fYaxis.GetNbins(); + Double_t xmin = fXaxis.GetXmin(); + Double_t xmax = fXaxis.GetXmax(); + Double_t ymin = fYaxis.GetXmin(); + Double_t ymax = fYaxis.GetXmax(); + if ((nxgroup <= 0) || (nxgroup > nxbins)) { + Error("Rebin2D", "Illegal value of nxgroup=%d", nxgroup); + return nullptr; + } + if ((nygroup <= 0) || (nygroup > nybins)) { + Error("Rebin2D", "Illegal value of nygroup=%d", nygroup); + return nullptr; + } + if (!newname && (xbins || ybins)) { + Error("Rebin2D", "if xbins or ybins are specified, newname must be given"); + return nullptr; + } + + // number of bins of the rebinned profile: for an axis with user-provided + // bin edges the group parameter is directly the new number of bins, + // otherwise the old bins are merged in groups + Int_t newxbins = xbins ? nxgroup : nxbins / nxgroup; + Int_t newybins = ybins ? nygroup : nybins / nygroup; + // warning if bins are added to the overflow bin + if (!xbins && newxbins * nxgroup != nxbins) { + Warning("Rebin2D", "nxgroup=%d should be an exact divider of nxbins=%d", nxgroup, nxbins); + } + if (!ybins && newybins * nygroup != nybins) { + Warning("Rebin2D", "nygroup=%d should be an exact divider of nybins=%d", nygroup, nybins); + } + + // save old bin contents in new arrays + const Int_t ncells = (nxbins + 2) * (nybins + 2); + std::vector oldBins(GetW(), GetW() + ncells); + std::vector oldErrors(GetW2(), GetW2() + ncells); + std::vector oldCount(GetB(), GetB() + ncells); + std::vector oldBinw2; + if (fBinSumw2.fN) + oldBinw2.assign(GetB2(), GetB2() + ncells); + + // create a clone of the old profile if newname is specified + TProfile2D *hnew = this; + if ((newname && strlen(newname) > 0) || xbins || ybins) { + hnew = (TProfile2D *)Clone(newname); + } + + // in case of nxgroup/nygroup not an exact divider of nxbins/nybins, + // top limit is changed (see NOTE in method comment) + if (!xbins && (newxbins * nxgroup != nxbins)) { + xmax = fXaxis.GetBinUpEdge(newxbins * nxgroup); + hnew->fTsumw = 0; // stats must be reset because top bins will be moved to overflow bin + } + if (!ybins && (newybins * nygroup != nybins)) { + ymax = fYaxis.GetBinUpEdge(newybins * nygroup); + hnew->fTsumw = 0; // stats must be reset because top bins will be moved to overflow bin + } + + // define the axes of the rebinned profile and the mapping of old to new + // bins, before hnew->SetBins() below possibly modifies fXaxis/fYaxis in + // the in-place case (hnew == this) + TAxis newXaxis, newYaxis; + ROOT::Internal::DefineRebinnedAxis(fXaxis, nxgroup, newxbins, xbins, xmin, xmax, newXaxis); + ROOT::Internal::DefineRebinnedAxis(fYaxis, nygroup, newybins, ybins, ymin, ymax, newYaxis); + std::vector binMapX = ROOT::Internal::MakeRebinMap(fXaxis, newXaxis, xbins != nullptr, *this, "Rebin2D"); + std::vector binMapY = ROOT::Internal::MakeRebinMap(fYaxis, newYaxis, ybins != nullptr, *this, "Rebin2D"); + + // rebin the axes + ROOT::Internal::SetRebinnedBins2D(*hnew, newXaxis, newYaxis); + + // merge bins: add the content of each old cell (including under- and + // overflow) to the new cell that contains its bin center + const Int_t newncells = (newxbins + 2) * (newybins + 2); + Double_t *cu2 = hnew->GetW(); + Double_t *er2 = hnew->GetW2(); + Double_t *en2 = hnew->GetB(); + std::fill(cu2, cu2 + newncells, 0.); + std::fill(er2, er2 + newncells, 0.); + std::fill(en2, en2 + newncells, 0.); + if (fBinSumw2.fN) { + Double_t *ew2 = hnew->GetB2(); + std::fill(ew2, ew2 + newncells, 0.); + ROOT::Internal::MergeRebinnedCells( + nxbins, nybins, newxbins, binMapX, binMapY, + {{oldBins.data(), cu2}, {oldErrors.data(), er2}, {oldCount.data(), en2}, {oldBinw2.data(), ew2}}); + } else { + ROOT::Internal::MergeRebinnedCells(nxbins, nybins, newxbins, binMapX, binMapY, + {{oldBins.data(), cu2}, {oldErrors.data(), er2}, {oldCount.data(), en2}}); + } + + return hnew; } //////////////////////////////////////////////////////////////////////////////// From aa92a9a3187e7f905fc509f276c99a29d99a569a Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Sun, 6 Sep 2026 19:14:25 +0000 Subject: [PATCH 07/10] [hist] Add tests for variable-bin 2D rebinning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rebinned histograms are validated against reference histograms created directly with the target binning and filled with the same weighted pseudo-data, including under-/overflow entries, bin errors, profile bin entries and the global statistics. Also covered: the constant-group rebinning of a variable-width TProfile2D (which used to crash), the statistics recomputation when the group count does not divide the number of bins, and the diagnostics for a missing new name and for misaligned bin edges. 🤖 Done with the help of AI --- hist/hist/test/CMakeLists.txt | 1 + hist/hist/test/test_rebin2D.cxx | 214 ++++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 hist/hist/test/test_rebin2D.cxx diff --git a/hist/hist/test/CMakeLists.txt b/hist/hist/test/CMakeLists.txt index 3a9a1623c6d42..f970aa730ff7f 100644 --- a/hist/hist/test/CMakeLists.txt +++ b/hist/hist/test/CMakeLists.txt @@ -13,6 +13,7 @@ ROOT_ADD_GTEST(testTH2PolyGetNumberOfBins test_TH2Poly_GetNumberOfBins.cxx LIBRA ROOT_ADD_GTEST(testTHn THn.cxx LIBRARIES Hist Matrix MathCore RIO) ROOT_ADD_GTEST(testTH1 test_TH1.cxx LIBRARIES Hist) ROOT_ADD_GTEST(testTH2 test_TH2.cxx LIBRARIES Hist) +ROOT_ADD_GTEST(testRebin2D test_rebin2D.cxx LIBRARIES Hist MathCore) ROOT_ADD_GTEST(testTH3 test_TH3.cxx LIBRARIES Hist) ROOT_ADD_GTEST(testTHStack test_THStack.cxx LIBRARIES Hist) ROOT_ADD_GTEST(testProject3Dname test_Project3D_name.cxx LIBRARIES Hist) diff --git a/hist/hist/test/test_rebin2D.cxx b/hist/hist/test/test_rebin2D.cxx new file mode 100644 index 0000000000000..a0d78525891b2 --- /dev/null +++ b/hist/hist/test/test_rebin2D.cxx @@ -0,0 +1,214 @@ +// Tests for rebinning TH2 and TProfile2D with variable bin edges (ROOT-5224). +// The rebinned histograms are compared against reference histograms created +// directly with the target binning and filled with the same pseudo-data. + +#include "gtest/gtest.h" + +#include "ROOT/TestSupport.hxx" + +#include "TH2D.h" +#include "TProfile2D.h" +#include "TRandom3.h" + +#include +#include +#include + +namespace { + +void fillOne(TH2D &h, double x, double y, double, double w) +{ + h.Fill(x, y, w); +} + +void fillOne(TProfile2D &p, double x, double y, double z, double w) +{ + p.Fill(x, y, z, w); +} + +// Fill both histograms with the same weighted pseudo-data, including entries +// that end up in the under- and overflow bins. +template +void fillSame(Hist &a, Hist &b) +{ + TRandom3 rng(42); + for (int i = 0; i < 10000; ++i) { + const double x = rng.Uniform(-10., 110.); + const double y = rng.Uniform(-10., 110.); + const double z = rng.Gaus(5., 1.); + const double w = rng.Uniform(0.1, 2.); + fillOne(a, x, y, z, w); + fillOne(b, x, y, z, w); + } +} + +void expectSameBinsAndStats(const TH2 &h, const TH2 &ref, const std::string &ctx) +{ + ASSERT_EQ(h.GetNbinsX(), ref.GetNbinsX()) << ctx; + ASSERT_EQ(h.GetNbinsY(), ref.GetNbinsY()) << ctx; + for (Int_t j = 0; j <= ref.GetNbinsY() + 1; ++j) { + for (Int_t i = 0; i <= ref.GetNbinsX() + 1; ++i) { + const std::string binCtx = ctx + " bin (" + std::to_string(i) + "," + std::to_string(j) + ")"; + const Int_t bin = ref.GetBin(i, j); + const double refContent = ref.GetBinContent(bin); + const double refError = ref.GetBinError(bin); + EXPECT_NEAR(h.GetBinContent(bin), refContent, 1e-6 * std::max(1., std::abs(refContent))) << binCtx; + EXPECT_NEAR(h.GetBinError(bin), refError, 1e-6 * std::max(1., refError)) << binCtx; + } + } + EXPECT_NEAR(h.GetEntries(), ref.GetEntries(), 1e-6) << ctx; + EXPECT_NEAR(h.GetMean(1), ref.GetMean(1), 1e-9) << ctx; + EXPECT_NEAR(h.GetMean(2), ref.GetMean(2), 1e-9) << ctx; + EXPECT_NEAR(h.GetStdDev(1), ref.GetStdDev(1), 1e-9) << ctx; +} + +const double xEdges[5] = {0., 10., 40., 70., 100.}; +const double yEdges[5] = {0., 30., 50., 90., 100.}; + +} // namespace + +// Rebin a uniform TH2 into variable bins on both axes. +TEST(Rebin2DVariable, TH2BothAxes) +{ + TH2D fine("fine", "fine", 100, 0., 100., 100, 0., 100.); + fine.Sumw2(); + TH2D ref("ref", "ref", 4, xEdges, 4, yEdges); + ref.Sumw2(); + fillSame(fine, ref); + + std::unique_ptr hnew{fine.Rebin2D(4, 4, "hnew", xEdges, yEdges)}; + ASSERT_NE(hnew, nullptr); + ASSERT_NE(hnew.get(), &fine); + expectSameBinsAndStats(*hnew, ref, "TH2BothAxes"); +} + +// Variable bins on the x-axis only: the y-axis is grouped by a constant factor. +TEST(Rebin2DVariable, TH2SingleAxis) +{ + TH2D fine("fine", "fine", 100, 0., 100., 100, 0., 100.); + fine.Sumw2(); + TH2D ref("ref", "ref", 4, xEdges, 50, 0., 100.); + ref.Sumw2(); + fillSame(fine, ref); + + std::unique_ptr hnew{fine.Rebin2D(4, 2, "hnew", xEdges, nullptr)}; + ASSERT_NE(hnew, nullptr); + expectSameBinsAndStats(*hnew, ref, "TH2SingleAxis"); +} + +// TH2::Rebin with bin edges forwards to Rebin2D with the TH1 conventions, +// leaving the y-axis untouched. +TEST(Rebin2DVariable, TH2RebinForwards) +{ + TH2D fine("fine", "fine", 100, 0., 100., 100, 0., 100.); + fine.Sumw2(); + TH2D ref("ref", "ref", 4, xEdges, 100, 0., 100.); + ref.Sumw2(); + fillSame(fine, ref); + + std::unique_ptr hnew{fine.Rebin(4, "hnew", xEdges)}; + ASSERT_NE(hnew, nullptr); + expectSameBinsAndStats(*hnew, ref, "TH2RebinForwards"); +} + +// Rebinning a variable-bin TH2 back into a coarser variable binning whose +// edges are a subset of the original ones. +TEST(Rebin2DVariable, TH2VariableSource) +{ + TH2D fine("fine", "fine", 4, xEdges, 4, yEdges); + fine.Sumw2(); + const double xCoarse[3] = {0., 40., 100.}; + const double yCoarse[3] = {0., 50., 100.}; + TH2D ref("ref", "ref", 2, xCoarse, 2, yCoarse); + ref.Sumw2(); + fillSame(fine, ref); + + std::unique_ptr hnew{fine.Rebin2D(2, 2, "hnew", xCoarse, yCoarse)}; + ASSERT_NE(hnew, nullptr); + expectSameBinsAndStats(*hnew, ref, "TH2VariableSource"); +} + +// Rebin a uniform TProfile2D into variable bins on both axes. +TEST(Rebin2DVariable, Profile2DBothAxes) +{ + TProfile2D fine("fine", "fine", 100, 0., 100., 100, 0., 100.); + fine.Sumw2(); + TProfile2D ref("ref", "ref", 4, xEdges, 4, yEdges); + ref.Sumw2(); + fillSame(fine, ref); + + std::unique_ptr pnew{fine.Rebin2D(4, 4, "pnew", xEdges, yEdges)}; + ASSERT_NE(pnew, nullptr); + expectSameBinsAndStats(*pnew, ref, "Profile2DBothAxes"); + for (Int_t j = 0; j <= ref.GetNbinsY() + 1; ++j) { + for (Int_t i = 0; i <= ref.GetNbinsX() + 1; ++i) { + const Int_t bin = ref.GetBin(i, j); + EXPECT_NEAR(pnew->GetBinEntries(bin), ref.GetBinEntries(bin), 1e-6) + << "Profile2DBothAxes entries bin (" << i << "," << j << ")"; + } + } +} + +// Constant-group rebinning of a TProfile2D that has variable-width axes +// (regression test: this used to pass null bin edges to SetBins). +TEST(Rebin2DVariable, Profile2DVariableSourceConstantGroups) +{ + TProfile2D fine("fine", "fine", 4, xEdges, 4, yEdges); + const double xCoarse[3] = {0., 40., 100.}; + const double yCoarse[3] = {0., 50., 100.}; + TProfile2D ref("ref", "ref", 2, xCoarse, 2, yCoarse); + fillSame(fine, ref); + + std::unique_ptr pnew{fine.Rebin2D(2, 2, "pnew")}; + ASSERT_NE(pnew, nullptr); + expectSameBinsAndStats(*pnew, ref, "Profile2DVariableSourceConstantGroups"); +} + +// When the group count does not divide the number of bins, the top bins move +// to the overflow and the statistics must be recomputed from the bin contents. +TEST(Rebin2DVariable, TH2NonDivisorGroupStats) +{ + TH2D h("h", "h", 10, 0., 10., 10, 0., 10.); + TRandom3 rng(43); + for (int i = 0; i < 10000; ++i) + h.Fill(rng.Uniform(0., 10.), rng.Uniform(0., 10.)); + + { + ROOT::TestSupport::CheckDiagsRAII checkDiag; + checkDiag.requiredDiag(kWarning, "TH2D::Rebin2D", "is not an exact divider", false); + ASSERT_EQ(h.Rebin2D(3, 3), &h); // in-place + } + ASSERT_EQ(h.GetNbinsX(), 3); + ASSERT_DOUBLE_EQ(h.GetXaxis()->GetXmax(), 9.); + + // reference statistics recomputed from the rebinned contents + double sw = 0., swx = 0., swx2 = 0.; + for (Int_t j = 1; j <= h.GetNbinsY(); ++j) { + for (Int_t i = 1; i <= h.GetNbinsX(); ++i) { + const double c = h.GetBinContent(i, j); + const double x = h.GetXaxis()->GetBinCenter(i); + sw += c; + swx += c * x; + swx2 += c * x * x; + } + } + EXPECT_NEAR(h.GetMean(1), swx / sw, 1e-9); + EXPECT_NEAR(h.GetStdDev(1), std::sqrt(swx2 / sw - (swx / sw) * (swx / sw)), 1e-9); +} + +// Variable-bin rebinning requires a new name, and warns when a new bin edge +// does not line up with an old bin edge. +TEST(Rebin2DVariable, Diagnostics) +{ + TH2D h("h", "h", 100, 0., 100., 100, 0., 100.); + { + ROOT::TestSupport::CheckDiagsRAII checkDiag(kError, "TH2D::Rebin2D", "newname must be given", false); + EXPECT_EQ(h.Rebin2D(4, 4, nullptr, xEdges, yEdges), nullptr); + } + { + ROOT::TestSupport::CheckDiagsRAII checkDiag(kWarning, "TH2D::Rebin2D", "does not match any bin edges", false); + const double misaligned[3] = {0., 10.5, 100.}; + std::unique_ptr hnew{h.Rebin2D(2, 2, "hnew", misaligned, nullptr)}; + EXPECT_NE(hnew, nullptr); + } +} From 5eb77450dd337aae4e01a9e511287373cba7948f Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Sun, 6 Sep 2026 19:48:44 +0000 Subject: [PATCH 08/10] [hist] Address review findings for variable-bin 2D rebinning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidate the per-axis setup of Rebin2D (validation, computation of the new number of bins, divider warning, axis truncation for non-dividing group counts, axis definition and old-to-new bin map) into a shared SetupRebinnedAxis helper, removing the diverging duplicated preambles of TH2 and TProfile2D. Behavior fixes on top of the consolidation: - Reject an empty newname (not only a null one) when bin edges are passed: Clone("") would silently register a second histogram under the original name. - Check the misaligned-edge warning per new bin edge against the old axis instead of per old-bin-group: the previous check could not fire for old bins mapped to the flow bins or for new bins that receive no old bin center. - Base the under-/overflow "will not be used" warnings on the content of the full flow row/column instead of single corner cells, whose 1D-style indexing was wrong for 2D arrays, and emit them for TProfile2D too. - Drop the stats save/restore in TH2::Rebin2D, which was copied from TH1::Rebin where it compensates for SetBinContent zeroing fTsumw; with UpdateBinContent it was redundant and could bake in stats computed from the old bin centers when fTsumw was zero. Only ResetStats() for a truncating rebin is needed. - Drop the SetCanExtend(kNoAxis) guard for the same reason: nothing in the rewritten flow can extend an axis. - Give TH2::Rebin2D the same no-op early return for nxgroup == nygroup == 1 that TProfile2D has. - Unify the divider warning text between TH2 and TProfile2D. 🤖 Done with the help of AI --- hist/hist/src/Rebin2DHelpers.h | 102 +++++++++++++++++---- hist/hist/src/TH2.cxx | 157 +++++++++++--------------------- hist/hist/src/TProfile2D.cxx | 79 ++++++---------- hist/hist/test/test_rebin2D.cxx | 14 +++ 4 files changed, 181 insertions(+), 171 deletions(-) diff --git a/hist/hist/src/Rebin2DHelpers.h b/hist/hist/src/Rebin2DHelpers.h index 2797f0b14e02d..83c6dbce018ea 100644 --- a/hist/hist/src/Rebin2DHelpers.h +++ b/hist/hist/src/Rebin2DHelpers.h @@ -45,33 +45,101 @@ inline void DefineRebinnedAxis(const TAxis &oldAxis, Int_t ngroup, Int_t nnew, c /// Map each cell of the old axis (including underflow 0 and overflow n+1) to /// the cell of the new axis that contains its bin center. Old bins outside -/// the new axis range are mapped to the new under-/overflow. If checkEdges is -/// true (variable rebinning with user-provided edges), emit a warning through -/// hist when a new bin edge does not line up with an old bin edge, because -/// the entries of the bin that is split cannot be distributed correctly. -inline std::vector -MakeRebinMap(const TAxis &oldAxis, const TAxis &newAxis, bool checkEdges, TH1 &hist, const char *where) +/// the new axis range are mapped to the new under-/overflow. +inline std::vector MakeRebinMap(const TAxis &oldAxis, const TAxis &newAxis) { const Int_t nOld = oldAxis.GetNbins(); - const Int_t nNew = newAxis.GetNbins(); std::vector map(nOld + 2); map[0] = 0; - map[nOld + 1] = nNew + 1; - Int_t prev = -1; - for (Int_t o = 1; o <= nOld; ++o) { - const Int_t b = newAxis.FindFixBin(oldAxis.GetBinCenter(o)); - if (checkEdges && b != prev && b >= 1 && b <= nNew && - !TMath::AreEqualAbs(oldAxis.GetBinLowEdge(o), newAxis.GetBinLowEdge(b), - TMath::Max(1.E-8 * oldAxis.GetBinWidth(o), 1.E-16))) { + map[nOld + 1] = newAxis.GetNbins() + 1; + for (Int_t o = 1; o <= nOld; ++o) + map[o] = newAxis.FindFixBin(oldAxis.GetBinCenter(o)); + return map; +} + +/// Warn when a bin edge of the new axis that lies inside the old axis range +/// does not line up with a bin edge of the old axis: the entries of the old +/// bin that is split cannot be distributed correctly. +inline void WarnAboutMisalignedEdges(const TAxis &oldAxis, const TAxis &newAxis, TH1 &hist, const char *where) +{ + for (Int_t b = 0; b <= newAxis.GetNbins(); ++b) { + const Double_t edge = newAxis.GetBinUpEdge(b); // GetBinUpEdge(0) is the axis minimum + if (edge <= oldAxis.GetXmin() || edge >= oldAxis.GetXmax()) + continue; + const Int_t o = oldAxis.FindFixBin(edge); + const Double_t tol = TMath::Max(1.E-8 * oldAxis.GetBinWidth(o), 1.E-16); + if (!TMath::AreEqualAbs(edge, oldAxis.GetBinLowEdge(o), tol) && + !TMath::AreEqualAbs(edge, oldAxis.GetBinUpEdge(o), tol)) { hist.Warning(where, "Bin edge %d of rebinned histogram does not match any bin edges of the old histogram. " "Result can be inconsistent", b); } - map[o] = b; - prev = b; } - return map; +} + +/// The definition of one axis of the rebinned histogram. +struct RebinnedAxisInfo { + Int_t nNewBins = 0; ///< number of bins of the rebinned axis + TAxis newAxis; ///< the rebinned axis + std::vector binMap; ///< map from old cell (0..n+1) to new cell + bool truncated = false; ///< the group count does not divide the old bin count: top bins move to the overflow +}; + +/// Validate the rebinning parameters for one axis and fill the definition of +/// the rebinned axis and the map from old to new bins. For an axis with +/// user-provided bin edges, ngroup is directly the new number of bins, +/// otherwise the old bins are merged in groups of ngroup. Returns false on an +/// invalid group count. +inline bool SetupRebinnedAxis(const TAxis &oldAxis, Int_t ngroup, const Double_t *userBins, char axisName, TH1 &hist, + const char *where, RebinnedAxisInfo &info) +{ + const Int_t nOldBins = oldAxis.GetNbins(); + if (ngroup <= 0 || ngroup > nOldBins) { + hist.Error(where, "Illegal value of n%cgroup=%d", axisName, ngroup); + return false; + } + Double_t newMax = oldAxis.GetXmax(); + if (userBins) { + info.nNewBins = ngroup; + } else { + info.nNewBins = nOldBins / ngroup; + if (info.nNewBins * ngroup != nOldBins) { + hist.Warning(where, "n%cgroup=%d is not an exact divider of n%cbins=%d.", axisName, ngroup, axisName, + nOldBins); + // the top limit is truncated and the top bins move to the overflow + newMax = oldAxis.GetBinUpEdge(info.nNewBins * ngroup); + info.truncated = true; + } + } + DefineRebinnedAxis(oldAxis, ngroup, info.nNewBins, userBins, oldAxis.GetXmin(), newMax, info.newAxis); + if (userBins) + WarnAboutMisalignedEdges(oldAxis, info.newAxis, hist, where); + info.binMap = MakeRebinMap(oldAxis, info.newAxis); + return true; +} + +/// Warn when the range of the new axis extends beyond the old one while the +/// corresponding flow bins hold content: that content stays in the flow bins +/// and is not redistributed into the range of the new axis. The flow bins of +/// the mapped axis are addressed as flowIndex * stride + k * otherStride for +/// the nOther cells of the other axis. +inline void WarnAboutUnusedFlowContent(const TAxis &oldAxis, const RebinnedAxisInfo &info, const Double_t *userBins, + char axisName, const Double_t *bins, Int_t stride, Int_t nOther, + Int_t otherStride, TH1 &hist, const char *where) +{ + if (!userBins) + return; + auto flowContent = [&](Int_t flowIndex) { + Double_t sum = 0.; + for (Int_t k = 0; k < nOther; ++k) + sum += bins[flowIndex * stride + k * otherStride]; + return sum; + }; + if (userBins[0] < oldAxis.GetXmin() && flowContent(0) != 0.) + hist.Warning(where, "underflow entries for %c axis will not be used when rebinning", axisName); + if (userBins[info.nNewBins] > oldAxis.GetXmax() && flowContent(oldAxis.GetNbins() + 1) != 0.) + hist.Warning(where, "overflow entries for %c axis will not be used when rebinning", axisName); } /// Apply the axes of the rebinned histogram, using explicit bin edges if any diff --git a/hist/hist/src/TH2.cxx b/hist/hist/src/TH2.cxx index 846b6468fc2e3..349e5bcc6daef 100644 --- a/hist/hist/src/TH2.cxx +++ b/hist/hist/src/TH2.cxx @@ -1705,89 +1705,56 @@ TH2 *TH2::Rebin(Int_t ngroup, const char *newname, const Double_t *xbins) TH2 *TH2::Rebin2D(Int_t nxgroup, Int_t nygroup, const char *newname, const Double_t *xbins, const Double_t *ybins) { - Int_t nxbins = fXaxis.GetNbins(); - Int_t nybins = fYaxis.GetNbins(); - Double_t xmin = fXaxis.GetXmin(); - Double_t xmax = fXaxis.GetXmax(); - Double_t ymin = fYaxis.GetXmin(); - Double_t ymax = fYaxis.GetXmax(); - if (GetDimension() != 2) { Error("Rebin2D", "Histogram must be TH2. This histogram has %d dimensions.", GetDimension()); return nullptr; } - if ((nxgroup <= 0) || (nxgroup > nxbins)) { - Error("Rebin2D", "Illegal value of nxgroup=%d",nxgroup); - return nullptr; - } - if ((nygroup <= 0) || (nygroup > nybins)) { - Error("Rebin2D", "Illegal value of nygroup=%d",nygroup); - return nullptr; + // something to do? + if (nxgroup == 1 && nygroup == 1 && !xbins && !ybins) { + return (newname && strlen(newname) > 0) ? (TH2 *)Clone(newname) : this; } - if (!newname && (xbins || ybins)) { + if ((!newname || strlen(newname) == 0) && (xbins || ybins)) { Error("Rebin2D", "if xbins or ybins are specified, newname must be given"); return nullptr; } - // number of bins of the rebinned histogram: for an axis with user-provided - // bin edges the group parameter is directly the new number of bins, - // otherwise the old bins are merged in groups - Int_t newxbins = xbins ? nxgroup : nxbins / nxgroup; - Int_t newybins = ybins ? nygroup : nybins / nygroup; - if (!xbins && newxbins * nxgroup != nxbins) { - Warning("Rebin2D", "nxgroup=%d is not an exact divider of nxbins=%d.", nxgroup, nxbins); - } - if (!ybins && newybins * nygroup != nybins) { - Warning("Rebin2D", "nygroup=%d is not an exact divider of nybins=%d.", nygroup, nybins); + const Int_t nxbins = fXaxis.GetNbins(); + const Int_t nybins = fYaxis.GetNbins(); + + // validate the parameters and define the axes of the rebinned histogram + // and the mapping of old to new bins + ROOT::Internal::RebinnedAxisInfo infoX, infoY; + if (!ROOT::Internal::SetupRebinnedAxis(fXaxis, nxgroup, xbins, 'x', *this, "Rebin2D", infoX) || + !ROOT::Internal::SetupRebinnedAxis(fYaxis, nygroup, ybins, 'y', *this, "Rebin2D", infoY)) { + return nullptr; } + const Int_t newxbins = infoX.nNewBins; + const Int_t newybins = infoY.nNewBins; // Save old bin contents into a new array - Double_t entries = fEntries; std::vector oldBins(fNcells); - for (Int_t i = 0; i < fNcells; ++i) oldBins[i] = RetrieveBinContent(i); + for (Int_t i = 0; i < fNcells; ++i) + oldBins[i] = RetrieveBinContent(i); std::vector oldErrors; if (fSumw2.fN != 0) { oldErrors.resize(fNcells); - for (Int_t i = 0; i < fNcells; ++i) oldErrors[i] = GetBinErrorSqUnchecked(i); + for (Int_t i = 0; i < fNcells; ++i) + oldErrors[i] = GetBinErrorSqUnchecked(i); } - // rebin will not include underflow/overflow if new axis range is larger than old axis range - if (xbins) { - if (xbins[0] < fXaxis.GetXmin() && oldBins[0] != 0 ) - Warning("Rebin2D","underflow entries for X axis will not be used when rebinning"); - if (xbins[newxbins] > fXaxis.GetXmax() && oldBins[nxbins+1] != 0 ) - Warning("Rebin2D","overflow entries for X axis will not be used when rebinning"); - } - if (ybins) { - if (ybins[0] < fYaxis.GetXmin() && oldBins[0] != 0 ) - Warning("Rebin2D","underflow entries for Y axis will not be used when rebinning"); - if (ybins[newybins] > fYaxis.GetXmax() && oldBins[nybins+1] != 0 ) - Warning("Rebin2D","overflow entries for Y axis will not be used when rebinning"); - } - - // create a clone of the old histogram if newname is specified - TH2* hnew = this; - if ((newname && strlen(newname)) || xbins || ybins) { - hnew = (TH2*)Clone(newname); - } - - //reset can extend bit to avoid an axis extension in SetBinContent - UInt_t oldExtendBitMask = hnew->SetCanExtend(kNoAxis); - - // save original statistics - Double_t stat[kNstat]; - GetStats(stat); - bool resetStat = false; - - // change axis specs and rebuild bin contents array - if(!xbins && (newxbins * nxgroup != nxbins)) { - xmax = fXaxis.GetBinUpEdge(newxbins * nxgroup); - resetStat = true; // stats must be reset because top bins will be moved to overflow bin - } - if(!ybins && (newybins * nygroup != nybins)) { - ymax = fYaxis.GetBinUpEdge(newybins * nygroup); - resetStat = true; // stats must be reset because top bins will be moved to overflow bin + // rebinning will not redistribute under-/overflow content into the range + // of new axes that extend beyond the old ones + ROOT::Internal::WarnAboutUnusedFlowContent(fXaxis, infoX, xbins, 'X', oldBins.data(), 1, nybins + 2, nxbins + 2, + *this, "Rebin2D"); + ROOT::Internal::WarnAboutUnusedFlowContent(fYaxis, infoY, ybins, 'Y', oldBins.data(), nxbins + 2, nxbins + 2, 1, + *this, "Rebin2D"); + + // create a clone of the old histogram if newname is specified (guaranteed + // when bin edges are passed) + TH2 *hnew = this; + if (newname && strlen(newname) > 0) { + hnew = (TH2 *)Clone(newname); } // save the TAttAxis members (reset by SetBins) for x axis @@ -1815,36 +1782,25 @@ TH2 *TH2::Rebin2D(Int_t nxgroup, Int_t nygroup, const char *newname, const Doubl Color_t yTitleColor = fYaxis.GetTitleColor(); Style_t yTitleFont = fYaxis.GetTitleFont(); - // define the axes of the rebinned histogram and the mapping of old to new - // bins, before hnew->SetBins() below possibly modifies fXaxis/fYaxis in - // the in-place case (hnew == this) - TAxis newXaxis, newYaxis; - ROOT::Internal::DefineRebinnedAxis(fXaxis, nxgroup, newxbins, xbins, xmin, xmax, newXaxis); - ROOT::Internal::DefineRebinnedAxis(fYaxis, nygroup, newybins, ybins, ymin, ymax, newYaxis); - std::vector binMapX = ROOT::Internal::MakeRebinMap(fXaxis, newXaxis, xbins != nullptr, *this, "Rebin2D"); - std::vector binMapY = ROOT::Internal::MakeRebinMap(fYaxis, newYaxis, ybins != nullptr, *this, "Rebin2D"); - - // copy merged bin contents, including under-/overflows - if (nxgroup != 1 || nygroup != 1 || xbins || ybins) { - ROOT::Internal::SetRebinnedBins2D(*hnew, newXaxis, newYaxis); // changes also errors array (if any) - - // add the content of each old cell to the new cell that contains its bin center - const Int_t newncells = (newxbins + 2) * (newybins + 2); - std::vector newBins(newncells, 0.); - std::vector newErrors; - if (oldErrors.empty()) { - ROOT::Internal::MergeRebinnedCells(nxbins, nybins, newxbins, binMapX, binMapY, - {{oldBins.data(), newBins.data()}}); - } else { - newErrors.resize(newncells, 0.); - ROOT::Internal::MergeRebinnedCells(nxbins, nybins, newxbins, binMapX, binMapY, - {{oldBins.data(), newBins.data()}, {oldErrors.data(), newErrors.data()}}); - } - for (Int_t i = 0; i < newncells; ++i) { - hnew->UpdateBinContent(i, newBins[i]); - if (!oldErrors.empty()) - hnew->fSumw2[i] = newErrors[i]; - } + ROOT::Internal::SetRebinnedBins2D(*hnew, infoX.newAxis, infoY.newAxis); // changes also errors array (if any) + + // add the content of each old cell (including under- and overflows) to + // the new cell that contains its bin center + const Int_t newncells = (newxbins + 2) * (newybins + 2); + std::vector newBins(newncells, 0.); + std::vector newErrors; + if (oldErrors.empty()) { + ROOT::Internal::MergeRebinnedCells(nxbins, nybins, newxbins, infoX.binMap, infoY.binMap, + {{oldBins.data(), newBins.data()}}); + } else { + newErrors.resize(newncells, 0.); + ROOT::Internal::MergeRebinnedCells(nxbins, nybins, newxbins, infoX.binMap, infoY.binMap, + {{oldBins.data(), newBins.data()}, {oldErrors.data(), newErrors.data()}}); + } + for (Int_t i = 0; i < newncells; ++i) { + hnew->UpdateBinContent(i, newBins[i]); + if (!oldErrors.empty()) + hnew->fSumw2[i] = newErrors[i]; } // Restore x axis attributes @@ -1872,21 +1828,14 @@ TH2 *TH2::Rebin2D(Int_t nxgroup, Int_t nygroup, const char *newname, const Doubl fYaxis.SetTitleColor(yTitleColor); fYaxis.SetTitleFont(yTitleFont); - hnew->SetCanExtend(oldExtendBitMask); // restore previous state - - // restore the statistics and entries, or recompute them when bins were - // moved to the overflow - if (resetStat) { + // when the group count does not divide the old bin count, the top bins + // moved to the overflow: recompute the statistics from the bin contents + if (infoX.truncated || infoY.truncated) hnew->ResetStats(); - } else { - hnew->SetEntries(entries); - hnew->PutStats(stat); - } return hnew; } - //////////////////////////////////////////////////////////////////////////////// TProfile *TH2::DoProfile(bool onX, const char *name, Int_t firstbin, Int_t lastbin, Option_t *option) const diff --git a/hist/hist/src/TProfile2D.cxx b/hist/hist/src/TProfile2D.cxx index 9749be20f45a4..6088c5a44995c 100644 --- a/hist/hist/src/TProfile2D.cxx +++ b/hist/hist/src/TProfile2D.cxx @@ -1614,37 +1614,23 @@ TProfile2D::Rebin2D(Int_t nxgroup, Int_t nygroup, const char *newname, const Dou return this; } - Int_t nxbins = fXaxis.GetNbins(); - Int_t nybins = fYaxis.GetNbins(); - Double_t xmin = fXaxis.GetXmin(); - Double_t xmax = fXaxis.GetXmax(); - Double_t ymin = fYaxis.GetXmin(); - Double_t ymax = fYaxis.GetXmax(); - if ((nxgroup <= 0) || (nxgroup > nxbins)) { - Error("Rebin2D", "Illegal value of nxgroup=%d", nxgroup); - return nullptr; - } - if ((nygroup <= 0) || (nygroup > nybins)) { - Error("Rebin2D", "Illegal value of nygroup=%d", nygroup); - return nullptr; - } - if (!newname && (xbins || ybins)) { + if ((!newname || strlen(newname) == 0) && (xbins || ybins)) { Error("Rebin2D", "if xbins or ybins are specified, newname must be given"); return nullptr; } - // number of bins of the rebinned profile: for an axis with user-provided - // bin edges the group parameter is directly the new number of bins, - // otherwise the old bins are merged in groups - Int_t newxbins = xbins ? nxgroup : nxbins / nxgroup; - Int_t newybins = ybins ? nygroup : nybins / nygroup; - // warning if bins are added to the overflow bin - if (!xbins && newxbins * nxgroup != nxbins) { - Warning("Rebin2D", "nxgroup=%d should be an exact divider of nxbins=%d", nxgroup, nxbins); - } - if (!ybins && newybins * nygroup != nybins) { - Warning("Rebin2D", "nygroup=%d should be an exact divider of nybins=%d", nygroup, nybins); + const Int_t nxbins = fXaxis.GetNbins(); + const Int_t nybins = fYaxis.GetNbins(); + + // validate the parameters and define the axes of the rebinned profile and + // the mapping of old to new bins + ROOT::Internal::RebinnedAxisInfo infoX, infoY; + if (!ROOT::Internal::SetupRebinnedAxis(fXaxis, nxgroup, xbins, 'x', *this, "Rebin2D", infoX) || + !ROOT::Internal::SetupRebinnedAxis(fYaxis, nygroup, ybins, 'y', *this, "Rebin2D", infoY)) { + return nullptr; } + const Int_t newxbins = infoX.nNewBins; + const Int_t newybins = infoY.nNewBins; // save old bin contents in new arrays const Int_t ncells = (nxbins + 2) * (nybins + 2); @@ -1655,34 +1641,27 @@ TProfile2D::Rebin2D(Int_t nxgroup, Int_t nygroup, const char *newname, const Dou if (fBinSumw2.fN) oldBinw2.assign(GetB2(), GetB2() + ncells); - // create a clone of the old profile if newname is specified + // rebinning will not redistribute under-/overflow content into the range + // of new axes that extend beyond the old ones + ROOT::Internal::WarnAboutUnusedFlowContent(fXaxis, infoX, xbins, 'X', oldBins.data(), 1, nybins + 2, nxbins + 2, + *this, "Rebin2D"); + ROOT::Internal::WarnAboutUnusedFlowContent(fYaxis, infoY, ybins, 'Y', oldBins.data(), nxbins + 2, nxbins + 2, 1, + *this, "Rebin2D"); + + // create a clone of the old profile if newname is specified (guaranteed + // when bin edges are passed) TProfile2D *hnew = this; - if ((newname && strlen(newname) > 0) || xbins || ybins) { + if (newname && strlen(newname) > 0) { hnew = (TProfile2D *)Clone(newname); } - // in case of nxgroup/nygroup not an exact divider of nxbins/nybins, - // top limit is changed (see NOTE in method comment) - if (!xbins && (newxbins * nxgroup != nxbins)) { - xmax = fXaxis.GetBinUpEdge(newxbins * nxgroup); - hnew->fTsumw = 0; // stats must be reset because top bins will be moved to overflow bin - } - if (!ybins && (newybins * nygroup != nybins)) { - ymax = fYaxis.GetBinUpEdge(newybins * nygroup); - hnew->fTsumw = 0; // stats must be reset because top bins will be moved to overflow bin - } - - // define the axes of the rebinned profile and the mapping of old to new - // bins, before hnew->SetBins() below possibly modifies fXaxis/fYaxis in - // the in-place case (hnew == this) - TAxis newXaxis, newYaxis; - ROOT::Internal::DefineRebinnedAxis(fXaxis, nxgroup, newxbins, xbins, xmin, xmax, newXaxis); - ROOT::Internal::DefineRebinnedAxis(fYaxis, nygroup, newybins, ybins, ymin, ymax, newYaxis); - std::vector binMapX = ROOT::Internal::MakeRebinMap(fXaxis, newXaxis, xbins != nullptr, *this, "Rebin2D"); - std::vector binMapY = ROOT::Internal::MakeRebinMap(fYaxis, newYaxis, ybins != nullptr, *this, "Rebin2D"); + // when the group count does not divide the old bin count, the top bins + // move to the overflow and the stats must be recomputed + if (infoX.truncated || infoY.truncated) + hnew->fTsumw = 0; // rebin the axes - ROOT::Internal::SetRebinnedBins2D(*hnew, newXaxis, newYaxis); + ROOT::Internal::SetRebinnedBins2D(*hnew, infoX.newAxis, infoY.newAxis); // merge bins: add the content of each old cell (including under- and // overflow) to the new cell that contains its bin center @@ -1697,10 +1676,10 @@ TProfile2D::Rebin2D(Int_t nxgroup, Int_t nygroup, const char *newname, const Dou Double_t *ew2 = hnew->GetB2(); std::fill(ew2, ew2 + newncells, 0.); ROOT::Internal::MergeRebinnedCells( - nxbins, nybins, newxbins, binMapX, binMapY, + nxbins, nybins, newxbins, infoX.binMap, infoY.binMap, {{oldBins.data(), cu2}, {oldErrors.data(), er2}, {oldCount.data(), en2}, {oldBinw2.data(), ew2}}); } else { - ROOT::Internal::MergeRebinnedCells(nxbins, nybins, newxbins, binMapX, binMapY, + ROOT::Internal::MergeRebinnedCells(nxbins, nybins, newxbins, infoX.binMap, infoY.binMap, {{oldBins.data(), cu2}, {oldErrors.data(), er2}, {oldCount.data(), en2}}); } diff --git a/hist/hist/test/test_rebin2D.cxx b/hist/hist/test/test_rebin2D.cxx index a0d78525891b2..a78b0a97cdcf4 100644 --- a/hist/hist/test/test_rebin2D.cxx +++ b/hist/hist/test/test_rebin2D.cxx @@ -205,10 +205,24 @@ TEST(Rebin2DVariable, Diagnostics) ROOT::TestSupport::CheckDiagsRAII checkDiag(kError, "TH2D::Rebin2D", "newname must be given", false); EXPECT_EQ(h.Rebin2D(4, 4, nullptr, xEdges, yEdges), nullptr); } + { + // an empty name must be rejected too, otherwise Clone("") would create + // a second histogram registered under the original name + ROOT::TestSupport::CheckDiagsRAII checkDiag(kError, "TH2D::Rebin2D", "newname must be given", false); + EXPECT_EQ(h.Rebin2D(4, 4, "", xEdges, yEdges), nullptr); + } { ROOT::TestSupport::CheckDiagsRAII checkDiag(kWarning, "TH2D::Rebin2D", "does not match any bin edges", false); const double misaligned[3] = {0., 10.5, 100.}; std::unique_ptr hnew{h.Rebin2D(2, 2, "hnew", misaligned, nullptr)}; EXPECT_NE(hnew, nullptr); } + { + // a new top edge that splits an old bin between range and overflow must + // warn as well + ROOT::TestSupport::CheckDiagsRAII checkDiag(kWarning, "TH2D::Rebin2D", "does not match any bin edges", false); + const double splitTop[3] = {0., 50., 99.5}; + std::unique_ptr hnew{h.Rebin2D(2, 2, "hnew", splitTop, nullptr)}; + EXPECT_NE(hnew, nullptr); + } } From 3e163ec38c03a6cbea537b5ccd2a555195c44d65 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Sun, 6 Sep 2026 19:58:21 +0000 Subject: [PATCH 09/10] [hist] Trim redundancy in the Rebin2D tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fold the TH2::Rebin forwarding check into the single-axis test as a direct comparison against the equivalent Rebin2D call, replace the variable-to-variable TH2 test (which exercised the same code paths as the both-axes test) with a constant-group rebin of a variable-width TH2 that was not explicitly covered before, and merge the two newname guard checks into one diagnostics scope. 🤖 Done with the help of AI --- hist/hist/test/test_rebin2D.cxx | 38 ++++++++++++--------------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/hist/hist/test/test_rebin2D.cxx b/hist/hist/test/test_rebin2D.cxx index a78b0a97cdcf4..50da82c025813 100644 --- a/hist/hist/test/test_rebin2D.cxx +++ b/hist/hist/test/test_rebin2D.cxx @@ -94,26 +94,19 @@ TEST(Rebin2DVariable, TH2SingleAxis) std::unique_ptr hnew{fine.Rebin2D(4, 2, "hnew", xEdges, nullptr)}; ASSERT_NE(hnew, nullptr); expectSameBinsAndStats(*hnew, ref, "TH2SingleAxis"); -} -// TH2::Rebin with bin edges forwards to Rebin2D with the TH1 conventions, -// leaving the y-axis untouched. -TEST(Rebin2DVariable, TH2RebinForwards) -{ - TH2D fine("fine", "fine", 100, 0., 100., 100, 0., 100.); - fine.Sumw2(); - TH2D ref("ref", "ref", 4, xEdges, 100, 0., 100.); - ref.Sumw2(); - fillSame(fine, ref); - - std::unique_ptr hnew{fine.Rebin(4, "hnew", xEdges)}; - ASSERT_NE(hnew, nullptr); - expectSameBinsAndStats(*hnew, ref, "TH2RebinForwards"); + // TH2::Rebin with bin edges must forward to Rebin2D with the TH1 + // conventions, leaving the y-axis untouched + std::unique_ptr viaRebin{fine.Rebin(4, "viaRebin", xEdges)}; + std::unique_ptr viaRebin2D{fine.Rebin2D(4, 1, "viaRebin2D", xEdges, nullptr)}; + ASSERT_NE(viaRebin, nullptr); + ASSERT_NE(viaRebin2D, nullptr); + expectSameBinsAndStats(*viaRebin, *viaRebin2D, "TH2RebinForwards"); } -// Rebinning a variable-bin TH2 back into a coarser variable binning whose -// edges are a subset of the original ones. -TEST(Rebin2DVariable, TH2VariableSource) +// Constant-group rebinning of a TH2 that has variable-width axes: the new +// edges are synthesized from the old axis. +TEST(Rebin2DVariable, TH2VariableSourceConstantGroups) { TH2D fine("fine", "fine", 4, xEdges, 4, yEdges); fine.Sumw2(); @@ -123,9 +116,9 @@ TEST(Rebin2DVariable, TH2VariableSource) ref.Sumw2(); fillSame(fine, ref); - std::unique_ptr hnew{fine.Rebin2D(2, 2, "hnew", xCoarse, yCoarse)}; + std::unique_ptr hnew{fine.Rebin2D(2, 2, "hnew")}; ASSERT_NE(hnew, nullptr); - expectSameBinsAndStats(*hnew, ref, "TH2VariableSource"); + expectSameBinsAndStats(*hnew, ref, "TH2VariableSourceConstantGroups"); } // Rebin a uniform TProfile2D into variable bins on both axes. @@ -202,13 +195,10 @@ TEST(Rebin2DVariable, Diagnostics) { TH2D h("h", "h", 100, 0., 100., 100, 0., 100.); { + // an empty name must be rejected like a null one, otherwise Clone("") + // would create a second histogram registered under the original name ROOT::TestSupport::CheckDiagsRAII checkDiag(kError, "TH2D::Rebin2D", "newname must be given", false); EXPECT_EQ(h.Rebin2D(4, 4, nullptr, xEdges, yEdges), nullptr); - } - { - // an empty name must be rejected too, otherwise Clone("") would create - // a second histogram registered under the original name - ROOT::TestSupport::CheckDiagsRAII checkDiag(kError, "TH2D::Rebin2D", "newname must be given", false); EXPECT_EQ(h.Rebin2D(4, 4, "", xEdges, yEdges), nullptr); } { From c11ec943d1eaede83d24eef1a848f12b6493d34c Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Sun, 6 Sep 2026 22:46:47 +0200 Subject: [PATCH 10/10] [hist] Fixup documentation wording --- hist/hist/src/TH2.cxx | 36 +++++++++++++----------------------- hist/hist/src/TProfile2D.cxx | 29 +++++++++++++++-------------- 2 files changed, 28 insertions(+), 37 deletions(-) diff --git a/hist/hist/src/TH2.cxx b/hist/hist/src/TH2.cxx index 349e5bcc6daef..04e7ded3b810b 100644 --- a/hist/hist/src/TH2.cxx +++ b/hist/hist/src/TH2.cxx @@ -1674,33 +1674,23 @@ TH2 *TH2::Rebin(Int_t ngroup, const char *newname, const Double_t *xbins) /// #### case 2 `xbins`!=0 || `ybins`!=0 /// /// A new histogram is created and `newname` must be specified. -/// For each axis with a non-null bin-edges array, the parameter `nxgroup` -/// (`nygroup`) is the number of variable size bins for the x-axis (y-axis) in -/// the created histogram, and the array `xbins` (`ybins`) must contain -/// `nxgroup+1` (`nygroup+1`) elements that represent the low-edges of the new -/// bins plus the upper edge of the last bin. An axis without a bin-edges +/// For an axis with a non-null bin-edges array, `nxgroup` (`nygroup`) is the +/// number of bins of the new x-axis (y-axis) and `xbins` (`ybins`) must hold +/// the `nxgroup+1` (`nygroup+1`) edges of the new bins. An axis without an /// array is rebinned in constant groups as in case 1. -/// The content of an old bin is added to the new bin containing the old bin -/// center; old bins outside the range of the new axes are added to the -/// under-/overflow bins. -/// If the original histogram has errors stored (via Sumw2), the resulting -/// histograms has new errors correctly calculated. +/// The content of each old bin is added to the new bin containing its center; +/// old bins outside the range of the new axes end up in the under-/overflow +/// bins. Errors stored via Sumw2 are correctly recalculated. /// -/// \note The bin edges specified in xbins and ybins should correspond -/// to bin edges in the original histogram. If a bin edge in the new histogram -/// is in the middle of a bin in the original histogram, all entries in -/// the split bin in the original histogram will be transfered to the -/// lower of the two possible bins in the new histogram. This is -/// probably not what you want. A warning message is emitted in this -/// case. -/// -/// examples: if h2 is an existing TH2F histogram with 100 bins on x-axis -/// and 100 bins y-axis +/// \note The new bin edges should line up with old bin edges: the entries of +/// an old bin that is split between two new bins are all transferred to the +/// bin containing the old bin center, and a warning is emitted. /// +/// example: rebinning a TH2F with 100 x 100 bins into 24 x 24 variable bins /// ~~~ {.cpp} -/// Double_t xbins[25] = {...} array of low-edges for x-axis (xbins[24] is the upper edge of last bin) -/// Double_t ybins[25] = {...} array of low-edges for y-axis (ybins[24] is the upper edge of last bin) -/// h2->Rebin2D(24,24,"hnew",xbins,ybins); //creates a new variable bin size histogram hnew +/// Double_t xbins[25] = {...}; // low-edges plus upper edge of last bin +/// Double_t ybins[25] = {...}; +/// TH2 *hnew = h2->Rebin2D(24, 24, "hnew", xbins, ybins); /// ~~~ TH2 *TH2::Rebin2D(Int_t nxgroup, Int_t nygroup, const char *newname, const Double_t *xbins, const Double_t *ybins) diff --git a/hist/hist/src/TProfile2D.cxx b/hist/hist/src/TProfile2D.cxx index 6088c5a44995c..fa2e5ef736c6d 100644 --- a/hist/hist/src/TProfile2D.cxx +++ b/hist/hist/src/TProfile2D.cxx @@ -1583,24 +1583,25 @@ void TProfile2D::ExtendAxis(Double_t x, TAxis *axis) /// Statistics will be recomputed from the new bin contents. /// /// ## case 2 `xbins`!=0 || `ybins`!=0 -/// a new profile is created and `newname` must be specified. -/// For each axis with a non-null bin-edges array, the parameter `nxgroup` -/// (`nygroup`) is the number of variable size bins for the x-axis (y-axis) -/// in the created profile, and the array `xbins` (`ybins`) must contain -/// `nxgroup+1` (`nygroup+1`) elements that represent the low-edges of the -/// new bins plus the upper edge of the last bin. An axis without a bin-edges +/// +/// A new profile is created and `newname` must be specified. +/// For an axis with a non-null bin-edges array, `nxgroup` (`nygroup`) is the +/// number of bins of the new x-axis (y-axis) and `xbins` (`ybins`) must hold +/// the `nxgroup+1` (`nygroup+1`) edges of the new bins. An axis without an /// array is rebinned in constant groups as in case 1. -/// The data of the old bins are added to the new bin which contains the bin center -/// of the old bins. It is possible that information from the old binning are attached -/// to the under-/overflow bins of the new binning. +/// The data of each old bin are added to the new bin containing its center; +/// old bins outside the range of the new axes end up in the under-/overflow +/// bins. /// -/// examples: if hp is an existing TProfile2D with 100 bins on x-axis -/// and 100 bins y-axis +/// \note The new bin edges should line up with old bin edges: the entries of +/// an old bin that is split between two new bins are all transferred to the +/// bin containing the old bin center, and a warning is emitted. /// +/// example: rebinning a TProfile2D with 100 x 100 bins into 24 x 24 variable bins /// ~~~ {.cpp} -/// Double_t xbins[25] = {...} array of low-edges for x-axis (xbins[24] is the upper edge of last bin) -/// Double_t ybins[25] = {...} array of low-edges for y-axis (ybins[24] is the upper edge of last bin) -/// hp->Rebin2D(24,24,"hpnew",xbins,ybins); //creates a new variable bin size profile hpnew +/// Double_t xbins[25] = {...}; // low-edges plus upper edge of last bin +/// Double_t ybins[25] = {...}; +/// TProfile2D *hpnew = hp->Rebin2D(24, 24, "hpnew", xbins, ybins); /// ~~~ TProfile2D *