diff --git a/Modules/Common/CMakeLists.txt b/Modules/Common/CMakeLists.txt index c0ddc6308e..3cc21015a7 100644 --- a/Modules/Common/CMakeLists.txt +++ b/Modules/Common/CMakeLists.txt @@ -30,6 +30,8 @@ target_sources(O2QcCommon src/ReferenceComparatorTask.cxx src/ReferenceComparatorTaskConfig.cxx src/ReferenceComparatorCheck.cxx + src/CheckerThresholdsConfig.cxx + src/TrendCheck.cxx src/NonEmpty.cxx src/MeanIsAbove.cxx src/TH1Reductor.cxx @@ -68,6 +70,7 @@ add_root_dictionary(O2QcCommon include/Common/ObjectComparatorKolmogorov.h include/Common/ReferenceComparatorTask.h include/Common/ReferenceComparatorCheck.h + include/Common/TrendCheck.h include/Common/MeanIsAbove.h include/Common/TH1Ratio.h include/Common/TH2Ratio.h diff --git a/Modules/Common/include/Common/CheckerThresholdsConfig.h b/Modules/Common/include/Common/CheckerThresholdsConfig.h new file mode 100644 index 0000000000..04c874f365 --- /dev/null +++ b/Modules/Common/include/Common/CheckerThresholdsConfig.h @@ -0,0 +1,71 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// +/// \file CheckerThresholdsConfig.h +/// \author Andrea Ferrero +/// \brief Utility class handling thresholds and axis ranges and retrieve them from the custom parameters +/// + +#ifndef QC_MODULE_COMMON_CHECKERTHRESHOLDSCONFIG_H +#define QC_MODULE_COMMON_CHECKERTHRESHOLDSCONFIG_H + +#include "QualityControl/Activity.h" +#include "QualityControl/CustomParameters.h" + +#include +#include +#include +#include + +using namespace o2::quality_control::core; + +namespace o2::quality_control_modules::common +{ + +namespace internal +{ +class Thresholds; +class XYRanges; +} // namespace internal + +class CheckerThresholdsConfig +{ + public: + CheckerThresholdsConfig(const CustomParameters& customParameters, const Activity& activity); + ~CheckerThresholdsConfig() = default; + + /// \brief function to retrieve the thresholds for a given interaction rate, if available + std::array>, 2> getThresholdsForPlot(const std::string& plotName, double rate); + + /// \brief optional X-Y ranges over which the check must be restricted + std::array>, 2> getRangesForPlot(const std::string& plotName); + + private: + void initThresholdsForPlot(const std::string& plotName); + void initRangesForPlot(const std::string& plotName); + + CustomParameters mCustomParameters; + Activity mActivity; + + /// vectors of [min,max,rate] tuples. The first two values are the minimum and maximum threshold values, + /// and the third is the associated reference interaction rate (optional) + std::array, 2> mDefaultThresholds; + std::array>, 2> mThresholds; + + /// \brief optional X-Y ranges over which the check must be restricted + std::array, 2> mDefaultRanges; + std::array>, 2> mRanges; +}; + +} // namespace o2::quality_control_modules::common + +#endif // QC_MODULE_COMMON_CHECKERTHRESHOLDSCONFIG_H diff --git a/Modules/Common/include/Common/LinkDef.h b/Modules/Common/include/Common/LinkDef.h index 3295ba72e4..267f0d7932 100644 --- a/Modules/Common/include/Common/LinkDef.h +++ b/Modules/Common/include/Common/LinkDef.h @@ -24,6 +24,7 @@ #pragma link C++ class o2::quality_control_modules::common::ObjectComparatorKolmogorov + ; #pragma link C++ class o2::quality_control_modules::common::ReferenceComparatorTask + ; #pragma link C++ class o2::quality_control_modules::common::ReferenceComparatorCheck + ; +#pragma link C++ class o2::quality_control_modules::common::TrendCheck + ; #pragma link C++ class o2::quality_control_modules::common::WorstOfAllAggregator + ; #pragma link C++ class o2::quality_control_modules::common::IncreasingEntries + ; #pragma link C++ class o2::quality_control_modules::common::TH1SliceReductor + ; diff --git a/Modules/Common/include/Common/TrendCheck.h b/Modules/Common/include/Common/TrendCheck.h new file mode 100644 index 0000000000..f52c1bad17 --- /dev/null +++ b/Modules/Common/include/Common/TrendCheck.h @@ -0,0 +1,83 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// +/// \file TrendCheck.h +/// \author Andrea Ferrero +/// \brief Generic checker for trending graphs +/// + +#ifndef QC_MODULE_COMMON_TRENDCHECK_H +#define QC_MODULE_COMMON_TRENDCHECK_H + +#include "QualityControl/CheckInterface.h" + +#include +#include +#include + +class TGraph; +class TObject; + +using namespace o2::quality_control::core; + +namespace o2::quality_control_modules::common +{ + +class CheckerThresholdsConfig; + +/// \brief Generic checker for trending graphs +/// +/// \author Andrea Ferrero +class TrendCheck : public o2::quality_control::checker::CheckInterface +{ + public: + /// Default constructor + TrendCheck() = default; + /// Destructor + ~TrendCheck() override = default; + + void configure() override; + Quality check(std::map>* moMap) override; + void beautify(std::shared_ptr mo, Quality checkResult = Quality::Null) override; + std::string getAcceptedType() override; + + void startOfActivity(const Activity& activity) override; + void endOfActivity(const Activity& activity) override; + + ClassDefOverride(TrendCheck, 1); + + private: + enum ThresholdsMode { + ExpectedRange, + DeviationFromMean, + StdDeviation + }; + + std::array>, 2> getThresholds(std::string key, TGraph* graph); + void getGraphsFromObject(TObject* object, std::vector& graphs); + double getInteractionRate(); + + Activity mActivity; + ThresholdsMode mTrendCheckMode{ ExpectedRange }; + int mNPointsForAverage{ 0 }; + std::pair mQualityLabelPosition{ 0.12, 0.8 }; + std::pair mQualityLabelSize{ 0.5, 0.07 }; + std::shared_ptr mThresholds; + std::unordered_map>>> mAverageTrend; + std::unordered_map>>> mThresholdsTrendBad; + std::unordered_map>>> mThresholdsTrendMedium; + std::unordered_map mQualities; +}; + +} // namespace o2::quality_control_modules::common + +#endif // QC_MODULE_COMMON_TRENDCHECK_H diff --git a/Modules/Common/include/Common/Utils.h b/Modules/Common/include/Common/Utils.h index 0f7b8b0381..5a84b88a2c 100644 --- a/Modules/Common/include/Common/Utils.h +++ b/Modules/Common/include/Common/Utils.h @@ -89,7 +89,11 @@ T getFromExtendedConfig(const quality_control::core::Activity& activity, const q if (auto param = params.atOptional(name, activity)) { parameter = param.value(); } else { - parameter = params.atOrDefaultValue(name, std::to_string(retVal)); + if constexpr (std::is_same::value) { + parameter = params.atOrDefaultValue(name, retVal); + } else { + parameter = params.atOrDefaultValue(name, std::to_string(retVal)); + } } return internal::stringToType(parameter); } diff --git a/Modules/Common/src/CheckerThresholdsConfig.cxx b/Modules/Common/src/CheckerThresholdsConfig.cxx new file mode 100644 index 0000000000..155eed92e1 --- /dev/null +++ b/Modules/Common/src/CheckerThresholdsConfig.cxx @@ -0,0 +1,264 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// +/// \file CheckerThresholdsConfig.cxx +/// \author Andrea Ferrero +/// \brief Utility class handling thresholds and axis ranges and retrieve them from the custom parameters +/// + +#include "Common/CheckerThresholdsConfig.h" +#include "Common/Utils.h" +#include "QualityControl/QcInfoLogger.h" +#include + +namespace o2::quality_control_modules::common +{ + +namespace internal +{ + +class Thresholds +{ + public: + /// \brief thersholds initialization from custom parameters + Thresholds(const CustomParameters& customParameters, const std::string& plotName, const std::string& qualityLabel, const Activity& activity); + + /// \brief function to retrieve the thresholds for a given interaction rate, if available + std::optional> getThresholdsForRate(double rate); + + private: + /// vectors of [min,max,rate] tuples. The first two values are the minimum and maximum threshold values, + /// and the third is the associated reference interaction rate (optional) + std::vector>> mThresholds; +}; + +Thresholds::Thresholds(const CustomParameters& customParameters, const std::string& plotName, const std::string& qualityLabel, const Activity& activity) +{ + // get configuration parameter associated to the key + std::string parKey = std::string("thresholds") + qualityLabel; + if (!plotName.empty()) { + parKey += ":"; + parKey += plotName; + } + + std::string parValue = getFromExtendedConfig(activity, customParameters, parKey); + if (parValue.empty()) { + return; + } + + // extract information for each nominal interaction rate value + auto interactionRatePoints = o2::utils::Str::tokenize(parValue, ';', false, true); + for (const auto& interactionRatePoint : interactionRatePoints) { + double thresholdMin = 0; + double thresholdMax = 0; + std::optional nominalRate; + + // extract interaction rate and threshold pairs + auto rateAndThresholds = o2::utils::Str::tokenize(interactionRatePoint, ':', false, true); + std::string thresholdValues; + if (rateAndThresholds.size() == 2) { + // the token contains both the rate and threshold values + // the rate is comverted from kHz to Hz + try { + nominalRate = std::stod(rateAndThresholds[0]) * 1000; + } catch (std::exception& exception) { + ILOG(Error, Support) << "Cannot convert values from string to double for " << qualityLabel << " thresholds of plot \"" << plotName << "\", string is " << rateAndThresholds[0] << ENDM; + } + thresholdValues = rateAndThresholds[1]; + } else { + // the token only contains threshold values, which are then valid for any rate + thresholdValues = rateAndThresholds[0]; + } + + // extract the comma-separated minimum and maximum threshold values + std::vector thresholdMinMax = o2::utils::Str::tokenize(thresholdValues, ',', false, true); + if (thresholdMinMax.size() == 2) { + try { + thresholdMin = std::stod(thresholdMinMax[0]); + thresholdMax = std::stod(thresholdMinMax[1]); + } catch (std::exception& exception) { + ILOG(Error, Support) << "Cannot convert values from string to double for " << qualityLabel << " thresholds of plot \"" << plotName << "\", string is " << thresholdValues << ENDM; + } + } + + mThresholds.emplace_back(std::make_tuple(thresholdMin, thresholdMax, nominalRate)); + } +} + +static std::pair interpolateThresholds(double fraction, const std::tuple>& thresholdsLow, const std::tuple>& thresholdsHigh) +{ + double thresholdMin = std::get<0>(thresholdsLow) * (1.0 - fraction) + std::get<0>(thresholdsHigh) * fraction; + double thresholdMax = std::get<1>(thresholdsLow) * (1.0 - fraction) + std::get<1>(thresholdsHigh) * fraction; + return std::make_pair(thresholdMin, thresholdMax); +} + +std::optional> Thresholds::getThresholdsForRate(double rate) +{ + std::optional> result; + + // index and corresponding rate of the element immediately below the requested rate + int indexLow = -1; + double rateLow = -1; + + // index and corresponding rate of the element immediately above the requested rate + int indexHigh = -1; + double rateHigh = -1; + + // search the closest element above and below the requested rate + for (int index = 0; index < mThresholds.size(); index++) { + const auto& element = mThresholds[index]; + // skip if rate is not specified + if (!std::get<2>(element)) { + continue; + } + double rateCurrent = std::get<2>(element).value(); + + if (rateCurrent <= rate) { + if (indexLow < 0 || rateLow < rateCurrent) { + indexLow = index; + rateLow = rateCurrent; + } + } + + if (rateCurrent >= rate) { + if (indexHigh < 0 || rateHigh > rateCurrent) { + indexHigh = index; + rateHigh = rateCurrent; + } + } + } + + if (indexLow >= 0 && indexHigh >= 0 && indexLow != indexHigh) { + double fraction = (rate - rateLow) / (rateHigh - rateLow); + result = interpolateThresholds(fraction, mThresholds[indexLow], mThresholds[indexHigh]); + } else if (indexLow >= 0) { + result = std::make_pair(std::get<0>(mThresholds[indexLow]), std::get<1>(mThresholds[indexLow])); + } else if (indexHigh >= 0) { + result = std::make_pair(std::get<0>(mThresholds[indexHigh]), std::get<1>(mThresholds[indexHigh])); + } else if (mThresholds.size() > 0) { + result = std::make_pair(std::get<0>(mThresholds[0]), std::get<1>(mThresholds[0])); + } + + return result; +} + +class XYRanges +{ + public: + /// \brief range initialization from custom parameters + XYRanges(const CustomParameters& customParameters, const std::string& plotName, const std::string& axisName, const Activity& activity); + + /// \brief retrieve the axis range over which the check must be restricted + std::optional> getRange() { return mRange; } + + private: + /// \brief axis range over which the check must be restricted + std::optional> mRange; +}; + +XYRanges::XYRanges(const CustomParameters& customParameters, const std::string& plotName, const std::string& axisName, const Activity& activity) +{ + // build the key associated to the configuration parameter + std::string parKey = std::string("range") + axisName; + if (!plotName.empty()) { + parKey += ":"; + parKey += plotName; + } + + std::string parValue = getFromExtendedConfig(activity, customParameters, parKey); + if (parValue.empty()) { + return; + } + + // extract min and max values of the range + auto rangeMinMax = o2::utils::Str::tokenize(parValue, ',', false, true); + if (rangeMinMax.size() != 2) { + ILOG(Error, Support) << "Cannot parse values for " << axisName << " axis range of plot \"" << plotName << "\", string is " << parValue << ENDM; + return; + } + + try { + mRange = std::make_pair(std::stof(rangeMinMax[0]), std::stof(rangeMinMax[1])); + } catch (std::exception& exception) { + ILOG(Error, Support) << "Cannot convert values from string to double for " << axisName << " axis range of plot \"" << plotName << "\", string is " << parValue << ENDM; + } +} + +} // namespace internal + +CheckerThresholdsConfig::CheckerThresholdsConfig(const CustomParameters& customParameters, const Activity& activity) + : mCustomParameters(customParameters), mActivity(activity) +{ + mDefaultThresholds[0] = std::make_shared(mCustomParameters, std::string(), std::string("Bad"), mActivity); + mDefaultThresholds[1] = std::make_shared(mCustomParameters, std::string(), std::string("Medium"), mActivity); + + mDefaultRanges[0] = std::make_shared(mCustomParameters, std::string(), std::string("X"), mActivity); + mDefaultRanges[1] = std::make_shared(mCustomParameters, std::string(), std::string("Y"), mActivity); +} + +void CheckerThresholdsConfig::initThresholdsForPlot(const std::string& plotName) +{ + // if not done yet, read from the configuration the threshold settings specific to plotName + using MapElementType = std::unordered_map>::value_type; + if (mThresholds[0].count(plotName) == 0) { + mThresholds[0].emplace(MapElementType(plotName, std::make_shared(mCustomParameters, plotName, std::string("Bad"), mActivity))); + mThresholds[1].emplace(MapElementType(plotName, std::make_shared(mCustomParameters, plotName, std::string("Medium"), mActivity))); + } +} + +std::array>, 2> CheckerThresholdsConfig::getThresholdsForPlot(const std::string& plotName, double rate) +{ + initThresholdsForPlot(plotName); + + std::array>, 2> result; + + // get thresholds for Bad and Medium quality + for (size_t index = 0; index < 2; index++) { + result[index] = mThresholds[index][plotName]->getThresholdsForRate(rate); + if (!result[index]) { + // try with default threshold settings if the plot-specific ones are not available + result[index] = mDefaultThresholds[index]->getThresholdsForRate(rate); + } + } + + return result; +} + +void CheckerThresholdsConfig::initRangesForPlot(const std::string& plotName) +{ + // if not done yet, read from the configuration the ranges settings specific to plotName + using MapElementType = std::unordered_map>::value_type; + if (mRanges[0].count(plotName) == 0) { + mRanges[0].emplace(MapElementType(plotName, std::make_shared(mCustomParameters, plotName, std::string("X"), mActivity))); + mRanges[1].emplace(MapElementType(plotName, std::make_shared(mCustomParameters, plotName, std::string("Y"), mActivity))); + } +} + +std::array>, 2> CheckerThresholdsConfig::getRangesForPlot(const std::string& plotName) +{ + initRangesForPlot(plotName); + + std::array>, 2> result; + + // get ranges for X and Y axes + for (size_t index = 0; index < 2; index++) { + result[index] = mRanges[index][plotName]->getRange(); + if (!result[index]) { + // try with default threshold settings if the plot-specific ones are not available + result[index] = mDefaultRanges[index]->getRange(); + } + } + + return result; +} + +} // namespace o2::quality_control_modules::common diff --git a/Modules/Common/src/TrendCheck.cxx b/Modules/Common/src/TrendCheck.cxx new file mode 100644 index 0000000000..0cf1e6c242 --- /dev/null +++ b/Modules/Common/src/TrendCheck.cxx @@ -0,0 +1,466 @@ +// Copyright 2019-2020 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// +/// \file TrendCheck.cxx +/// \author Andrea Ferrero +/// + +#include "Common/TrendCheck.h" +#include "Common/CheckerThresholdsConfig.h" +#include "QualityControl/MonitorObject.h" +#include "QualityControl/Quality.h" +#include "QualityControl/QcInfoLogger.h" +#include +#include +#include +#include +#include +#include +#include + +namespace o2::quality_control_modules::common +{ + +void TrendCheck::configure() +{ +} + +void TrendCheck::startOfActivity(const Activity& activity) +{ + mActivity = activity; + + // initialize the thresholds configuration + mThresholds = std::make_shared(mCustomParameters, mActivity); + + // comparison method between actual values and thresholds + std::string parKey = "trendCheckMode"; + auto parOpt = mCustomParameters.atOptional(parKey, mActivity); + if (!parOpt) { + parOpt = mCustomParameters.atOptional(parKey); + } + mTrendCheckMode = ExpectedRange; + if (parOpt.has_value()) { + if (parOpt.value() == "DeviationFromMean") { + mTrendCheckMode = DeviationFromMean; + } else if (parOpt.value() == "StdDeviation") { + mTrendCheckMode = StdDeviation; + } else if (parOpt.value() != "ExpectedRange") { + ILOG(Warning, Devel) << "unrecognized threshold mode \"" << parOpt.value() << "\", using default \"ExpectedRange\" mode" << ENDM; + } + } + switch (mTrendCheckMode) { + case ExpectedRange: + ILOG(Info, Support) << "thresholds mode set to \"ExpectedRange\"" << ENDM; + break; + case DeviationFromMean: + ILOG(Info, Support) << "thresholds mode set to \"DeviationFromMean\"" << ENDM; + break; + case StdDeviation: + ILOG(Info, Support) << "thresholds mode set to \"StdDeviation\"" << ENDM; + break; + } + + // number of points, excluding the last one, used to compute the average and standard deviation of the trend + // all points are considered if the value is smaller or equal to zero + parKey = "nPointsForAverage"; + parOpt = mCustomParameters.atOptional(parKey, mActivity); + if (!parOpt) { + parOpt = mCustomParameters.atOptional(parKey); + } + if (parOpt.has_value()) { + if (parOpt.value() == "all") { + mNPointsForAverage = 0; + } else { + mNPointsForAverage = std::stoi(parOpt.value()); + } + } + + if (mNPointsForAverage == 0) { + ILOG(Info, Support) << "using all points for statistics calculation" << ENDM; + } else { + ILOG(Info, Support) << "using at most " << mNPointsForAverage << " points for statistics calculation" << ENDM; + } + + // position and size of the label showing the result of the check + parKey = "qualityLabelPosition"; + parOpt = mCustomParameters.atOptional(parKey, mActivity); + if (!parOpt) { + parOpt = mCustomParameters.atOptional(parKey); + } + if (parOpt.has_value()) { + auto position = o2::utils::Str::tokenize(parOpt.value(), ',', false, true); + if (position.size() == 2) { + mQualityLabelPosition = std::make_pair(std::stof(position[0]), std::stof(position[1])); + } + } + + parKey = "qualityLabelSize"; + parOpt = mCustomParameters.atOptional(parKey, mActivity); + if (!parOpt) { + parOpt = mCustomParameters.atOptional(parKey); + } + if (parOpt.has_value()) { + auto position = o2::utils::Str::tokenize(parOpt.value(), ',', false, true); + if (position.size() == 2) { + mQualityLabelSize = std::make_pair(std::stof(position[0]), std::stof(position[1])); + } + } +} + +void TrendCheck::endOfActivity(const Activity& activity) +{ + // reset all members + mActivity = Activity{}; + mAverageTrend.clear(); + mThresholdsTrendBad.clear(); + mThresholdsTrendMedium.clear(); + mQualities.clear(); +} + +static std::string getBaseName(std::string name) +{ + auto pos = name.rfind("/"); + return ((pos < std::string::npos) ? name.substr(pos + 1) : name); +} + +// compute the mean and standard deviation from a given number of points bewfore the last one +// all points are used if nPointsForAverage <= 0 +static std::optional> getGraphStatistics(TGraph* graph, int nPointsForAverage) +{ + std::optional> result; + + int nPoints = graph->GetN(); + // we need at least two points before the last one + if (nPoints < 3) { + return result; + } + + // index of the last-but-one point + int pointIndexMax = nPoints - 2; + // index of the starting point for the mean and stadrad deviation computation + int pointIndexMin = (nPointsForAverage > 0 && pointIndexMax >= nPointsForAverage) ? pointIndexMax - nPointsForAverage + 1 : 0; + // number of points used for the computation, must be greater than one + int nPointsForStats = pointIndexMax - pointIndexMin + 1; + if (nPointsForStats < 2) { + return result; + } + + // compute the mean of the points + double mean = 0; + for (int pointIndex = pointIndexMin; pointIndex <= pointIndexMax; pointIndex++) { + mean += graph->GetPointY(pointIndex); + } + mean /= nPointsForStats; + + // compute the standard deviation of the points + double stdDev = 0; + for (int pointIndex = pointIndexMin; pointIndex <= pointIndexMax; pointIndex++) { + double delta = graph->GetPointY(pointIndex) - mean; + stdDev += delta * delta; + } + stdDev /= (nPointsForStats - 1) * nPointsForStats; + stdDev = std::sqrt(stdDev); + + result = std::make_pair(mean, stdDev); + return result; +} + +double TrendCheck::getInteractionRate() +{ + return 750000; +} + +/// \brief compute the thresholds for a given graph, taking into account the current interaction rate +/// +/// \param key string identifying some plot-specific threshold values +/// \param graph the graph object to be checked +/// \return an array of optional (min,max) threshold values for Bad and Medium qualities +std::array>, 2> TrendCheck::getThresholds(std::string plotName, TGraph* graph) +{ + double rate = getInteractionRate(); + std::array>, 2> result = mThresholds->getThresholdsForPlot(plotName, rate); + if (!result[0]) { + ILOG(Warning, Support) << "Cannot retrieve thresholds for \"" << plotName << "\"" << ENDM; + return result; + } + + if (mTrendCheckMode != ExpectedRange) { + // the thresholds retrieved from the configuration need to be converted into absolute values + auto graphStatistics = getGraphStatistics(graph, mNPointsForAverage); + if (!graphStatistics) { + result[0].reset(); + result[1].reset(); + return result; + } + + if (mTrendCheckMode == DeviationFromMean) { + double mean = graphStatistics.value().first; + // the thresholds retrieved from the configuration are relative to the mean value of the last N points, we need to convert them into absolute values + if (result[0].has_value()) { + result[0].value().first = mean + result[0].value().first * std::fabs(mean); + result[0].value().second = mean + result[0].value().second * std::fabs(mean); + } + + if (result[1].has_value()) { + result[1].value().first = mean + result[1].value().first * std::fabs(mean); + result[1].value().second = mean + result[1].value().second * std::fabs(mean); + } + } else if (mTrendCheckMode == StdDeviation) { + // the thresholds retrieved from the configuration are expressed as number of sigmas from the mean value of the last N points, we need to convert them into absolute values + double mean = graphStatistics.value().first; + double stdDevOfMean = graphStatistics.value().second; + double lastPointValue = graph->GetPointY(graph->GetN() - 1); + double lastPointError = graph->GetErrorY(graph->GetN() - 1); + if (lastPointError < 0) { + lastPointError = 0; + } + const double totalError = sqrt(stdDevOfMean * stdDevOfMean + lastPointError * lastPointError); + + result[0].value().first = mean + result[0].value().first * totalError; + result[0].value().second = mean + result[0].value().second * totalError; + + if (result[1].has_value()) { + result[1].value().first = mean + result[1].value().first * totalError; + result[1].value().second = mean + result[1].value().second * totalError; + } + } + } + + return result; +} + +// helper function to retrieve a TGraph drawn inside a TPad +TGraph* getGraphFromPad(TPad* pad) +{ + if (!pad) { + return nullptr; + } + + TGraph* graph{ nullptr }; + int jList = pad->GetListOfPrimitives()->LastIndex(); + for (; jList >= 0; jList--) { + graph = dynamic_cast(pad->GetListOfPrimitives()->At(jList)); + if (graph) { + break; + } + } + + return graph; +} + +// retrieve a vector containing all the TGraph objects embedded in a given TObject +// the TObject can also be the TGraph itself +void TrendCheck::getGraphsFromObject(TObject* object, std::vector& graphs) +{ + TGraph* graph = dynamic_cast(object); + if (graph) { + graphs.push_back(graph); + } else { + // extract the TGraph from the canvas + TCanvas* canvas = dynamic_cast(object); + auto graph = getGraphFromPad(canvas); + if (graph) { + graphs.push_back(graph); + } else { + ILOG(Info, Devel) << "No TGraph found in the canvas, checking sub-pads" << ENDM; + // loop over the pads in the canvas and extract the TGraph from each pad + const int numberPads = canvas->GetListOfPrimitives()->GetEntries(); + for (int iPad = 0; iPad < numberPads; iPad++) { + auto pad = dynamic_cast(canvas->GetListOfPrimitives()->At(iPad)); + graph = getGraphFromPad(pad); + if (graph) { + graphs.push_back(graph); + } + } + } + } +} + +Quality TrendCheck::check(std::map>* moMap) +{ + for (auto& [moKey, mo] : *moMap) { + + std::vector graphs; + getGraphsFromObject(mo->getObject(), graphs); + if (graphs.empty()) { + continue; + } + + auto moName = mo->getName(); + auto key = getBaseName(moName); + + for (size_t graphIndex = 0; graphIndex < graphs.size(); graphIndex++) { + + TGraph* graph = graphs[graphIndex]; + if (!graph) { + continue; + } + + auto graphName = moName + "_" + std::to_string(graphIndex); + + // check that the graph is not empty + int nPoints = graph->GetN(); + if (nPoints < 1) { + continue; + } + + // get the value for the last point + double value = graph->GetPointY(nPoints - 1); + + // get acceptable range for the current plot + auto thresholds = getThresholds(key, graph); + // check that at least the thresholds for Bad quality are available + if (!thresholds[0]) { + continue; + } + + // Quality is good by default, unless the last point is outside the acceptable range + mQualities[graphName] = Quality::Good; + + mThresholdsTrendBad[graphName].emplace_back(graph->GetPointX(nPoints - 1), thresholds[0].value()); + if (thresholds[1].has_value()) { + mThresholdsTrendMedium[graphName].emplace_back(graph->GetPointX(nPoints - 1), thresholds[1].value()); + } + + if (value < thresholds[0]->first || value > thresholds[0]->second) { + mQualities[graphName] = Quality::Bad; + } else if (thresholds[1].has_value()) { + if (value < thresholds[1]->first || value > thresholds[1]->second) { + mQualities[graphName] = Quality::Medium; + } + } + } + } + + // we return the worse quality of all the objects we checked, but we preserve all FlagTypes + Quality result = mQualities.empty() ? Quality::Null : Quality::Good; + for (auto& [key, quality] : mQualities) { + (void)key; + for (const auto& flag : quality.getFlags()) { + result.addFlag(flag.first, flag.second); + } + if (quality.isWorseThan(result)) { + result.set(quality); + } + } + + return result; +} + +std::string TrendCheck::getAcceptedType() { return "TObject"; } + +static void drawThresholds(TGraph* graph, const std::vector>>& thresholds, int lineColor, int lineStyle) +{ + if (thresholds.empty()) { + return; + } + + double rangeMin = graph->GetMinimum(); + double rangeMax = graph->GetMaximum(); + + rangeMin = TMath::Min(rangeMin, TMath::MinElement(graph->GetN(), graph->GetY())); + rangeMax = TMath::Max(rangeMax, TMath::MaxElement(graph->GetN(), graph->GetY())); + + double* xValues = new double[thresholds.size()]; + double* yValuesMin = new double[thresholds.size()]; + double* yValuesMax = new double[thresholds.size()]; + + for (size_t index = 0; index < thresholds.size(); index++) { + const auto& thresholdsPoint = thresholds[index]; + xValues[index] = thresholdsPoint.first; + yValuesMin[index] = thresholdsPoint.second.first; + yValuesMax[index] = thresholdsPoint.second.second; + + // add some margin above an below the threshold values, if needed + auto delta = yValuesMax[index] - yValuesMin[index]; + rangeMin = TMath::Min(rangeMin, yValuesMin[index] - 0.1 * delta); + rangeMax = TMath::Max(rangeMax, yValuesMax[index] + 0.1 * delta); + } + + TPolyLine* lineMin = new TPolyLine(thresholds.size(), xValues, yValuesMin); + lineMin->SetLineColor(lineColor); + lineMin->SetLineStyle(lineStyle); + lineMin->SetLineWidth(2); + graph->GetListOfFunctions()->Add(lineMin); + + TPolyLine* lineMax = new TPolyLine(thresholds.size(), xValues, yValuesMax); + lineMax->SetLineColor(lineColor); + lineMax->SetLineStyle(lineStyle); + lineMax->SetLineWidth(2); + graph->GetListOfFunctions()->Add(lineMax); + + graph->SetMinimum(rangeMin); + graph->SetMaximum(rangeMax); +} + +// return the ROOT color index associated to a give quality level +static int getQualityColor(const Quality& q) +{ + if (q == Quality::Null) + return kViolet - 6; + if (q == Quality::Bad) + return kRed; + if (q == Quality::Medium) + return kOrange - 3; + if (q == Quality::Good) + return kGreen + 2; + + return 0; +} + +void TrendCheck::beautify(std::shared_ptr mo, Quality checkResult) +{ + std::vector graphs; + getGraphsFromObject(mo->getObject(), graphs); + if (graphs.empty()) { + return; + } + + auto moName = mo->getName(); + + for (size_t graphIndex = 0; graphIndex < graphs.size(); graphIndex++) { + + TGraph* graph = graphs[graphIndex]; + if (!graph || graph->GetN() < 1) { + return; + } + + auto graphName = moName + "_" + std::to_string(graphIndex); + + Quality quality = mQualities[graphName]; + + if (mThresholdsTrendMedium.count(graphName) > 0) { + const auto& thresholds = mThresholdsTrendMedium[graphName]; + drawThresholds(graph, thresholds, kOrange, 9); + } + + if (mThresholdsTrendBad.count(graphName) > 0) { + const auto& thresholds = mThresholdsTrendBad[graphName]; + drawThresholds(graph, thresholds, kRed, 9); + } + + // draw the label with the check result + TPaveText* label = new TPaveText(mQualityLabelPosition.first, + mQualityLabelPosition.second, + mQualityLabelPosition.first + mQualityLabelSize.first, + mQualityLabelPosition.second + mQualityLabelSize.second, + "brNDC"); + label->SetTextColor(getQualityColor(quality)); + label->AddText(quality.getName().c_str()); + label->SetFillStyle(0); + label->SetBorderSize(0); + label->SetTextAlign(12); + graph->GetListOfFunctions()->Add(label); + } +} + +} // namespace o2::quality_control_modules::common diff --git a/doc/Advanced.md b/doc/Advanced.md index 3f4690c4dc..9800958c16 100644 --- a/doc/Advanced.md +++ b/doc/Advanced.md @@ -1723,6 +1723,145 @@ The number of cycles during which we tolerate increasing (or not respectively) t ``` In the example above, the quality goes to bad when there are 3 cycles in a row with no increase in the number of entries. +## Common check `TrendCheck` + +This check compares the last point of a trending plot with some minimum and maximum thresholds. + +The thresholds can be defined in different ways, controlled by the `trendCheckMode` parameter: + +* `"trendCheckMode": "ExpectedRange"` ==> fixed threshold values: the thresholds represent the minimum and maximum allowed values for the last point +* `"trendCheckMode": "DeviationFromMean"` ==> the thresholds represent the relative variation with respect to the mean of the N points preceding the last one (which is checked) +For example: + +``` + "thresholdsBad": "-0.1,0.2", +``` + +means that the last point should not be lower than `(mean - 0.1 * |mean|)` and not higher than `(mean + 0.2 * |mean|)`. +* `"trendCheckMode": "StdDeviation"` ==> the thresholds represent the relative variation with respect to the total error of the N points preceding the last one (which is checked) +For example: + +``` + "thresholdsBad": "-1,2", +``` + +means that the last point should not be lower than `(mean - 1 * TotError)` and not higher than `(mean + 2 * TotError)`. +The total error takes into account the standard deviation of the N points before the current one, as well as the error associated to the current point. + + +In general, the threshold values are configured separately for the Bad and Medium qualities, like this: + +``` + "thresholdsBad": "min,max", + "thresholdsMedium": "min,max", +``` + +It is also possible to customize the threshold values for specific plots: + +``` + "thresholdsBad:PlotName": "min,max", + "thresholdsMedium:PlotName": "min,max", +``` +Here `PlotName` represents the name of the plot, stripped from all the QCDB path. + +The position and size of the text label that shows the check result can also be customized in the configuration: + +``` + "qualityLabelPosition": "0.5,0.8", + "qualityLabelSize": "0.5,0.1" +``` +The values are relative to the canvas size, so in the example above the label width is 50% of the canvas width and the label height is 10% of the canvas height. + + +#### Full configuration example + +```json + "MyTrendingCheckFixed": { + "active": "true", + "className": "o2::quality_control_modules::common::TrendCheck", + "moduleName": "QualityControl", + "detectorName": "TST", + "policy": "OnAll", + "extendedCheckParameters": { + "default": { + "default": { + "trendCheckMode": "ExpectedRange", + "nPointsForAverage": "3", + "": "default threshold values not specifying the plot name", + "thresholdsMedium": "3000,7000", + "thresholdsBad": "2000,8000" + "": "thresholds specific to one plot", + "thresholdsBad:mean_of_histogram_2": "1000,9000", + "": "customize the position and size of the text label showing the quality" + "qualityLabelPosition": "0.5,0.8", + "qualityLabelSize": "0.5,0.1" + } + } + }, + "dataSource": [ + { + "type": "PostProcessing", + "name": "MyTrendingTask", + "MOs" : [ + "mean_of_histogram_1", "mean_of_histogram_2" + ] + } + ] + }, + "MyTrendingCheckMean": { + "active": "true", + "className": "o2::quality_control_modules::common::TrendCheck", + "moduleName": "QualityControl", + "detectorName": "TST", + "policy": "OnAll", + "extendedCheckParameters": { + "default": { + "default": { + "trendCheckMode": "DeviationFromMean", + "nPointsForAverage": "3", + "thresholdsBad": "-0.2,0.5", "": "from -20% to +50%", + "thresholdsMedium": "-0.1,0.25" + } + } + }, + "dataSource": [ + { + "type": "PostProcessing", + "name": "MyTrendingTask", + "MOs" : [ + "mean_of_histogram_3" + ] + } + ] + }, + "MyTrendingCheckStdDev": { + "active": "true", + "className": "o2::quality_control_modules::common::TrendCheck", + "moduleName": "QualityControl", + "detectorName": "TST", + "policy": "OnAll", + "extendedCheckParameters": { + "default": { + "default": { + "trendCheckMode": "StdDeviation", + "nPointsForAverage": "5", + "thresholdsBad:mean_of_histogram_3": "-2,5", "": "from -2sigma to +5sigma", + "thresholdsMedium:mean_of_histogram_3": "-1,2.5" + } + } + }, + "dataSource": [ + { + "type": "PostProcessing", + "name": "MyTrendingTask", + "MOs" : [ + "mean_of_histogram_4" + ] + } + ] + } +``` + ## Update the shmem segment size of a detector In consul go to `o2/runtime/aliecs/defaults` and modify the file corresponding to the detector: [det]_qc_shm_segment_size