TTree::GetMinimum/GetMaximum only scan one file of a friend TChain


If a branch lives in a friend that is a multi-file TChain and the main object is a plain TTree, GetMinimum/GetMaximum return the min/max over only the friend chain’s currently-loaded file, with no warning. If the main object is itself a (file-aligned) TChain, the same query is correct. Per-entry access reads the friend correctly in all cases — Draw("w","","goff") on the very same TTree returns the true range — so it looks specific to GetMinimum/GetMaximum. Self-contained reproducer below (generates its own files). Not sure if related to TChain::Add(TChain*) leaves GetEntries() reporting a wrong, too-small value

// TTree::GetMinimum / GetMaximum do not scan a friend TChain correctly.
//
// Setup: N data files and N friend files, file-aligned (same entries per file).
// The friend branch "w" lives in disjoint ranges per file:
//     file 0 : w in [4,5]   (the file a fresh chain has loaded)
//     file 1 : w in [0,1]   (holds the global MIN)
//     file 2 : w in [9,10]  (holds the global MAX)
// so the true min/max of "w" over the whole friend is ~0 / ~10.
//
// Observed (ROOT 6.40.00):
//   CASE A  main is a TChain (file-aligned with the friend)
//             -> GetMinimum/GetMaximum = ~0 / ~10   (correct)
//   CASE B  main is a plain TTree, same friend TChain
//             -> GetMinimum/GetMaximum = ~4 / ~5    (only the friend's first file!)
//             -> Draw("w") = ~0 / ~10, so the friend values ARE reachable;
//                only GetMinimum/GetMaximum are wrong.
//
// Run:  root -l -b -q getminimum_friend_chain.C

void getminimum_friend_chain()
{
   printf("ROOT version: %s\n\n", gROOT->GetVersion());
   TRandom3 rng(1);
   const std::string dir = "gmfc_data";
   gSystem->Exec(("rm -rf " + dir).c_str());
   gSystem->mkdir(dir.c_str(), kTRUE);

   const int per = 100, nfiles = 3;
   const double lo[nfiles] = {4.0, 0.0, 9.0};   // file 0 holds neither global min nor max

   for (int f = 0; f < nfiles; ++f) {
      TFile fd((dir + "/data_" + std::to_string(f) + ".root").c_str(), "RECREATE");
      TTree td("data", "data"); Double_t x; td.Branch("x", &x);
      for (int i = 0; i < per; ++i) { x = rng.Gaus(); td.Fill(); }
      td.Write(); fd.Close();

      TFile ff((dir + "/friend_" + std::to_string(f) + ".root").c_str(), "RECREATE");
      TTree tf("fr", "fr"); Double_t w; tf.Branch("w", &w);
      for (int i = 0; i < per; ++i) { w = lo[f] + rng.Uniform(); tf.Fill(); }
      tf.Write(); ff.Close();
   }
   printf("friend \"w\":  file0 in [4,5], file1 in [0,1], file2 in [9,10]\n");
   printf("TRUE min ~ 0 , TRUE max ~ 10\n\n");

   auto friendChain = [&]{
      TChain* fr = new TChain("fr");
      for (int f = 0; f < nfiles; ++f) fr->Add((dir + "/friend_" + std::to_string(f) + ".root").c_str());
      return fr;
   };

   // CASE A: main is a TChain, file-aligned with the friend chain
   {
      TChain* m = new TChain("data");
      for (int f = 0; f < nfiles; ++f) m->Add((dir + "/data_" + std::to_string(f) + ".root").c_str());
      m->AddFriend(friendChain());
      printf("CASE A  main->IsA() = %-7s :  GetMinimum=%g  GetMaximum=%g   (expect ~0 / ~10)\n",
             m->IsA()->GetName(), m->GetMinimum("w"), m->GetMaximum("w"));
   }

   // CASE B: main is a plain TTree (CopyTree of the friend-less data chain), same friend
   {
      TChain dc("data");
      for (int f = 0; f < nfiles; ++f) dc.Add((dir + "/data_" + std::to_string(f) + ".root").c_str());
      TFile* out = new TFile((dir + "/merged.root").c_str(), "RECREATE");
      TTree* m = (TTree*)dc.CopyTree("");           // plain TTree
      m->AddFriend(friendChain());
      printf("CASE B  main->IsA() = %-7s :  GetMinimum=%g  GetMaximum=%g   (expect ~0 / ~10)\n",
             m->IsA()->GetName(), m->GetMinimum("w"), m->GetMaximum("w"));
      m->SetEstimate(m->GetEntries() + 1);
      Long64_t n = m->Draw("w", "", "goff");
      printf("        Draw(\"w\")           :  min=%g  max=%g  (rows=%lld)\n",
             TMath::MinElement(n, m->GetV1()), TMath::MaxElement(n, m->GetV1()), n);
      out->Close();
   }

   printf("\n(generated files in ./%s/ ; delete with `rm -rf %s`)\n", dir.c_str(), dir.c_str());
}

output:

ROOT version: 6.40.00

friend “w”: file0 in [4,5], file1 in [0,1], file2 in [9,10]
TRUE min ~ 0 , TRUE max ~ 10

CASE A main->IsA() = TChain : GetMinimum=0.00800271 GetMaximum=9.98806 (expect ~0 / ~10)
CASE B main->IsA() = TTree : GetMinimum=4.00287 GetMaximum=4.99732 (expect ~0 / ~10)
Draw(“w”) : min=0.00800271 max=9.98806 (rows=300)

(generated files in ./gmfc_data/ ; delete with rm -rf gmfc_data)

ROOT Version: 6.40.00
Platform: Linux (cvmfs)
Compiler: linuxx8664gcc


@pcanal

Dear @avenkate ,

Thanks for the post with the reproducer! On the surface it sounds like a bug, I will run it locally to confirm and if so I will open an issue on github to keep track of it.

Cheers,
Vincenzo