ATLAS Offline Software
Loading...
Searching...
No Matches
AnomalyDetectionBDT.cxx
Go to the documentation of this file.
1/*
2 Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
3*/
4
5/***************************
6
7 * AnomalyDetectionBDT.cxx
8 * Created by Santiago Cane 4/15/2025
9 *
10 * @brief Algorithm is a Boosted Decision Tree for regression.
11 * It uses the Pt, Eta and Phi of 3 muons to regress an anomaly score of a
12 * previously trained Variational Autoencoder and compares it to a threshold value.
13 *
14 *
15 *@param ScoreThreshold
16 *
17 ***************************/
18
23#ifndef TRIGCONF_STANDALONE
25#endif
26#include <fstream>
27#include <iostream>
28
29REGISTER_ALG_TCS(AnomalyDetectionBDT)
30
31template <typename T>
32std::string
33vectorToString(const std::vector<T>& vec) {
34 std::ostringstream oss;
35 oss << "[";
36 bool first = true;
37 for (const auto& val : vec) {
38 if (!first) {
39 oss << ", ";
40 }
41 oss << val;
42 first = false;
43 }
44 oss << "]";
45 return oss.str();
46}
47
48namespace TCS {
49 std::ostream&
50 operator<<(std::ostream& os, const TCS::Bin& bin) {
51 os << "Bin(score=" << bin.score << ", nVar=" << bin.nVar
52 << ", minVals=" << vectorToString(bin.minVals)
53 << ", maxVals=" << vectorToString(bin.maxVals) << ")";
54 return os;
55 }
56}
57
58namespace {
59
60 int scale(float val,float min, float max, int bits=7) {
61 float arrRange = max - min;
62 int nPoints = (1 << bits) - 1;
63 float resolution = arrRange / nPoints;
64 int out = static_cast<int>((val - min) / resolution);
65
66 // Staying within a valid range
67 if (out < 0) out = 0;
68 if (out > nPoints) out = nPoints;
69
70 return out;
71 }
72
73}
74
75namespace {
76 int mapval(int value, const std::string & vartype){
77
78 static const std::map<int,int> eta_LUT = {
80 //Add eta LUT here
81 {-100,4},
82 {-99,5},
83 {-98,5},
84 {-97,6},
85 {-96,7},
86 {-95,7},
87 {-94,8},
88 {-93,8},
89 {-92,9},
90 {-91,9},
91 {-90,10},
92 {-89,11},
93 {-88,11},
94 {-87,12},
95 {-86,12},
96 {-85,13},
97 {-84,14},
98 {-83,14},
99 {-82,15},
100 {-81,15},
101 {-80,16},
102 {-79,17},
103 {-78,17},
104 {-77,18},
105 {-76,18},
106 {-75,19},
107 {-74,19},
108 {-73,20},
109 {-72,21},
110 {-71,21},
111 {-70,22},
112 {-69,22},
113 {-68,23},
114 {-67,24},
115 {-66,24},
116 {-65,25},
117 {-64,25},
118 {-63,26},
119 {-62,27},
120 {-61,27},
121 {-60,28},
122 {-59,28},
123 {-58,29},
124 {-57,29},
125 {-56,30},
126 {-55,31},
127 {-54,31},
128 {-53,32},
129 {-52,32},
130 {-51,33},
131 {-50,34},
132 {-49,34},
133 {-48,35},
134 {-47,35},
135 {-46,36},
136 {-45,37},
137 {-44,37},
138 {-43,38},
139 {-42,38},
140 {-41,39},
141 {-40,39},
142 {-39,40},
143 {-38,41},
144 {-37,41},
145 {-36,42},
146 {-35,42},
147 {-34,43},
148 {-33,44},
149 {-32,44},
150 {-31,45},
151 {-30,45},
152 {-29,46},
153 {-28,47},
154 {-27,47},
155 {-26,48},
156 {-25,48},
157 {-24,49},
158 {-23,49},
159 {-22,50},
160 {-21,51},
161 {-20,51},
162 {-19,52},
163 {-18,52},
164 {-17,53},
165 {-16,54},
166 {-15,54},
167 {-14,55},
168 {-13,55},
169 {-12,56},
170 {-11,57},
171 {-10,57},
172 {-9,58},
173 {-8,58},
174 {-7,59},
175 {-6,59},
176 {-5,60},
177 {-4,61},
178 {-3,61},
179 {-2,62},
180 {-1,62},
181 {0,63},
182 {1,64},
183 {2,64},
184 {3,65},
185 {4,65},
186 {5,66},
187 {6,67},
188 {7,67},
189 {8,68},
190 {9,68},
191 {10,69},
192 {11,69},
193 {12,70},
194 {13,71},
195 {14,71},
196 {15,72},
197 {16,72},
198 {17,73},
199 {18,74},
200 {19,74},
201 {20,75},
202 {21,75},
203 {22,76},
204 {23,77},
205 {24,77},
206 {25,78},
207 {26,78},
208 {27,79},
209 {28,79},
210 {29,80},
211 {30,81},
212 {31,81},
213 {32,82},
214 {33,82},
215 {34,83},
216 {35,84},
217 {36,84},
218 {37,85},
219 {38,85},
220 {39,86},
221 {40,87},
222 {41,87},
223 {42,88},
224 {43,88},
225 {44,89},
226 {45,89},
227 {46,90},
228 {47,91},
229 {48,91},
230 {49,92},
231 {50,92},
232 {51,93},
233 {52,94},
234 {53,94},
235 {54,95},
236 {55,95},
237 {56,96},
238 {57,97},
239 {58,97},
240 {59,98},
241 {60,98},
242 {61,99},
243 {62,99},
244 {63,100},
245 {64,101},
246 {65,101},
247 {66,102},
248 {67,102},
249 {68,103},
250 {69,104},
251 {70,104},
252 {71,105},
253 {72,105},
254 {73,106},
255 {74,107},
256 {75,107},
257 {76,108},
258 {77,108},
259 {78,109},
260 {79,109},
261 {80,110},
262 {81,111},
263 {82,111},
264 {83,112},
265 {84,112},
266 {85,113},
267 {86,114},
268 {87,114},
269 {88,115},
270 {89,115},
271 {90,116},
272 {91,117},
273 {92,117},
274 {93,118},
275 {94,118},
276 {95,119},
277 {96,119},
278 {97,120},
279 {98,121},
280 {99,121},
281 {100,122}
283 };
284
285 static const std::map<int,int> phi_LUT = []{
286 std::map<int,int> m;
287 for (int i = 0; i <= 125; ++i) m.emplace(i,i);
288 m.emplace(126,127);
289 m.emplace(127,127);
290 return m;
291 }();
292
293 static const std::map<int,int> pt_LUT = {
295 //Add pt LUT here
296 {30,0},
297 {40,8},
298 {50,15},
299 {60,22},
300 {70,30},
301 {80,37},
302 {90,45},
303 {100,52},
304 {110,59},
305 {120,67},
306 {130,74},
307 {140,81},
308 {150,89},
309 {160,96},
310 {170,104},
311 {180,111},
312 {190,118},
313 {200,126}
315 };
316
317 //If a transformation is not found in the provided LUT, the function will return the original input
318
319 if(vartype=="eta"){
320 auto it_eta = eta_LUT.find(value);
321 return it_eta != eta_LUT.end() ? it_eta->second : value;
322 }
323
324 else if (vartype=="phi"){
325 auto it_phi = phi_LUT.find(value);
326 return it_phi != phi_LUT.end() ? it_phi->second : value;
327 }
328
329 else if(vartype =="pt"){
330 auto it_pt = pt_LUT.find(value);
331 return it_pt != pt_LUT.end() ? it_pt->second : value;
332 }
333
334 else{
335 return value;
336 }
337 }
338}
339
340TCS::Bin::Bin(const nlohmann::json& obj, int nVars)
341: score(obj["score"]), nVar(nVars) {
342 for (auto &[name, val] : obj["ranges"]["min"].items()) {
343 // this works for vectors [a,b,c] and dictionary values {"0": a, "1": b, "2": c}
344 minVals.push_back(val);
345 }
346 for (auto &[name, val] : obj["ranges"]["max"].items()) {
347 maxVals.push_back(val);
348 }
349}
350
351bool
352TCS::Bin::isInside(const std::vector<int64_t>& inputEvent) const {
353 for (size_t i = 0; i < inputEvent.size(); ++i) {
354 if (inputEvent[i] <= minVals[i]) {
355 return false;
356 }
357 else if (inputEvent[i] > maxVals[i]) {
358 return false;
359 }
360 }
361 return true;
362}
363
364
365TCS::Tree::Tree(const nlohmann::json& obj, int nVars) {
366 for (auto &[binName, bin] : obj["bins"].items()) {
367 bins.push_back(Bin(bin, nVars));
368 }
369}
370
371int64_t
372TCS::Tree::getTreeScore(const std::vector<int64_t>& inputEvent) const {
373 for(const Bin& bin : bins) {
374 if(bin.isInside(inputEvent)) {
375 return bin.score;
376 }
377 }
378 return 0;
379 //throw std::runtime_error("AnomalyDetection: the code has reached an unreachable state");
380}
381
382
383// constructor
385: DecisionAlg(name) {
386 defineParameter("MinET1",0);
387 defineParameter("MinET2",0);
388 defineParameter("ScoreThreshold", 1, 0);
389 defineParameter("ScoreThreshold", 1, 1);
390 defineParameter("MaxTob",3);
391 defineParameter("NumResultBits",2);
392 setNumberOutputBits(2); // 2 decision bits for 2 different score thresholds
393 // (BDT Regression score is calculated internally)
394}
395
396// destructor
399
402
403 p_MaxTob = parameter("MaxTob").value();
404 p_minEt1 = parameter("MinET1").value();
405 p_minEt2 = parameter("MinET2").value();
406 for (size_t i=0; i <numberOutputBits(); ++i) {
407 p_ScoreThreshold[i] = parameter("ScoreThreshold", i).value();
408 }
409 TRG_MSG_INFO("ADBDT: Threshold set to " << p_ScoreThreshold);
410 const std::string bdtfn = "TrigAnomalyDetectionBDT/2025-06-18/fwX-config_nomAD_Jun3_2pi200t20d.json";
411
412 std::string fileLocation;
413
414 #ifndef TRIGCONF_STANDALONE
415 fileLocation = PathResolver::find_calib_file(bdtfn);
416 #else
417 return StatusCode::SUCCESS;
418 #endif
419 // Reading in the config
420 std::ifstream configFile(fileLocation);
421
422 if (!configFile.is_open()) {
423 TRG_MSG_ERROR("Unable to open BDT configuration file." << fileLocation);
424 return StatusCode::FAILURE;
425 }
426
427 nlohmann::json bdt_config; // BDT configuration
428 configFile >> bdt_config;
429 m_nVar = bdt_config["nDim"];
430 m_mu1_ptmin = bdt_config["variable_float_ranges"]["flat_dimu_mu1_pt_100MeV"]["min"];
431 m_mu1_ptmax = bdt_config["variable_float_ranges"]["flat_dimu_mu1_pt_100MeV"]["max"];
432 m_mu1_etamin = bdt_config["variable_float_ranges"]["flat_dimu_mu1_eta"]["min"];
433 m_mu1_etamax = bdt_config["variable_float_ranges"]["flat_dimu_mu1_eta"]["max"];
434 m_mu1_phimin = bdt_config["variable_float_ranges"]["flat_dimu_mu1_phi_2pi"]["min"];
435 m_mu1_phimax = bdt_config["variable_float_ranges"]["flat_dimu_mu1_phi_2pi"]["max"];
436 m_mu2_ptmin = bdt_config["variable_float_ranges"]["flat_dimu_mu2_pt_100MeV"]["min"];
437 m_mu2_ptmax = bdt_config["variable_float_ranges"]["flat_dimu_mu2_pt_100MeV"]["max"];
438 m_mu2_etamin = bdt_config["variable_float_ranges"]["flat_dimu_mu2_eta"]["min"];
439 m_mu2_etamax = bdt_config["variable_float_ranges"]["flat_dimu_mu2_eta"]["max"];
440 m_mu2_phimin = bdt_config["variable_float_ranges"]["flat_dimu_mu2_phi_2pi"]["min"];
441 m_mu2_phimax = bdt_config["variable_float_ranges"]["flat_dimu_mu2_phi_2pi"]["max"];
442
443 for (auto& [treeName, tree] : bdt_config["trees"].items()) {
444 m_trees.emplace_back(tree, m_nVar);
445 }
446
447 TRG_MSG_DEBUG("In initialize. There are " << m_trees.size() << " trees for AnomalyDetectionBDT.");
448
449 //Setting simulation mode.
450 //This can be set to "lut" or to "math"
451 //"lut" mimics exactly what is done in firmware
452 m_sim_mode = "lut";
453 TRG_MSG_INFO("The sim_mode is " << m_sim_mode);
454
455 //Booking histograms
456 for (size_t i=0; i<numberOutputBits(); ++i) {
457 std::string hname_accept = "hAnomalyDetecionBDT_accept_bit"+std::to_string((int)i);
458 std::string hname_reject = "hAnomalyDetecionBDT_reject_bit"+std::to_string((int)i);
459
460 bookHist(m_histAccept, hname_accept, "AnomalyScore", 100, 0, 128);
461 bookHist(m_histReject, hname_reject, "AnomalyScore", 100, 0, 128);
462 }
463 return StatusCode::SUCCESS;
464}
465
467TCS::AnomalyDetectionBDT::processBitCorrect(const std::vector<TCS::TOBArray const *> & input,
468 const std::vector<TCS::TOBArray *> & output,
469 Decision & decision) {
470
471 if (input.size() != 1){
472 TCS_EXCEPTION("ADBDT algorithm expects only one TOBArray input (muons), but got " << input.size());
473 }
474
475 const TCS::TOBArray* muons = input[0];
476
477 //Reading in number of muons:
478 TRG_MSG_DEBUG("There are " << muons->size() << " muons.");
479
480 if (muons->size() < 2) {
481 TRG_MSG_DEBUG("There are less than 2 muons. Skipping event.");
482 m_totalScore = 0;
483 return StatusCode::SUCCESS;
484 }
485
486 bool hasAmbiguousInputs = TSU::isAmbiguousAnywhere(muons, p_MaxTob, std::min(p_minEt1, p_minEt2));
487
488 int64_t maxScore = 0;
489
490 size_t nMuons = p_MaxTob > 0 ? std::min(muons->size(), p_MaxTob) : muons->size();
491 //We define muon pairs based on the 2-3 leading muons
492 std::vector<std::pair<int, int>> muonPairs;
493 for (size_t i = 0; i < nMuons; ++i) {
494 for (size_t j = i+1; j < nMuons; ++j) {
495 muonPairs.emplace_back(i, j);
496 }
497 }
498
499 for (const auto& [i,j] : muonPairs) {
500
501 std::vector<int64_t> eventValues;
502
503 const auto& mu1 = (*muons)[i];
504 const auto& mu2 = (*muons)[j];
505
506 //ignore combinations failing minET cuts
507 if ( parType_t( mu1.Et() ) <= p_minEt1 ) continue;
508 if ( parType_t( mu2.Et() ) <= p_minEt2 ) continue;
509
510 if (scale(mu1.Et(),m_mu1_ptmin, m_mu1_ptmax) != mapval(mu1.Et(), "pt")
511 || scale(mu1.eta()/40.0, m_mu1_etamin, m_mu1_etamax) != mapval(mu1.eta(), "eta")
512 || scale(mu1.phi()/20.0, m_mu1_phimin, m_mu1_phimax) != mapval(mu1.phi(), "phi")
513 || scale(mu2.Et(),m_mu2_ptmin, m_mu2_ptmax) != mapval(mu2.Et(), "pt")
514 || scale(mu2.eta()/40.0, m_mu2_etamin, m_mu2_etamax) != mapval(mu2.eta(), "eta")
515 || scale(mu2.phi()/20.0, m_mu2_phimin, m_mu2_phimax) != mapval(mu2.phi(), "phi")){
516
517 TRG_MSG_WARNING("The math function for the mapping of Muon pt,eta,or phi does not match the LUT mapping. Check LUT, 'scale' function, or if illegal vartype was given to 'mapval' function");
518
519 }
520
521 if (mapval(mu1.Et(), "pt") == int(mu1.Et()) || mapval(mu2.Et(), "pt") == int(mu2.Et())) {
522
523 TRG_MSG_WARNING("No input transformation done by LUT for muon ET");
524
525 }
526
527 if (mapval(mu1.eta(), "eta") == mu1.eta() || mapval(mu2.eta(), "eta") == mu2.eta()) {
528
529 TRG_MSG_WARNING("No input transformation done by LUT for muon Eta");
530
531 }
532
533 if (mapval(mu1.phi(), "phi") == mu1.phi() || mapval(mu2.phi(), "phi") == mu2.phi()) {
534
535
536 TRG_MSG_DEBUG("No input transformation done by LUT for muon Phi"); //Not a warning, since current transformation for phi
537 //does not change the value except for one input number
538 }
539
540 if (m_sim_mode == "math"){
541 eventValues.push_back(scale(mu1.Et(), m_mu1_ptmin, m_mu1_ptmax)); // muon pT (100MeV)
542 eventValues.push_back(scale(mu1.eta()/40.0, m_mu1_etamin, m_mu1_etamax)); // muon eta (25mrad)
543 eventValues.push_back(scale(mu1.phi()/20.0, m_mu1_phimin, m_mu1_phimax)); // muon phi (50mrad)
544
545 eventValues.push_back(scale(mu2.Et(), m_mu2_ptmin, m_mu2_ptmax)); // muon pT (100MeV)
546 eventValues.push_back(scale(mu2.eta()/40.0, m_mu2_etamin, m_mu2_etamax)); // muon eta (25mrad)
547 eventValues.push_back(scale(mu2.phi()/20.0, m_mu2_phimin, m_mu2_phimax)); // muon phi (50mrad)
548 }
549
550 else if(m_sim_mode == "lut"){
551 eventValues.push_back(mapval(mu1.Et(), "pt"));
552 eventValues.push_back(mapval(mu1.eta(), "eta"));
553 eventValues.push_back(mapval(mu1.phi(), "phi"));
554
555 eventValues.push_back(mapval(mu2.Et(), "pt"));
556 eventValues.push_back(mapval(mu2.eta(), "eta"));
557 eventValues.push_back(mapval(mu2.phi(), "phi"));
558 }
559
560 else{
561 eventValues.push_back(scale(mu1.Et(), m_mu1_ptmin, m_mu1_ptmax)); // muon pT (100MeV)
562 eventValues.push_back(scale(mu1.eta()/40.0, m_mu1_etamin, m_mu1_etamax)); // muon eta (25mrad)
563 eventValues.push_back(scale(mu1.phi()/20.0, m_mu1_phimin, m_mu1_phimax)); // muon phi (50mrad)
564
565 eventValues.push_back(scale(mu2.Et(), m_mu2_ptmin, m_mu2_ptmax)); // muon pT (100MeV)
566 eventValues.push_back(scale(mu2.eta()/40.0, m_mu2_etamin, m_mu2_etamax)); // muon eta (25mrad)
567 eventValues.push_back(scale(mu2.phi()/20.0, m_mu2_phimin, m_mu2_phimax)); // muon phi (50mrad)
568 }
569
570 // calculate the score as sum of score from all trees
571 int64_t score = 0;
572 for (Tree tree : m_trees) {
573 score += tree.getTreeScore(eventValues);
574 }
575
576 // from all muon combinations keep the highest score
577 if (score > maxScore) {
578 maxScore = score;
579 }
580 }
581
582 m_totalScore = maxScore;
583 TRG_MSG_DEBUG("Anomaly Score = " << m_totalScore);
584
585 //Now, we compare and set the decision bit:
586 for (size_t i=0; i<numberOutputBits(); ++i){
587 bool accept = false;
588 // const bool fillAccept = fillHistos() && (fillHistosBasedOnHardware() ? getDecisionHardwareBit(i) : accept);
589 // const bool fillReject = fillHistos() && !fillAccept;
590 // const bool alreadyFilled = decision.bit(i);
591
593 accept = true;
594 decision.setBit(i,true);
595 for (size_t k = 0; k < 3 && k < muons->size(); ++k){
596 output[i]->push_back((*muons)[k]);
597 }
598 }
599
600 output[i]->setAmbiguityFlag(hasAmbiguousInputs);
601
602 if(fillHistos()) {
603 const bool fillAccept = fillHistosBasedOnHardware() ? getDecisionHardwareBit(i) : accept;
604 if (fillAccept) {
606 } else {
608 }
609 }
610
611 TRG_MSG_DEBUG("Decision for bit " << i << ": " <<(accept?"pass":"fail") << "with an anomaly score = " << m_totalScore << std::endl);
612 }
613 return StatusCode::SUCCESS;
614}
615
616TCS::StatusCode TCS::AnomalyDetectionBDT::process(const std::vector<TCS::TOBArray const *> & input,
617 const std::vector<TCS::TOBArray *> & output,
618 Decision & decision) {
619#ifdef TRIGCONF_STANDALONE
620 return StatusCode::SUCCESS;
621#endif
622 TCS::StatusCode status = processBitCorrect(input,output,decision);
623 if (!status.isSuccess()){
624 return status;
625 }
626
627 // Check if totalScore exceeds the bits that we expect
628 if (m_totalScore >= (1L << 8)) {
629 TRG_MSG_WARNING("ADBDT:The calculated anomaly score exceeds the allowed number of bits.");
630 }
631
633}
#define REGISTER_ALG_TCS(CLASS)
Definition AlgFactory.h:62
std::string vectorToString(const std::vector< T > &vec)
std::vector< size_t > vec
#define min(a, b)
Definition cfImp.cxx:40
#define max(a, b)
Definition cfImp.cxx:41
static std::string find_calib_file(const std::string &logical_file_name)
virtual StatusCode process(const std::vector< TCS::TOBArray const * > &input, const std::vector< TCS::TOBArray * > &output, Decision &decision)
virtual StatusCode processBitCorrect(const std::vector< TCS::TOBArray const * > &input, const std::vector< TCS::TOBArray * > &output, Decision &decision)
AnomalyDetectionBDT(const std::string &name)
std::vector< Tree > m_trees
virtual StatusCode initialize()
std::vector< int64_t > minVals
std::vector< int64_t > maxVals
bool isInside(const std::vector< int64_t > &inputEvent) const
Bin(const nlohmann::json &obj, int nVars)
const Parameter & parameter(const std::string &parameterName) const
const std::string & name() const
void bookHist(std::vector< std::string > &regName, const std::string &name, const std::string &title, const int binx, const int xmin, const int xmax)
void fillHist1D(const std::string &histName, double x)
void defineParameter(const std::string &name, TCS::parType_t value)
size_t size() const
void setNumberOutputBits(unsigned int numberOutputBits)
Definition DecisionAlg.h:40
DecisionAlg(const std::string &name)
Definition DecisionAlg.h:25
bool fillHistosBasedOnHardware() const
! getter
bool fillHistos() const
whether the monitoring histograms should be filled
std::vector< std::string > m_histAccept
Definition DecisionAlg.h:73
std::vector< std::string > m_histReject
Definition DecisionAlg.h:74
unsigned int numberOutputBits() const
Definition DecisionAlg.h:39
bool getDecisionHardwareBit(const unsigned int &bitNumber) const
! get one hardware decision bit from this algo
void setBit(unsigned int index, bool value)
Definition Decision.cxx:12
Tree(const nlohmann::json &obj, int nVars)
std::vector< Bin > bins
int64_t getTreeScore(const std::vector< int64_t > &inputEvent) const
std::ostream & operator<<(std::ostream &os, const TCS::Bin &bin)
uint32_t parType_t
Definition Parameter.h:22
bool isAmbiguousAnywhere(TCS::TOBArray const *tobs, size_t pos, unsigned minEt=0)
TChain * tree