diff --git a/core/qa/CMakeLists.txt b/core/qa/CMakeLists.txt index 93567ed5908660e865073928194369398c949bb3..69c773be0852e1f8261675e9cd0aa5cf92e20d2a 100644 --- a/core/qa/CMakeLists.txt +++ b/core/qa/CMakeLists.txt @@ -13,6 +13,7 @@ set(SRCS CbmQaTask.cxx CbmQaIO.cxx CbmQaUtil.cxx + CbmQaCompare.cxx checker/CbmQaCheckerCore.cxx checker/CbmQaCheckerFileHandler.cxx checker/CbmQaCheckerHist1DHandler.cxx @@ -49,6 +50,7 @@ set(INTERFACE_DEPENDENCIES generate_cbm_library() Install(FILES + CbmQaCompare.h CbmQaManager.h CbmQaTask.h CbmQaCanvas.h diff --git a/core/qa/CbmQaCompare.cxx b/core/qa/CbmQaCompare.cxx new file mode 100644 index 0000000000000000000000000000000000000000..3d809f7e7fe10cbe28f0ea4ab159b7682bc5346b --- /dev/null +++ b/core/qa/CbmQaCompare.cxx @@ -0,0 +1,274 @@ +/* Copyright (C) 2023 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt + SPDX-License-Identifier: GPL-3.0-only + Authors: Sergei Zharko [committer] */ + +/// \file CbmQaCompare.cxx +/// \brief A histogram comparison module for the QA task (implementation) +/// \author S.Zharko <s.zharko@lx-pool.gsi.de> +/// \since 23.12.2023 + +#include "CbmQaCompare.h" + +#include "CbmQaCanvas.h" +#include "Logger.h" +#include "TAxis.h" +#include "TCanvas.h" +#include "TError.h" +#include "TGraphAsymmErrors.h" +#include "TH1.h" +#include "TH2.h" +#include "TLegend.h" +#include "TMath.h" +#include "TMultiGraph.h" +#include "TObject.h" +#include "TProfile.h" +#include "TProfile2D.h" +#include "TStyle.h" + +#include <sstream> +#include <type_traits> + + +// --------------------------------------------------------------------------------------------------------------------- +// +template<class Obj> +CbmQaCompare<Obj>::CbmQaCompare(const Obj* pHistL, const Obj* pHistR, int verbose) + : fpObjL(pHistL) + , fpObjR(pHistR) + , fVerbose(verbose) +{ +} + +// --------------------------------------------------------------------------------------------------------------------- +// +template<class Obj> +CbmQaCompare<Obj>::~CbmQaCompare() +{ +} + +// --------------------------------------------------------------------------------------------------------------------- +// +template<class Obj> +typename CbmQaCompare<Obj>::Result CbmQaCompare<Obj>::operator()(const std::string& opt, + const std::string& optStat) const +{ + // ---- Parse options + bool bCmpPointByPoint = opt.find("p") != std::string::npos; + bool bCmpStatTest = opt.find("s") != std::string::npos; + bool bCmpRatio = opt.find("r") != std::string::npos; + + Result res; + + + if constexpr (std::is_base_of_v<TH1, Obj>) { + auto CheckAxes = [&](const TAxis* pL, const TAxis* pR, const char* name) { + bool ret = true; + if (pL->GetNbins() != pR->GetNbins()) { + LOG(warn) << "histogram " << name << " has inconsistent bin number with the default histogram"; + ret = false; + } + if (pL->GetXmin() != pR->GetXmin()) { + LOG(warn) << "histogram " << name << " has inconsistent min of an axis x, y or z with the default histogram"; + ret = false; + } + if (pL->GetXmax() != pR->GetXmax()) { + LOG(warn) << "histogram " << name << " has inconsistent max of an axis x, y or z with the default histogram"; + ret = false; + } + return ret; + }; + + // ---- Consistency check ------------------------------------------------------------------------------------------ + if (!(CheckAxes(fpObjL->GetXaxis(), fpObjR->GetXaxis(), fpObjL->GetName()) + && CheckAxes(fpObjL->GetYaxis(), fpObjR->GetYaxis(), fpObjL->GetName()) + && CheckAxes(fpObjL->GetZaxis(), fpObjR->GetZaxis(), fpObjL->GetName()))) { + res.fConsistent = false; + return res; + } + else { + res.fConsistent = true; + } + + // ---- Point-by-point comparison ---------------------------------------------------------------------------------- + if (bCmpPointByPoint || bCmpRatio) { + res.fPointByPoint = true; + res.fRatioLo = 1.; + res.fRatioUp = 1.; + for (int iBinX = 0; iBinX <= fpObjL->GetNbinsX(); ++iBinX) { + for (int iBinY = 0; iBinY <= fpObjL->GetNbinsY(); ++iBinY) { + for (int iBinZ = 0; iBinZ <= fpObjL->GetNbinsZ(); ++iBinZ) { + auto numBinContent = fpObjL->GetBinContent(iBinX, iBinY, iBinZ); + auto denBinContent = fpObjR->GetBinContent(iBinX, iBinY, iBinZ); + double ratio = static_cast<double>(numBinContent) / denBinContent; + // Check bin content + if (!TMath::IsNaN(numBinContent) && !TMath::IsNaN(denBinContent)) { + if (numBinContent != denBinContent) { + res.fPointByPoint = false; + if (bCmpRatio) { + if (res.fRatioLo > ratio) { + res.fRatioLo = ratio; + } + if (res.fRatioUp < ratio) { + res.fRatioUp = ratio; + } + // NOTE: case numBinContent == denBinContent == 0 is accounted as true + } + } + } + else { + if (TMath::IsNaN(numBinContent) != TMath::IsNaN(denBinContent)) { + res.fPointByPoint = false; + } + } + auto numBinError = fpObjL->GetBinError(iBinX, iBinY, iBinZ); + auto denBinError = fpObjR->GetBinError(iBinX, iBinY, iBinZ); + // Check bin error + if (!TMath::IsNaN(numBinError) && !TMath::IsNaN(denBinError)) { + if (numBinError != denBinError) { + res.fPointByPoint = false; + } + } + else { + if (TMath::IsNaN(numBinError) != TMath::IsNaN(denBinError)) { + res.fPointByPoint = false; + } + } + } + } + } + } + // ---- Stat test comparison --------------------------------------------------------------------------------------- + if (bCmpStatTest) { + TString optParam = optStat + "CHI2/NDF"; // Set CHI2/NDF as an output + res.fChi2NDF = fpObjL->Chi2Test(fpObjR, optParam); + } + } + return res; +} + +// --------------------------------------------------------------------------------------------------------------------- +// +template<class Obj> +TCanvas* CbmQaCompare<Obj>::GetComparisonCanvas(const std::string& opt) +{ + // ---- Constant parameters + constexpr int kPadPX = 500; // Pad x-size in pixels + constexpr int kPadPY = 500; // Pad y-size in pixels + constexpr double kPadMarginL = 0.15; // Left margin of a pad + constexpr double kPadMarginR = 0.05; // Right margin of a pad + constexpr double kPadMarginB = 0.12; // Bottom margin of a pad + constexpr double kPadMarginT = 0.10; // Top margin of a pad + constexpr Style_t kMarkerL = 24; + constexpr Style_t kMarkerR = 25; + constexpr Style_t kMarkerDif = 20; + constexpr Style_t kMarkerRat = 20; + + // ---- Parse options + bool bDrawCmp = opt.find("c") != std::string::npos; + bool bDrawRat = opt.find("r") != std::string::npos; + bool bDrawDif = opt.find("d") != std::string::npos; + int nPads = bDrawCmp + bDrawDif + bDrawRat; + + if (!nPads) { + return nullptr; + } + + auto* pCanv = new TCanvas("comparison", "comparison", kPadPX * nPads, kPadPY); + pCanv->Divide(nPads, 1); + + if constexpr (std::is_base_of_v<TH1, Obj>) { + if (bDrawCmp) { + pCanv->cd(1); + gPad->SetMargin(kPadMarginL, kPadMarginR, kPadMarginB, kPadMarginT); + auto* pMGraph = new TMultiGraph(Form("%s_cmp", fpObjL->GetName()), fpObjL->GetTitle()); + auto* pGrL = new TGraphAsymmErrors(fpObjL); + pGrL->SetLineColor(kOrange + 5); + pGrL->SetMarkerColor(kOrange + 5); + pGrL->SetMarkerStyle(kMarkerL); + auto* pGrR = new TGraphAsymmErrors(fpObjR); + pGrR->SetLineColor(kCyan - 2); + pGrR->SetMarkerColor(kCyan - 2); + pGrR->SetMarkerStyle(kMarkerR); + pMGraph->Add(pGrL, "P"); + pMGraph->Add(pGrR, "P"); + pMGraph->GetXaxis()->SetTitle(fpObjL->GetXaxis()->GetTitle()); + pMGraph->GetYaxis()->SetTitle(fpObjL->GetYaxis()->GetTitle()); + pMGraph->Draw("AP"); + auto* pLegend = new TLegend(0.55, 0.80, (1. - kPadMarginR), (1. - kPadMarginT)); + pLegend->SetTextSize(0.035); + //pLegend->SetBorderSize(0); + pLegend->SetFillStyle(0); + pLegend->SetMargin(0.2); + pLegend->AddEntry(pGrL, fsLabelL.c_str(), "P"); + pLegend->AddEntry(pGrR, fsLabelR.c_str(), "P"); + pLegend->Draw(); + } + + TH1* pHistL = nullptr; // Temporary histogram + TH1* pHistR = nullptr; // Temporary histogram + if constexpr (std::is_base_of_v<TProfile2D, Obj>) { + pHistL = fpObjL->ProjectionXY(Form("tmp_%s", fsLabelL.c_str())); + pHistR = fpObjR->ProjectionXY(Form("tmp_%s", fsLabelR.c_str())); + } + else if constexpr (std::is_base_of_v<TProfile, Obj>) { + pHistL = fpObjL->ProjectionX(Form("tmp_%s", fsLabelL.c_str())); + pHistR = fpObjR->ProjectionX(Form("tmp_%s", fsLabelR.c_str())); + } + else { + pHistL = static_cast<Obj*>(fpObjL->Clone(Form("tmp_%s", fsLabelL.c_str()))); + pHistR = static_cast<Obj*>(fpObjR->Clone(Form("tmp_%s", fsLabelR.c_str()))); + } + + // Ratio plot + if (bDrawRat) { + pCanv->cd(static_cast<int>(bDrawCmp) + static_cast<int>(bDrawDif) + 1); + gPad->SetMargin(kPadMarginL, kPadMarginR, kPadMarginB, kPadMarginT); + auto currErrorLevel = gErrorIgnoreLevel; // Suppress log error about skipping null points + gErrorIgnoreLevel = kError; + auto* pGrRat = new TGraphAsymmErrors(pHistL, pHistR, "pois"); + gErrorIgnoreLevel = currErrorLevel; + pGrRat->GetXaxis()->SetTitle(pHistL->GetXaxis()->GetTitle()); + pGrRat->GetYaxis()->SetTitle("ratio"); + pGrRat->SetTitle(Form("Ratio of \"%s\" and \"%s\"", fsLabelL.c_str(), fsLabelR.c_str())); + pGrRat->SetMarkerStyle(kMarkerRat); + pGrRat->Draw("AP"); + } + + // Difference plot + if (bDrawDif) { + pCanv->cd(static_cast<int>(bDrawCmp) + 1); + gPad->SetMargin(kPadMarginL, kPadMarginR, kPadMarginB, kPadMarginT); + auto* pDif = static_cast<Obj*>(pHistL->Clone()); + pDif->Add(pHistR, -1.); + auto* pGrDif = new TGraphAsymmErrors(pDif); + pGrDif->GetXaxis()->SetTitle(pHistL->GetXaxis()->GetTitle()); + pGrDif->GetYaxis()->SetTitle("difference"); + pGrDif->SetTitle(Form("Difference of \"%s\" and \"%s\"", fsLabelL.c_str(), fsLabelR.c_str())); + pGrDif->SetMarkerStyle(kMarkerDif); + pGrDif->Draw("AP"); + delete pDif; + } + + if (pHistL) { + delete pHistL; + } + if (pHistR) { + delete pHistR; + } + } + + return pCanv; +} + +// --------------------------------------------------------------------------------------------------------------------- +// +template<class Obj> +void CbmQaCompare<Obj>::SetObjectLabels(const std::string& labelL, const std::string& labelR) +{ + fsLabelL = labelL; + fsLabelR = labelR; +} + +template class CbmQaCompare<TH1>; +template class CbmQaCompare<TH2>; +template class CbmQaCompare<TProfile>; diff --git a/core/qa/CbmQaCompare.h b/core/qa/CbmQaCompare.h new file mode 100644 index 0000000000000000000000000000000000000000..75aef2d067e5da99a62e4df347d43bc9cf36127e --- /dev/null +++ b/core/qa/CbmQaCompare.h @@ -0,0 +1,67 @@ +/* Copyright (C) 2023 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt + SPDX-License-Identifier: GPL-3.0-only + Authors: Sergei Zharko [committer] */ + +/// \file CbmQaCompare.h +/// \brief A histogram comparison module for the QA task (declaration) +/// \author S.Zharko <s.zharko@lx-pool.gsi.de> +/// \since 23.12.2023 + +#pragma once + +#include <cassert> +#include <string> + +class TH1; +class TCanvas; + +/// \class CbmQaCompare +/// \brief Class to compare histograms of the QA task with default ones +/// \tparam Obj Root object type +template<class Obj> +class CbmQaCompare { + public: + struct Result { + bool fPointByPoint = true; + double fRatioLo = 1.; ///< Lower bound of the ratio + double fRatioUp = 1.; ///< Upper bound of the ratio + double fChi2NDF = 0.; ///< Chi2/NDF value + bool fConsistent = true; ///< Consistency flag + }; + + /// \brief Constructor + /// \param pHistL Left object + /// \param pHistR Right object + /// \param verbose Verbosity level + CbmQaCompare(const Obj* pHistL, const Obj* pHistR, int verbose); + + /// \brief Destructor + ~CbmQaCompare(); + + /// \brief Compares two histograms, returns a comparison status + /// \param opt Comparison options + /// - p: point-by-point + /// - r: ratio + /// - s[opt]: stat-test + /// \param optStat Option for the stat. test (see TH1 documentation) + /// \return Comparison results. By default the result is true + Result operator()(const std::string& opt, const std::string& optStat = "UU") const; + + /// \brief Creates a comparison canvas + /// \param opt Canvas options + /// - d: draw difference + /// - r: draw ratio + TCanvas* GetComparisonCanvas(const std::string& opt); + + /// \brief Set version lables + /// \param labelL Left object label + /// \param labelR Right object label + void SetObjectLabels(const std::string& labelL, const std::string& labelR); + + private: + const Obj* fpObjL = nullptr; ///< Left (new) histogram + const Obj* fpObjR = nullptr; ///< Right (default) histogram + std::string fsLabelL = "this"; + std::string fsLabelR = "default"; + int fVerbose = 0; ///< Verbosity +}; diff --git a/core/qa/CbmQaIO.cxx b/core/qa/CbmQaIO.cxx index 8c3b84699a6d5b79cfd8f09cb96c2bdd6adcb568..e0635994de7933262d51e7d8e6babb654aa7eb1d 100644 --- a/core/qa/CbmQaIO.cxx +++ b/core/qa/CbmQaIO.cxx @@ -113,7 +113,6 @@ void CbmQaIO::WriteToFile(TFile* pOutFile) const { pOutFile->cd(); for (auto& [pObject, sPath] : (*fpvObjList)) { - //LOG(info) << "Writing object " << pObject->GetName() << " -- " << sPath; if (!pOutFile->GetDirectory(sPath)) { pOutFile->mkdir(sPath); } @@ -124,7 +123,6 @@ void CbmQaIO::WriteToFile(TFile* pOutFile) const } } - // --------------------------------------------------------------------------------------------------------------------- // void CbmQaIO::MakeQaDirectory(TString sDirectory) diff --git a/core/qa/CbmQaIO.h b/core/qa/CbmQaIO.h index 7cb64f07ab5c0cae3ba406e281c6d3681b7aad09..ab6f83317d7db3025328a7c3f12656c1ebe68b8a 100644 --- a/core/qa/CbmQaIO.h +++ b/core/qa/CbmQaIO.h @@ -2,10 +2,10 @@ SPDX-License-Identifier: GPL-3.0-only Authors: Sergei Zharko [committer] */ -/// @file CbmQaIO.h -/// @brief Module for ROOT objects IO interface (header) -/// @author S.Zharko <s.zharko@gsi.de> -/// @since 29.03.2023 +/// \file CbmQaIO.h +/// \brief Module for ROOT objects IO interface (header) +/// \author S.Zharko <s.zharko@gsi.de> +/// \since 29.03.2023 #ifndef CbmQaIO_h #define CbmQaIO_h 1 @@ -35,7 +35,7 @@ class TFile; -/// @brief ROOT object IO interface for QA +/// \brief ROOT object IO interface for QA /// /// The class provides interface to write ROOT object into resulted files class CbmQaIO { @@ -48,37 +48,37 @@ class CbmQaIO { kSUBDIR ///< Objects of different type will be stored in different subdirectories like histograms/ canvases/ }; - /// @brief Constructor - /// @param prefixName Name of the unique prefix - /// @param pObjList List of already created objects + /// \brief Constructor + /// \param prefixName Name of the unique prefix + /// \param pObjList List of already created objects CbmQaIO(TString prefixName, std::shared_ptr<ObjList_t> pObjList = nullptr); - /// @brief Destructor + /// \brief Destructor virtual ~CbmQaIO(); - /// @brief Copy constructor + /// \brief Copy constructor CbmQaIO(const CbmQaIO&) = delete; - /// @brief Move constructor + /// \brief Move constructor CbmQaIO(CbmQaIO&&) = delete; - /// @brief Copy assignment operator + /// \brief Copy assignment operator CbmQaIO& operator=(const CbmQaIO&) = delete; - /// @brief Move assignment operator + /// \brief Move assignment operator CbmQaIO& operator=(CbmQaIO&&) = delete; - /// @brief Gets config name + /// \brief Gets config name const char* GetConfigName() const { return fsConfigName.Data(); } - /// @brief Creates a QA (ROOT) object, using properties defined with a tag in config - /// @tparam T Type of the object - /// @param sName Name of the object - /// @param sTitle Title of the object - /// @param args... The rest of the arguments, which will be passed to the histogram constructor - /// @note Tag is defined after the last ';' symbol in the nameBase string - /// @example + /// \brief Creates a QA (ROOT) object, using properties defined with a tag in config + /// \tparam T Type of the object + /// \param sName Name of the object + /// \param sTitle Title of the object + /// \param args... The rest of the arguments, which will be passed to the histogram constructor + /// \note Tag is defined after the last ';' symbol in the nameBase string + /// \example /// nambeBase = "/stations/station0/xy_occupancy;xy_station0" will be decayed into: /// 1) subdirectory "stations/station0" /// 2) name of histogram "catrd_hit_xy_occupancy" @@ -87,53 +87,56 @@ class CbmQaIO { template<typename T, typename... Args> T* MakeQaObject(TString sName, TString sTitle, Args... args); - /// @brief Creates a directory in the output file. + /// \brief Creates a directory in the output file. /// Only needed to place directories in a preffered order when doing Write() /// Call it prior to MakeQaObject() calls. - /// @param dirName Name of the directory - /// @note Tag is defined after the last ';' symbol in the nameBase string - /// @example + /// \param dirName Name of the directory + /// \note Tag is defined after the last ';' symbol in the nameBase string + /// \example /// dirName = "/stations/station0" void MakeQaDirectory(TString sName); - /// @brief Creates a ROOT object - /// @brief Sets config name - /// @param name A path to the config + /// \brief Creates a ROOT object + /// \brief Sets config name + /// \param name A path to the config void SetConfigName(const char* path); - /// @brief Sets a common root path to the objects in the output file - /// @param path A path to the object + /// \brief Sets a common root path to the objects in the output file + /// \param path A path to the object void SetRootFolderName(const TString& path) { fsRootFolderName = path.Strip(TString::kBoth, '/'); } - /// @brief Set storing mode + /// \brief Set storing mode void SetStoringMode(EStoringMode mode) { fStoringMode = mode; } protected: - /// @brief Function to check, if a property is defined - /// @param property A property to be tested - /// @param name A name of property (used for logging) - /// @note Throws an exception, if property is undefined + /// \brief Function to check, if a property is defined + /// \param property A property to be tested + /// \param name A name of property (used for logging) + /// \note Throws an exception, if property is undefined template<typename T> void CheckProperty(T&& property, const char* name) const; - /// @brief Applies properties on the histogram created with the MakeQaObject function - /// @param pHist Pointer to the histogram + /// \brief Compares the selected objects with default, if the default file is provided + + + /// \brief Applies properties on the histogram created with the MakeQaObject function + /// \param pHist Pointer to the histogram virtual void SetTH1Properties(TH1* pHist); - /// @brief Applies properties on the histogram created with the MakeQaObject function - /// @param pHist Pointer to the histogram + /// \brief Applies properties on the histogram created with the MakeQaObject function + /// \param pHist Pointer to the histogram virtual void SetTH2Properties(TH2* pHist); - /// @brief Applies properties on the profile 2D created with the MakeQaObject function - /// @param pHist Pointer to the profile + /// \brief Applies properties on the profile 2D created with the MakeQaObject function + /// \param pHist Pointer to the profile void SetTProfile2DProperties(TProfile2D* pHist); - /// @brief Applies properties on the canvas created with the MakeQaObject funciton - /// @param pCanv Pointer to the canvas + /// \brief Applies properties on the canvas created with the MakeQaObject funciton + /// \param pCanv Pointer to the canvas virtual void SetCanvasProperties(TCanvas* pCanv); - /// @brief Writes objects into file - /// @param pOutFile Pointer to output ROOT file + /// \brief Writes objects into file + /// \param pOutFile Pointer to output ROOT file void WriteToFile(TFile* pOutFile) const; TString fsRootFolderName = ""; ///< Name of root folder @@ -146,9 +149,9 @@ class CbmQaIO { YAML::Node fConfigNode{}; ///< Configuration node private: - /// @brief Creates and registers a ROOT object - /// @param name A name of the ROOT object, which can contain a sub-directory - /// @param args Other arguments, passed to the ROOT object constructor + /// \brief Creates and registers a ROOT object + /// \param name A name of the ROOT object, which can contain a sub-directory + /// \param args Other arguments, passed to the ROOT object constructor template<typename T, typename... Args> T* ConstructAndRegisterQaObject(TString name, Args... args); }; @@ -223,7 +226,12 @@ T* CbmQaIO::ConstructAndRegisterQaObject(TString sName, Args... args) // Add parent directory if (fsRootFolderName.Length() != 0) { - sDirectory = fsRootFolderName + "/" + sDirectory; + if (sDirectory.Length() != 0) { + sDirectory = fsRootFolderName + "/" + sDirectory; + } + else { + sDirectory = fsRootFolderName; + } } // Register the object in the list diff --git a/core/qa/CbmQaManager.cxx b/core/qa/CbmQaManager.cxx index aac24f8084e3b4d0cffa1e1e41881063605e03bc..ad7231030129b3a2cf23f17b29569198c03d9531 100644 --- a/core/qa/CbmQaManager.cxx +++ b/core/qa/CbmQaManager.cxx @@ -27,6 +27,7 @@ void CbmQaManager::Finish() using std::right; using std::setw; + // Check QA-tasks for (auto* task : *(this->GetListOfTasks())) { auto* pQaTask = dynamic_cast<CbmQaTask*>(task); if (pQaTask) { @@ -36,7 +37,8 @@ void CbmQaManager::Finish() } } - LOG(info) << fName << ": monitorable status:"; + // Process check flags + LOG(info) << fName << " check-list:"; fStatus = true; for (auto* task : *(this->GetListOfTasks())) { auto* pQaTask = dynamic_cast<CbmQaTask*>(task); @@ -53,6 +55,20 @@ void CbmQaManager::Finish() } } } + + // Process histogram checking + if (fpCrossCheckFile.get()) { + for (auto* task : *(this->GetListOfTasks())) { + auto* pQaTask = dynamic_cast<CbmQaTask*>(task); + if (pQaTask) { + LOG(info) << "Histograms check for the task " << pQaTask->GetName(); + pQaTask->CompareQaObjects(); + } + } + if (fpCompareOutput.get()) { + fpCompareOutput->Close(); + } + } } // --------------------------------------------------------------------------------------------------------------------- @@ -66,8 +82,48 @@ InitStatus CbmQaManager::Init() auto* pQaTask = dynamic_cast<CbmQaTask*>(task); if (pQaTask) { pQaTask->SetConfigName(fsConfigName.Data()); + pQaTask->SetVersionTag(fsVersionTag); + pQaTask->SetDefaultTag(fsDefaultTag); + if (fpCrossCheckFile.get() != nullptr) { + pQaTask->SetCheckFile(fpCrossCheckFile); + } + if (fpCompareOutput.get() != nullptr) { + pQaTask->SetCompareOutput(fpCompareOutput); + } } } } return kSUCCESS; } + +// --------------------------------------------------------------------------------------------------------------------- +// +void CbmQaManager::OpenCrossCheckFile(const TString& path) +{ + if (path.Length()) { + auto pFile = std::make_shared<TFile>(path, "READONLY"); + if (pFile->IsOpen()) { + fpCrossCheckFile = std::move(pFile); + LOG(info) << fName << ": opening cross-check file " << fpCrossCheckFile->GetName(); + } + else { + LOG(error) << fName << ": cross-check file " << path << " was not open"; + } + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// +void CbmQaManager::OpenCompareOutput(const TString& path) +{ + if (path.Length()) { + auto pFile = std::make_shared<TFile>(path, "RECREATE"); + if (pFile->IsOpen()) { + fpCompareOutput = std::move(pFile); + LOG(info) << fName << ": opening comparison output file " << fpCompareOutput->GetName(); + } + else { + LOG(error) << fName << ": comparison output file " << path << " was not open"; + } + } +} diff --git a/core/qa/CbmQaManager.h b/core/qa/CbmQaManager.h index 7919ad76db08233a8bfb69c863f7b15b946b1420..06f2f141a2e245d8dacc6421550b7dc7231d22ec 100644 --- a/core/qa/CbmQaManager.h +++ b/core/qa/CbmQaManager.h @@ -47,6 +47,18 @@ class CbmQaManager : public FairTask { /// \brief Action of the task in the end of the run void Finish(); + /// \brief Gets status flag + bool GetStatus() const { return fStatus; } + + /// \brief Gets YAML config name + const TString& GetConfigName() const { return fsConfigName; } + + /// \brief Gets default tag + const TString& GetDefaultTag() const { return fsDefaultTag; } + + /// \brief Gets version tag + const TString& GetVersionTag() const { return fsVersionTag; } + /// \brief Task initialization InitStatus Init(); @@ -55,24 +67,31 @@ class CbmQaManager : public FairTask { ClassDef(CbmQaManager, 0); - /// \brief Gets YAML config name - const TString& GetConfigName() const { return fsConfigName; } + /// \brief Open cross-check file + /// \param path Path to the cross-check file + /// + /// Opens the cross-check ROOT-file with the QA-chain output, obtained under the default code base. + void OpenCrossCheckFile(const TString& path); - /// \brief Gets status flag - bool GetStatus() const { return fStatus; } + /// \brief Open comparison output file + /// \param path Path to the comparison output + void OpenCompareOutput(const TString& path); /// \brief Sets YAML config name void SetConfigName(const TString& name) { fsConfigName = name; } - /// \brief Open cross-check file - /// \param path Path to the cross-check file - /// - /// Opens the cross-check ROOT-file with the QA-chain output, obtained under the default code base. - //void OpenCrossCheckFile(const TString& path); + /// \brief Sets version tag + void SetVersionTag(const TString& tag) { fsVersionTag = tag; } + + /// \brief Sets default tag + void SetDefaultTag(const TString& tag) { fsDefaultTag = tag; } private: bool fStatus = true; ///< Status of QA: true - all tasks passed, false - at least one of the task failed TString fsConfigName = ""; ///< Name of the configuration YAML file (passed to underlying QA tasks) + TString fsVersionTag = ""; ///< Version tag (git SHA etc.) + TString fsDefaultTag = ""; ///< Default tag (git SHA etc.) - //std::shared_ptr<TFile> fpCrossCheckFile = nullptr; ///< A file with default histograms to carry out a cross-check + std::shared_ptr<TFile> fpCrossCheckFile = nullptr; ///< A file with default ROOT objects used for the cross-check + std::shared_ptr<TFile> fpCompareOutput = nullptr; ///< An output file for histograms cross-check }; diff --git a/core/qa/CbmQaTask.cxx b/core/qa/CbmQaTask.cxx index df38af48b2fea93a5c2cac62f2e32423720d1c9c..ab431fafd4fba28266b802b82a13f1e29e36e2da 100644 --- a/core/qa/CbmQaTask.cxx +++ b/core/qa/CbmQaTask.cxx @@ -22,6 +22,7 @@ #include "TString.h" #include <array> +#include <limits> #include <regex> ClassImp(CbmQaTask); @@ -162,6 +163,92 @@ bool CbmQaTask::CheckRange(TH1* h, double meanMax, double rmsMin, double rmsMax) return res; } +// --------------------------------------------------------------------------------------------------------------------- +// +bool CbmQaTask::CompareQaObjects() +{ + YAML::Node tagNode = fConfigNode["check_histograms"]; + + + struct HashTString { + std::size_t operator()(const TString& str) const { return std::hash<std::string>()(str.Data()); }; + }; + + // Fill map of ROOT objects, which are going to be checked + std::unordered_map<TString, ObjectComparisonConfig, HashTString> mCompareList; + // TODO: 28.12.2023 SZh: Add a possibility to define a pack of histograms (in a similar way to check flags) + for (const auto& histNode : tagNode) { + try { + auto histName = fsRootFolderName + "/" + histNode["name"].as<std::string>().c_str(); + ObjectComparisonConfig entry; + if (const auto& subNode = histNode["point_to_point"]) { + entry.fPoint = subNode["use"].as<uint8_t>(); + } + if (const auto& subNode = histNode["ratio"]) { + entry.fRatio = subNode["use"].as<uint8_t>(); + entry.fRatioMin = subNode["min"].as<double>(); + entry.fRatioMax = subNode["max"].as<double>(); + } + if (const auto& subNode = histNode["chi2Test"]) { + entry.fStat = subNode["use"].as<uint8_t>(); + entry.fChi2NdfMax = subNode["chi2_ndf_max"].as<double>(); + } + if (const auto& subNode = histNode["canvas"]) { + entry.fCanvOpt = ""; + if (subNode["comp"].as<bool>(false)) { + entry.fCanvOpt += "c"; + } + if (subNode["ratio"].as<bool>(false)) { + entry.fCanvOpt += "r"; + } + if (subNode["diff"].as<bool>(false)) { + entry.fCanvOpt += "d"; + } + } + mCompareList[histName] = entry; + } + catch (const YAML::InvalidNode& exc) { + LOG(warn) << fName << "::CompareQaObjects(): attempt to access key which does not exist in the configuration" + << " file (message from YAML exception: " << exc.what() << ')'; + } + } + + // Process comparison + // Look up the objects in the object list and search for the name in the map + + bool bCheckResult = true; + for (auto& [pObject, sPath] : (*fpvObjList)) { + if (pObject) { + + TString objName = sPath + "/" + pObject->GetName(); + auto configEntryIt = mCompareList.find(objName); + if (configEntryIt == mCompareList.end()) { + continue; + } + const auto& configEntry = configEntryIt->second; + + // Get default object + auto* pObjCheck = fpCheckFile->Get(objName); + if (!pObjCheck) { + LOG(warn) << fName << "::CompareQaObjects(): ROOT object " << objName << " is not found in the check file"; + continue; + } + + // Call the comparison function depending on the object type + if (dynamic_cast<TProfile*>(pObject)) { + bCheckResult &= CompareTwoObjects<TProfile>(pObject, pObjCheck, objName, configEntry); + } + else if (dynamic_cast<TH2*>(pObject)) { + bCheckResult &= CompareTwoObjects<TH2>(pObject, pObjCheck, objName, configEntry); + } + else if (dynamic_cast<TH1*>(pObject)) { + bCheckResult &= CompareTwoObjects<TH1>(pObject, pObjCheck, objName, configEntry); + } + } + } + return bCheckResult; +} + // --------------------------------------------------------------------------------------------------------------------- // void CbmQaTask::PutSetupNameOnPad(double xMin, double yMin, double xMax, double yMax) @@ -217,7 +304,7 @@ void CbmQaTask::ReadCheckListFromConfig() checkListIt->second.fStatus = it->second.as<bool>(); } else { - LOG(warn) << fName << "::ReadCheckListFromConfig: config contains unknown entry " << configEntry; + LOG(warn) << fName << "::ReadCheckListFromConfig(): config contains unknown entry " << configEntry; } } ++iConfigEntry; diff --git a/core/qa/CbmQaTask.h b/core/qa/CbmQaTask.h index 27d023aa5e7a1f13541b6c047f9e533c7b7a8d1b..23d4d885f98680d798980dde9c2520108a5824c9 100644 --- a/core/qa/CbmQaTask.h +++ b/core/qa/CbmQaTask.h @@ -5,18 +5,19 @@ /// \file CbmQaTask.h /// \brief A base class for CBM QA task logic /// \author S.Zharko <s.zharko@gsi.de> -/// \data 12.01.2023 - +/// \since 12.01.2023 #pragma once #include "CbmEvent.h" +#include "CbmQaCompare.h" #include "CbmQaIO.h" #include "CbmQaTable.h" #include "FairTask.h" #include "Logger.h" #include "TCanvas.h" #include "TEfficiency.h" +#include "TFile.h" #include "TFolder.h" #include "TH1.h" #include "TH2.h" @@ -26,6 +27,7 @@ #include "TProfile2D.h" #include "TProfile3D.h" #include "TROOT.h" +#include "TString.h" #include <algorithm> #include <map> @@ -51,6 +53,19 @@ class CbmQaTask : public FairTask, public CbmQaIO { bool fStatus = false; ///< Status of the check }; + /// \struct ObjectComparisonConfig + /// \brief Contains configuration to compare two root objects (histograms) + struct ObjectComparisonConfig { + uint8_t fPoint = 0; + uint8_t fRatio = 0; + uint8_t fStat = 0; + double fRatioMin = 0; + double fRatioMax = std::numeric_limits<double>::max(); + double fChi2NdfMax = std::numeric_limits<double>::max(); + std::string fStatOpt = "UU"; + std::string fCanvOpt = ""; + }; + public: /// \brief Constructor from parameters /// \param name Name of the task @@ -76,48 +91,70 @@ class CbmQaTask : public FairTask, public CbmQaIO { /// \brief Move assignment operator CbmQaTask& operator=(CbmQaTask&&) = delete; + /// \brief Function to check, if the QA results are acceptable + /// + /// In the function one should provide the check flags with the function StoreCheckResult. + virtual void Check() = 0; + + /// \brief Process ROOT objects comparison + /// \return true All the object are consistent + /// \return false Some of the objects are inconsistent + bool CompareQaObjects(); + + /// \brief FairTask: Defines action of the task in the event/TS + void Exec(Option_t* /*option*/) override; + + /// \brief FairTask: Defines action of the task in the end of run + void Finish() override; + + /// \brief Gets check-list + const std::map<std::string, CheckFlags>& GetCheckList() const { return fmCheckList; } + + /// \brief Gets default tag + const TString& GetDefaultTag() const { return fsDefaultTag; } + /// \brief Gets name of the setup const std::string& GetSetupName() const { return fsSetupName; } - /// \brief Returns flag, whether MC information is used or not in the task - bool IsMCUsed() const { return fbUseMC; } - - /// \brief Sets events suppression flag - /// - /// By default the QA task runs on the reconstructed events (CbmEvent objects), if the tree of the objects is - /// presented in the input. This flag disables the events-based routine, so the QA is executed over the whole - /// Time-slice. - void SetProcessFullTs(bool bProcessFullTs) { fbProcessFullTs = bProcessFullTs; } + /// \brief Gets version tag + const TString& GetVersionTag() const { return fsVersionTag; } /// \brief FairTask: Task initialization in the beginning of the run InitStatus Init() override; + /// \brief Returns flag, whether MC information is used or not in the task + bool IsMCUsed() const { return fbUseMC; } + + /// \brief Reads check-list from the configuration file + void ReadCheckListFromConfig(); + /// \brief FairTask: Task reinitialization InitStatus ReInit() override; - /// \brief FairTask: Defines action of the task in the event/TS - void Exec(Option_t* /*option*/) override; + /// \brief Sets check-file + /// \param pCheckFile Shared pointer to the cross-check file + void SetCheckFile(const std::shared_ptr<TFile>& pCheckFile) { fpCheckFile = pCheckFile; } - /// \brief FairTask: Defines action of the task in the end of run - void Finish() override; + /// \brief Sets compare output file + /// \param pCompareOutput Shared pointer to the comparison output file + void SetCompareOutput(const std::shared_ptr<TFile>& pCompareOutput) { fpCompareOutput = pCompareOutput; } - /// \brief Sets name of the setup - void SetSetupName(const char* setup) { fsSetupName = setup; } + /// \brief Sets version tag + void SetVersionTag(const TString& tag) { fsVersionTag = tag; } - // ***************************************************** - // ** Functions accessible inside the derived classes ** - // ***************************************************** + /// \brief Sets default tag + void SetDefaultTag(const TString& tag) { fsDefaultTag = tag; } - /// \brief Function to check, if the QA results are acceptable + /// \brief Sets events suppression flag /// - /// In the function one should provide the check flags with the function StoreCheckResult. - virtual void Check() = 0; + /// By default the QA task runs on the reconstructed events (CbmEvent objects), if the tree of the objects is + /// presented in the input. This flag disables the events-based routine, so the QA is executed over the whole + /// Time-slice. + void SetProcessFullTs(bool bProcessFullTs) { fbProcessFullTs = bProcessFullTs; } - /// \brief Reads check-list from the configuration file - void ReadCheckListFromConfig(); + /// \brief Sets name of the setup + void SetSetupName(const char* setup) { fsSetupName = setup; } - /// \brief Gets check-list - const std::map<std::string, CheckFlags>& GetCheckList() const { return fmCheckList; } protected: /// \brief De-initialize the task @@ -183,6 +220,18 @@ class CbmQaTask : public FairTask, public CbmQaIO { /// \brief De-initializes this task void DeInitBase(); + /// \brief Process object comparison + /// \tparam Obj Class of the ROOT object + /// \param pObjL First object (this) + /// \param pObjR Second object (default) + /// \param objName Name of the object (contains full path to the object) + /// \param cfg Comparison configuration entry + /// \return true Histograms are equal within selected method + /// \return false Histograms differ within one of the comparison methods + template<class Obj> + bool CompareTwoObjects(const TObject* pObjL, const TObject* pObjR, const TString& objName, + const ObjectComparisonConfig& cfg); + /// \brief A QA check-list map /// /// The check list is updated with new entries with the StoreCheckResult(tag, result) function, which is to be called @@ -199,6 +248,12 @@ class CbmQaTask : public FairTask, public CbmQaIO { bool fbUseMC = false; ///< Flag, if MC is used bool fbProcessFullTs = false; ///< If true, routine runs on the full TS even if the CbmEvent branch is in + TString fsVersionTag = ""; ///< Version tag (git SHA etc.) + TString fsDefaultTag = ""; ///< Default tag (git SHA etc.) + + std::shared_ptr<TFile> fpCheckFile = nullptr; ///< A file with default ROOT objects used for the cross-check + std::shared_ptr<TFile> fpCompareOutput = nullptr; ///< An output file for histograms cross-check + ClassDefOverride(CbmQaTask, 0); }; @@ -233,3 +288,84 @@ bool CbmQaTask::CheckRange(std::string_view name, T var, T varErr, T lo, T hi) c } return ret; } + +// --------------------------------------------------------------------------------------------------------------------- +// +template<class Obj> +bool CbmQaTask::CompareTwoObjects(const TObject* pObjL, const TObject* pObjR, const TString& objName, + const ObjectComparisonConfig& cfg) +{ + const auto* pHistL = static_cast<const Obj*>(pObjL); + const auto* pHistR = dynamic_cast<const Obj*>(pObjR); + if (!pHistR) { + LOG(error) << fName << "::CompareObjects(): the default " << pObjR->GetName() + << " has different type to the new object"; + return false; + } + std::string opt = ""; + if (cfg.fPoint > 0) { + opt += "p"; + } + if (cfg.fRatio > 0) { + opt += "r"; + } + if (cfg.fStat > 0) { + opt += "s"; + } + + auto comparator = CbmQaCompare<Obj>(pHistL, pHistR, 0); + auto out = comparator(opt, cfg.fStatOpt); + + if (!out.fConsistent) { + LOG(info) << fName << ": This and default versions of the object " << pObjL->GetName() << " are incompatible"; + return false; + } + + // Collect status and print log + bool res = true; + { + std::stringstream msg; + using std::setw; + constexpr const char* kEqual = "\e[1;32mequal\e[0m"; + constexpr const char* kDiff = "\e[1;32mdifferent\e[0m"; + msg << "\tobject " << setw(12) << pObjL->ClassName() << ' ' << setw(50) << objName; + if (cfg.fPoint) { + msg << ": point-by-point -> " << (out.fPointByPoint ? kEqual : kDiff); + if (cfg.fPoint == 2) { + res &= out.fPointByPoint; + } + } + if (cfg.fRatio) { + bool bOk = (out.fRatioLo >= cfg.fRatioMin && out.fRatioUp <= cfg.fRatioMax); + msg << ", ratio -> " << (bOk ? kEqual : kDiff) << "(lo: " << out.fRatioLo << ", up: " << out.fRatioUp << ')'; + if (cfg.fRatio == 2) { + res &= bOk; + } + } + if (cfg.fStat) { + bool bOk = out.fChi2NDF <= cfg.fChi2NdfMax; + msg << ", stat. test -> " << (bOk ? kEqual : kDiff) << "(chi2/NDF: " << out.fChi2NDF << ')'; + if (cfg.fStat == 2) { + res &= bOk; + } + } + LOG(info) << msg.str(); + } + + // Write comparison result + if (fpCompareOutput.get()) { + fpCompareOutput->mkdir(objName); + auto* pDir = fpCompareOutput->GetDirectory(objName); + pDir->cd(); + pObjL->Write(Form("%s_%s", pObjL->GetName(), fsVersionTag.Data())); + pObjR->Write(Form("%s_%s", pObjL->GetName(), fsDefaultTag.Data())); + if (true || !res) { // Save canvas only if the histograms are different + auto* pCanv = comparator.GetComparisonCanvas(cfg.fCanvOpt); + if (pCanv) { + pCanv->Write(Form("%s_cmp_canvas", pObjL->GetName())); + delete pCanv; + } + } + } + return res; +} diff --git a/macro/mcbm/mcbm_qa.C b/macro/mcbm/mcbm_qa.C index 1a4723b9f03163274c47368b36d76c1e6b56c066..db7b44a8a16b17237258f0ea5652456fdd2823c4 100644 --- a/macro/mcbm/mcbm_qa.C +++ b/macro/mcbm/mcbm_qa.C @@ -18,11 +18,22 @@ // Includes needed for IDE #if !defined(__CLING__) +#include "CbmCaInputQaMuch.h" +#include "CbmCaInputQaSetup.h" +#include "CbmCaInputQaSts.h" +#include "CbmCaInputQaTof.h" +#include "CbmCaInputQaTrd.h" +#include "CbmCaOutputQa.h" #include "CbmDefs.h" +#include "CbmKF.h" #include "CbmMCDataManager.h" +#include "CbmMatchRecoToMC.h" +#include "CbmMuchGeoScheme.h" +#include "CbmMuchHitFinderQa.h" #include "CbmMuchTransportQa.h" #include "CbmQaManager.h" #include "CbmSetup.h" +#include "CbmTrackingDetectorInterfaceInit.h" #include <FairFileSource.h> #include <FairMonitor.h> @@ -33,11 +44,25 @@ #include <FairRuntimeDb.h> #include <FairSystemInfo.h> +#include <TObjString.h> #include <TStopwatch.h> #endif -void mcbm_qa(Int_t nEvents = 0, TString dataset = "data/mcbm_beam_2020_03_test", - TString setupName = "mcbm_beam_2020_03", Bool_t bUseMC = kTRUE, const TString& config = "") +/* clang-format off */ +/// \brief QA macro execution function +/// \param nEvents Number of events to proceed +/// \param dataset Prefix of the output files files upstream the simulation/reconstruction chain +/// \param setupName Name of the setup +/// \param bUseMC Flag for MC (simulation) usage +/// \param config QA YAML configuraiton file +/// \param defaultPath Path to a default QA output, obtained for a given setup and given number of events +void mcbm_qa(Int_t nEvents = 0, + TString dataset = "data/mcbm_beam_2020_03_test", + TString setupName = "mcbm_beam_2020_03", + Bool_t bUseMC = kTRUE, + TString config = "", + TString crossCheckFile = "") +/* clang-format on */ { // ======================================================================== @@ -166,9 +191,17 @@ void mcbm_qa(Int_t nEvents = 0, TString dataset = "data/mcbm_beam_2020_03_test", FairMonitor::GetMonitor()->EnableMonitor(kTRUE, monitorFile); // ------------------------------------------------------------------------ + // ----- QA manager --------------------------------------------------- auto* qaManager = new CbmQaManager(3); qaManager->SetConfigName(qaConfig); + if (crossCheckFile.Length()) { + qaManager->OpenCrossCheckFile(crossCheckFile); + qaManager->OpenCompareOutput("compareResult.root"); + qaManager->SetDefaultTag("default"); + qaManager->SetVersionTag("this"); // TODO: read git SHA + } run->AddTask(qaManager); + // ------------------------------------------------------------------------ // ----- MCDataManager (legacy mode) ----------------------------------- if (bUseMC) { diff --git a/macro/qa/configs/qa_tasks_config_mcbm.yaml b/macro/qa/configs/qa_tasks_config_mcbm.yaml index 351fbcb6e7e39e0c5cd4ef99b084537c788471da..01cac78248d8cd16d8a1c536f377471f2bc1286e 100644 --- a/macro/qa/configs/qa_tasks_config_mcbm.yaml +++ b/macro/qa/configs/qa_tasks_config_mcbm.yaml @@ -27,6 +27,15 @@ qa: pull_t_station_%d: true pull_u_%d_digis: true pull_v_%d_digis: true + check_histograms: + - name: "Station 0/pull_x_st0" + point_to_point: { use: 1 } + ratio: { use: 1, min: 0.5, max: 1.5 } + chi2Test: {use: 1, max: 10. } + - name: "Station 0/pull_y_st0" + point_to_point: { use: 1 } + ratio: { use: 1, min: 0.5, max: 1.5 } + chi2Test: {use: 1, max: 10., stat_opt: "UU" } CbmCaInputQaMuch: check_list: station_position_ordering: true diff --git a/macro/qa/configs/qa_tasks_config_mcbm_beam_2022_05_23_nickel.yaml b/macro/qa/configs/qa_tasks_config_mcbm_beam_2022_05_23_nickel.yaml index 50f9dc7093e7313b1fbf8aecbbc973eeadfb5d1a..dbe5a6099515592007831cb65694409c7b73f55d 100644 --- a/macro/qa/configs/qa_tasks_config_mcbm_beam_2022_05_23_nickel.yaml +++ b/macro/qa/configs/qa_tasks_config_mcbm_beam_2022_05_23_nickel.yaml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-only # Authors: Sergei Zharko [committer] # -## @file qa_task_config_mcbm.yaml +## @file qa_task_config_mcbm_beam_2022_05_23_nickel.yaml ## @brief Configuration file for QA-tasks in mCBM ## @since 27.07.2023 ## @author Sergei Zharko <s.zharko@gsi.de> @@ -27,6 +27,17 @@ qa: pull_t_station_%d: true pull_u_%d_digis: true pull_v_%d_digis: true + check_histograms: + - name: "Station 0/pull/pull_x_st0" + point_to_point: { use: 1 } + ratio: { use: 1, min: 0.5, max: 1.5 } + chi2Test: {use: 1, chi2_ndf_max: 10., stat_opt: "UU" } + canvas: {comp: true, ratio: true, diff: true} + - name: "Station 0/pull/pull_y_st0" + point_to_point: { use: 1 } + ratio: { use: 0, min: 0.5, max: 1.5 } + chi2Test: {use: 1, chi2_ndf_max: 10., stat_opt: "UU" } + canvas: {comp: true, ratio: false, diff: true} CbmCaInputQaMuch: check_list: station_position_ordering: true @@ -68,5 +79,22 @@ qa: pull_y_station_%d: true pull_t_station_%d: false pull_t_station_3: false + CbmCaOutputQa: + check_histograms: + - name: "all/eff_pMC" + point_to_point: { use: 1 } + ratio: { use: 0, min: 0.5, max: 1.5 } + chi2Test: {use: 1, chi2_ndf_max: 10., stat_opt: "WW" } + canvas: {comp: true, ratio: true, diff: true} + - name: "all/reco_p" + point_to_point: { use: 1 } + ratio: { use: 0, min: 0.5, max: 1.5 } + chi2Test: {use: 1, chi2_ndf_max: 10., stat_opt: "UU" } + canvas: {comp: true, ratio: true, diff: true} + - name: "ghost/reco_p" + point_to_point: { use: 1 } + ratio: { use: 0, min: 0.5, max: 1.5 } + chi2Test: {use: 1, chi2_ndf_max: 10., stat_opt: "UU" } + canvas: {comp: true, ratio: true, diff: true} ...