ATLAS Offline Software
Loading...
Searching...
No Matches
TableConstructorBase.py
Go to the documentation of this file.
1#!/usr/bin/env python
2#
3# Copyright (C) 2002-2021 CERN for the benefit of the ATLAS collaboration
4#
5
6'''
7@file TableConstructorBase.py
8@brief Defines TableConstructor base class to read ROOT histograms and convert it to csv table
9'''
10
11from math import fabs
12from collections import OrderedDict
13from TrigCostAnalysis.CostMetadataUtil import ignoreUnderflow
14from AthenaCommon.Logging import logging
15log = logging.getLogger('TableConstructor')
16
17
18class Column:
19 ''' @brief Class representing a single csv column
20
21 Used in TableConstructor to store header description and values of csv column
22 '''
23
24 def __init__(self, header = "Unknown", headerDesc = "Unknown", needNormalizing = False):
25 '''Column header'''
26 self.header = header
27
28 '''Column description'''
29 self.headerDesc = headerDesc
30
31 '''The list storing column content'''
32 self.content = []
33
34 '''Flag if column needs normalization'''
35 self.needNormalizing = needNormalizing
36
37
38 def addValue(self, value, index=-1):
39 ''' @brief Add value to the column '''
40 if index < 0 or index >= len(self.content):
41 self.content.append(value)
42 else:
43 self.content[index] += value
44
45 def updateValue(self, value, index=-1):
46 ''' @brief Add value to the column '''
47 if index < 0 or index >= len(self.content):
48 self.content.append(value)
49 else:
50 self.content[index] = value
51
52 def getValue(self, index):
53 ''' @brief Get value from column with given index'''
54 if index >= len(self.content):
55 log.error("Index out of range")
56 return None
57 else:
58 return self.content[index]
59
60
61class TableConstructorBase:
62 ''' @brief Class representing a single table
63
64 Base class trepresenting table. It defines basic behavior common for
65 all the tables like caching histograms or saving table to file.
66 '''
67
68 def __init__(self, tableObj, underflowThreshold, overflowThreshold):
69 '''ROOT table directory object, storing subdirs with histograms'''
70 self.tableObj = tableObj
71
72 '''Dictionary storing the columns. Each column is identified by given name (key)'''
73 self.columns = OrderedDict()
74
75 '''Map to cache expected histograms'''
76 self.histograms = {}
77
78 '''List with expected histograms'''
79 self.expectedHistograms = []
80
81 '''Number of underflow bins after which warning will be saved to metadata tree'''
82 self.underflowThreshold = underflowThreshold
83
84 '''Number of overflow bins after which warning will be saved to metadata tree'''
85 self.overflowThreshold = overflowThreshold
86
87 '''Overflow log'''
88 self.warningMsg = []
89
90
91 def getWarningMsgs(self):
92 ''' @brief Raturn warning messages concerning histogram under/over flows '''
93 return self.warningMsg
94
95
96 def getHistogram(self, name):
97 ''' @brief Return cached histogram with given name'''
98 if name not in self.histograms:
99 log.error("Histogram %s not found!", name)
100 return None
101 return self.histograms[name]
102
103
104 def cacheHistograms(self, dirName, prefix):
105 ''' @brief Cache histograms in map for given directory
106
107 Save histograms in the map by their short name as a key. If the histogram
108 with a given name is not found function logs an error.
109
110 @param[in] dirName Name of subdirectory to look for histograms
111 @param[in] prefix Prefix of the histogram name
112
113 '''
114 dirKey = [key for key in self.tableObj.GetListOfKeys() if key.GetName() == dirName]
115 if not dirKey:
116 log.error("Subdirectory %s not found", dirName)
117
118 dirObj = dirKey[0].ReadObj()
119 for histName in self.expectedHistograms:
120 fullHistName = prefix + dirName + '_' + histName
121 hist = dirObj.Get(fullHistName)
122
123 if not hist:
124 log.debug("Full name %s", fullHistName)
125 log.debug("Directory: %s", dirObj.ls())
126 log.error("Expected histogram %s not found", histName)
127 else:
128 # Check for under and overflows
129 # Under/overflow bin stores weighted value - we cannot base on entries
130 histIntegral = hist.Integral(0, hist.GetNbinsX() + 1) # Integral including overflows
131 overflowThreshold = self.overflowThreshold * histIntegral
132 overflow = hist.GetBinContent(hist.GetNbinsX() + 1)
133 if overflow > overflowThreshold:
134 log.warning("Histogram {0} contains overflow of {1}".format(fullHistName, overflow))
135 self.warningMsg.append("Overflow of {0} ({1} histogram integral) in {2}".format(overflow, histIntegral, fullHistName))
136
137 underflowThreshold = self.underflowThreshold * histIntegral
138 underflow = hist.GetBinContent(0)
139 if underflow > underflowThreshold and not ignoreUnderflow(fullHistName):
140 log.warning("Histogram {0} contains underflow of {1}".format(fullHistName, underflow))
141 self.warningMsg.append("Underflow of {0} ({1} histogram integral) in {2}".format(underflow, histIntegral, fullHistName))
142
143 self.histograms[histName] = hist
144
145
146 def getXWeightedIntegral(self, histName, isLog = True):
147 ''' @brief Get "total" value by integrating over a histogram, weighting every entry by its x-axis mean.
148
149 @param[in] histName Histogram name
150 @param[in] isLog If histogram is log x-axis, modifies how x-axis mean is computed for each bin.
151
152 @return Total value of the histogram.
153 '''
154
155 hist = self.getHistogram(histName)
156 if not hist:
157 return 0
158
159 total = 0
160 for i in range(1, hist.GetXaxis().GetNbins()+1):
161 if isLog:
162 total += hist.GetBinContent(i) * hist.GetXaxis().GetBinCenterLog(i)
163 else:
164 total += hist.GetBinContent(i) * hist.GetXaxis().GetBinCenter(i)
165
166 return total
167
168
169 def saveToFile(self, fileName):
170 ''' @brief Function to save table content to csv file with specified fileName
171
172 @param[in] fileName Name of the file to save the table
173 '''
174
175 from TrigCostAnalysis.Util import convert
176 import csv
177
178 with open(fileName, mode='w') as csvFile:
179 csvWriter = csv.writer(csvFile, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)
180
181 header = []
182 desc = []
183 rows = []
184 for column in self.columns.values():
185 header.append(column.header)
186 desc.append(column.headerDesc)
187 content = [ convert(entry) for entry in column.content ]
188 rows.append(content)
189
190 csvWriter.writerow(header)
191 csvWriter.writerow(desc)
192
193 # Transpose columns to rows
194 rows = list(zip(*rows))
195 for row in rows:
196 csvWriter.writerow(row)
197
198
199 def normalizeColumns(self, denominator):
200 ''' @brief Perform normalization on marked columns
201
202 @param[in] denominator Value to normalize choosen columns
203 '''
204 for columnName, column in self.columns.items():
205 if column.needNormalizing:
206 if fabs(denominator) < 1e-10:
207 log.error("Normalise denominator is zero. Setting %s to zero.", columnName)
208 column.content = [0] * len(column.content)
209 else:
210 column.content = [entry/denominator for entry in column.content]
211
212
213 def fillTable(self, prefix=''):
214 ''' @brief Fill the table based on ROOT directory's content.'''
215
216 self.defineColumns()
217
218 # Fill the columns
219 for hist in self.tableObj.GetListOfKeys():
220 self.cacheHistograms(hist.GetName(), prefix)
221 self.fillColumns(hist.GetName())
222
223 self.postProcessing()
224
225
226 def defineColumns(self):
227 ''' @brief Define the columns for the table
228
229 Columns should be objects of class Column, added to the map self.columns.
230 '''
231 log.error("Should not call defineColumns on base class!")
232
233
234 def fillColumns(self, histName=''):
235 ''' @brief Fill the columns with values '''
236 log.error("Should not call fillColumns on base class!")
237
238
239 def postProcessing(self):
240 ''' @brief Additional operations
241
242 Normalization in performed separatly!
243 '''
244 pass
__init__(self, header="Unknown", headerDesc="Unknown", needNormalizing=False)