From 57965ff2a1f3f1ccb3f2d8facbdb7741de6db852 Mon Sep 17 00:00:00 2001 From: aferrero2707 Date: Fri, 20 Sep 2024 13:55:53 +0200 Subject: [PATCH 1/5] [Common] handle std::string types in configuration helper function The `std::string` type was not correctly handled because the standard library does not provide an "identity" override for `std::to_string()` --- Modules/Common/include/Common/Utils.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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); } From ce72c936ce71f680d2eff24cd2be8fad211fba44 Mon Sep 17 00:00:00 2001 From: aferrero2707 Date: Fri, 20 Sep 2024 15:39:12 +0200 Subject: [PATCH 2/5] [Common] add utility class to handle the checker thresholds The ultility class provides the following functionalities: * stores interaction-rate-dependent threshold values * stores optional axis ranges on which the check should be restricted * provides the code to retrieve the threshold and range values from the custom parameters * provides a function to retrieve the optimal threshold values for a given interaction rate --- .../include/Common/CheckerThresholdsConfig.h | 71 +++++ .../Common/src/CheckerThresholdsConfig.cxx | 264 ++++++++++++++++++ 2 files changed, 335 insertions(+) create mode 100644 Modules/Common/include/Common/CheckerThresholdsConfig.h create mode 100644 Modules/Common/src/CheckerThresholdsConfig.cxx 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/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 From 83fdd471356f1221158ec8653fdec122395b21b2 Mon Sep 17 00:00:00 2001 From: aferrero2707 Date: Fri, 20 Sep 2024 15:58:56 +0200 Subject: [PATCH 3/5] [Common] added generic checker from trend graphs The checker is designed to verify that the values of trend graphs are within given minimum and maximum limits. The limits can be specified as fixed values, or values relative to either the mean or the standard deviation of a given set of graph points. --- Modules/Common/CMakeLists.txt | 3 + Modules/Common/include/Common/LinkDef.h | 1 + Modules/Common/include/Common/TrendCheck.h | 83 ++++ Modules/Common/src/TrendCheck.cxx | 466 +++++++++++++++++++++ 4 files changed, 553 insertions(+) create mode 100644 Modules/Common/include/Common/TrendCheck.h create mode 100644 Modules/Common/src/TrendCheck.cxx 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/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/src/TrendCheck.cxx b/Modules/Common/src/TrendCheck.cxx new file mode 100644 index 0000000000..043b9c66a9 --- /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 N = pointIndexMax - pointIndexMin + 1; + if (N < 2) { + return result; + } + + // compute the mean of the points + double mean = 0; + for (int pointIndex = pointIndexMin; pointIndex <= pointIndexMax; pointIndex++) { + mean += graph->GetPointY(pointIndex); + } + mean /= N; + + // 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 /= (N - 1) * N; + 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 From 3753734c7df20324e8d86d3c6fd9112da6b2ca0f Mon Sep 17 00:00:00 2001 From: aferrero2707 Date: Sat, 19 Oct 2024 18:41:00 +0200 Subject: [PATCH 4/5] [Common] improved name of one variable --- Modules/Common/src/TrendCheck.cxx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/Common/src/TrendCheck.cxx b/Modules/Common/src/TrendCheck.cxx index 043b9c66a9..0cf1e6c242 100644 --- a/Modules/Common/src/TrendCheck.cxx +++ b/Modules/Common/src/TrendCheck.cxx @@ -149,8 +149,8 @@ static std::optional> getGraphStatistics(TGraph* graph // 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 N = pointIndexMax - pointIndexMin + 1; - if (N < 2) { + int nPointsForStats = pointIndexMax - pointIndexMin + 1; + if (nPointsForStats < 2) { return result; } @@ -159,7 +159,7 @@ static std::optional> getGraphStatistics(TGraph* graph for (int pointIndex = pointIndexMin; pointIndex <= pointIndexMax; pointIndex++) { mean += graph->GetPointY(pointIndex); } - mean /= N; + mean /= nPointsForStats; // compute the standard deviation of the points double stdDev = 0; @@ -167,7 +167,7 @@ static std::optional> getGraphStatistics(TGraph* graph double delta = graph->GetPointY(pointIndex) - mean; stdDev += delta * delta; } - stdDev /= (N - 1) * N; + stdDev /= (nPointsForStats - 1) * nPointsForStats; stdDev = std::sqrt(stdDev); result = std::make_pair(mean, stdDev); From 8d111f094089e103369c56c6fc4c6a67932551ab Mon Sep 17 00:00:00 2001 From: aferrero2707 Date: Sat, 19 Oct 2024 18:41:29 +0200 Subject: [PATCH 5/5] [Common] added documentation for TrendCheck --- doc/Advanced.md | 139 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) 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