ATLAS Offline Software
Loading...
Searching...
No Matches
systematicsTool Namespace Reference

Functions

 renameFilesWithoutPrefix (directory)
 customReplacements (name, removeExtension=True, customReps=None)
 safeFileName (name, removeExtension=True, reverse=False)
 rivetINameReplacements (name, escapePlus=False)
 lookupPDFSetName (lhapdfid)
 getSumOfWeights (path, nominalWeight="", sumwHistName="")
 weightCorrection (var, nom, sampleDir="", varWeightName="", nominalWeightName="", sumwHistName="")
 getXSFromYODA (path)
 getXS (dsid, campaign=15, userFile=None)
 safeDiv (numerator, denominator)
 splitOperand (operand, bracket="()")
 getFormulaComponents (formula)
 resolveFormula (nominal, formula, componentsMap, level=0, verbose=0)
 combineVariationsLHAPDF (nom, variations, pset, asym=False)
 combineVariationsEnvelope (nom, variations, asym=True, includeErrorsInEnvelope=False)
 combineVariationsReplicas (nom, variations, asym=True)
 combineVariationsAlphaS (nom, variations)
 combineVariationsHessian (nom, variations, asym=True)
 combineVariationsFromFormula (nom, variations, formula)
 combineVariationsStat (nom)
 combineVariationsNorm (nom, val=0.05)
 readFromFile (filename, regexFilter=None, regexVeto=None)
 getFileKeys (d, basepath="/")
 readFromROOT (filename, regexFilter=None, regexVeto=None)
 readFromYODA (filename, regexFilter=None, regexVeto=None)
 writeToFile (histDict, fOut)
 writeToYODA (histDict, fOut)
 writeToROOT (histDict, fOut)
 getAverageUncertaintySizePerBin (fIn, regexFilter=None, regexVeto=None)
 combineVariation (wName, wInfo, fOut, regexFilter=None, regexVeto=None)
 arrayDictToTGraph (ao, isData=False, setYErrorsToZero=False, nominalAOForRatio=None)
 getPlotInfo (aoName, pathInRivetEnv)
 safeRootLatex (unsafeLatex)
 makeDummyHisto (tg, isLog=False, XandYMinAndMax=None, ratioZoom=None, isRatio=False)
 makeSystematicsPlotsWithROOT (mergedSystDict, outdir, nominalName="Nominal", ratioZoom=None, regexFilter=None, regexVeto=None, label="", plotInfo=None)
 makeSystematicsPlotsWithRIVET (mergedSystDict, plotsDir, nominalName="Nominal", ratioZoom=None, regexFilter=None, regexVeto=None, label="", plotInfo=None, normalize=False)
 getCombinationRecipe (systWeights, combinationRecipeFile=None, combinationRecipeName=None)
 combineAllVariations (weightList, indir, outdir, regexFilter=None, regexVeto=None, combinationRecipe=None, returnOnlyVariationsInComination=True, schema="!INDIR/!INFILE.yoda:!AONAME[!WEIGHTNAME]", orderedWeights=None, inFile=None, customWeightNameReplacements=None)
 extractTarballsFromDirectory (fulldir, force=False, verbose=False, rootOrYoda='yoda', notTGZ=False)
 updateProgress (progress, key, message)
 printProgress (progress)
 mergeInChunks (outfn, paths, progressDict=None, nFilesPerChunk=100, force=False, rootOrYoda='yoda')
 getCrossSectionCorrection (xsList, systFiles, nomFiles, rivetNormalised=False, applyKFactorCorrection=False, varWeightName="", nominalWeightName="", sumwHistName="")
 main (argv)

Detailed Description

This module contains helper functions which allow users to manipulate multiple
YODA files corresponding to different subsamples of a given process/generator,
different sources of theory uncertainty, etc. and combine them, plotting the
theory error bands along the way.

Can be used either as a standalone executable or the functions can be imported
into a custom python script.

This module uses a loosely-defined datatype which shall be referred to as
AnalysisObject, which is effectively a dict of np.arrays and strings.
This encodes the information from a yoda.Scatter2D(), yoda.Histo1D(),
root.TGraphAsymmErrors() or root.TH1D() in the same format, and which
can be easily manipulated since it consists of np.arrays or strings
An AO has the following keys:

`ao['name']`  the name of the object a la YODA
`ao['path']`  the path of the object a la YODA
`ao['annotations']` a dict of annotations a la YODA
`ao['bw']`    np.array of widths of each bin
`ao['x']`     np.array of mid-points of bins
`ao['xup']`   np.array of high bin edges
`ao['xdn']`   np.array of low bin edges
`ao['y']`     np.array of y values
`ao['yup']`   np.array of y + err_up values
`ao['ydn']`   np.array of y-err_dn values
`ao['nbins']` number of bins

This should probably be replaced by a Class in the next version !

For a full list of functions and their usage, type:
pydoc systematicsTool

TODO (perspectives for future development):
- Also handle ROOT as input/output instead of YODA
- Replace AnalysisObject by an actual Class rather than loosely-defined dicts
- Implement combineVariationsReplicas()
- Support other types in readFromYODA

Author: Louie D. Corpe (CERN)
Email: l.corpe@cern.ch

Function Documentation

◆ arrayDictToTGraph()

systematicsTool.arrayDictToTGraph ( ao,
isData = False,
setYErrorsToZero = False,
nominalAOForRatio = None )
`ao` AnalysisObject (the one to convert to a TGraph)
`isData` Bool [optional] (modify style of TGraph for data points)
`setYErrorsToZero` Bool [optional](Might want to do this if you don't care about the
error bands for some uncertainty component)
`nominalAOForRatio` [optional]AnalysisObject or None (if not None, make a ratio plot
with respect to this AnalysisObject)
`return` ROOT.TGraphAsymmErrors()

Fill a TGraphAsymmErrors from an AO, for plotting purposes.
Can format as MC or Data. If nominalAOForRatio is specified (ie not None),
the TGraphAsymmErrors is divided by the 'y' value of the nominalAOForRatio
in each bin.

Definition at line 1276 of file systematicsTool.py.

1276def arrayDictToTGraph(ao, isData=False, setYErrorsToZero=False, nominalAOForRatio=None):
1277 """
1278 `ao` AnalysisObject (the one to convert to a TGraph)
1279 `isData` Bool [optional] (modify style of TGraph for data points)
1280 `setYErrorsToZero` Bool [optional](Might want to do this if you don't care about the
1281 error bands for some uncertainty component)
1282 `nominalAOForRatio` [optional]AnalysisObject or None (if not None, make a ratio plot
1283 with respect to this AnalysisObject)
1284 `return` ROOT.TGraphAsymmErrors()
1285
1286 Fill a TGraphAsymmErrors from an AO, for plotting purposes.
1287 Can format as MC or Data. If nominalAOForRatio is specified (ie not None),
1288 the TGraphAsymmErrors is divided by the 'y' value of the nominalAOForRatio
1289 in each bin.
1290 """
1291 tg = r.TGraphAsymmErrors()
1292 for i in range(len(ao['y'])):
1293 scaleBy = 1.
1294 if (nominalAOForRatio is not None):
1295 if not (nominalAOForRatio['y'][i] == 0. or np.isnan(nominalAOForRatio['y'][i])): scaleBy = 1. / nominalAOForRatio['y'][i]
1296 yVal = ao['y'][i]
1297 if(np.isnan(yVal)):
1298 yVal = 0
1299 tg.SetPoint(i, ao['x'][i], yVal * scaleBy)
1300 yErrUp = 0. if (setYErrorsToZero or np.isnan(ao['yup'][i] - ao['y'][i])) else (ao['yup'][i] - ao['y'][i]) * scaleBy
1301 yErrDn = 0. if (setYErrorsToZero or np.isnan(ao['y'][i] - ao['ydn'][i])) else (ao['y'][i] - ao['ydn'][i]) * scaleBy
1302 tg.SetPointError(i, ao['x'][i] - ao['xdn'][i],
1303 ao['xup'][i] - ao['x'][i],
1304 yErrDn, yErrUp,
1305 )
1306 if not isData:
1307 tg.SetLineWidth(2)
1308 else:
1309 tg.SetMarkerSize(1)
1310 tg.SetMarkerStyle(20)
1311 tg.SetLineWidth(3)
1312
1313 return tg
1314
1315
if(febId1==febId2)

◆ combineAllVariations()

systematicsTool.combineAllVariations ( weightList,
indir,
outdir,
regexFilter = None,
regexVeto = None,
combinationRecipe = None,
returnOnlyVariationsInComination = True,
schema = "!INDIR/!INFILE.yoda:!AONAME[!WEIGHTNAME]",
orderedWeights = None,
inFile = None,
customWeightNameReplacements = None )
`weightList` dict in output format of the readDatabase.getWeight() tool.
See https://twiki.cern.ch/twiki/bin/viewauth/AtlasProtected/PmgSystTool
for more info.
`indir` String (input dir, where to find samples for each weight (merged
across jobs/DSIDs if needed)
`outdir` String (output dir, where to write the output files where the
individual weights are combined into systematic variations)
`regexFilter` String [optional] (AOs whose names match regex are processed)
`regexVeto` String [optional](AOs whose names match regex are NOT processed)
`combinationRecipe` String [optional] (if you want to use a specific
combination recipe, specify it here. If None, then the code will try to
auto-determine the correct recipe from the $SYSTTOOLSPATH/data/Syst_Database.yaml.
supports String:String where the first part is the yaml file containing the recipe)
`returnOnlyVariationsInComination` Bool [optional] (by default the code will return
only a list of files for variations which take part in the combination. Set to False
to return all possible variations)
`schema` String [optional] (tells the code how the naming convention for histograms is
structured within the input file.)
`schema` List[weight names] [optional] (if the `!WEIGHTINDEX!` keyword is used in `schema`
this option is needed so the code can convert from weight name to index)
`inFile` String [optional] (needed if the `!INFILE!` keyword is used in `schema`)
`customWeightNameReplacements` OrderedDict  of custom replacements to sequentially apply
to weight names, or txt file with one replacement per line, separated by "-->"
`return` dict{String, String} (format is as follows {N!label:filename},
where N is the order in which to plot the variations, labels is what to put on
the legend, and filename is the name of the YODA/ROOT file to use as input)

Using the information provided by the readDatabase tool (see
https://twiki.cern.ch/twiki/bin/viewauth/AtlasProtected/PmgSystTool),
combine individual YODA/ROOT files for each Matrix Element weights into
individual YODA/ROOT files for each systematic uncertainty.

Definition at line 1966 of file systematicsTool.py.

1966def combineAllVariations(weightList, indir, outdir, regexFilter=None, regexVeto=None, combinationRecipe=None, returnOnlyVariationsInComination=True, schema="!INDIR/!INFILE.yoda:!AONAME[!WEIGHTNAME]", orderedWeights=None, inFile=None, customWeightNameReplacements=None):
1967 """
1968 `weightList` dict in output format of the readDatabase.getWeight() tool.
1969 See https://twiki.cern.ch/twiki/bin/viewauth/AtlasProtected/PmgSystTool
1970 for more info.
1971 `indir` String (input dir, where to find samples for each weight (merged
1972 across jobs/DSIDs if needed)
1973 `outdir` String (output dir, where to write the output files where the
1974 individual weights are combined into systematic variations)
1975 `regexFilter` String [optional] (AOs whose names match regex are processed)
1976 `regexVeto` String [optional](AOs whose names match regex are NOT processed)
1977 `combinationRecipe` String [optional] (if you want to use a specific
1978 combination recipe, specify it here. If None, then the code will try to
1979 auto-determine the correct recipe from the $SYSTTOOLSPATH/data/Syst_Database.yaml.
1980 supports String:String where the first part is the yaml file containing the recipe)
1981 `returnOnlyVariationsInComination` Bool [optional] (by default the code will return
1982 only a list of files for variations which take part in the combination. Set to False
1983 to return all possible variations)
1984 `schema` String [optional] (tells the code how the naming convention for histograms is
1985 structured within the input file.)
1986 `schema` List[weight names] [optional] (if the `!WEIGHTINDEX!` keyword is used in `schema`
1987 this option is needed so the code can convert from weight name to index)
1988 `inFile` String [optional] (needed if the `!INFILE!` keyword is used in `schema`)
1989 `customWeightNameReplacements` OrderedDict of custom replacements to sequentially apply
1990 to weight names, or txt file with one replacement per line, separated by "-->"
1991 `return` dict{String, String} (format is as follows {N!label:filename},
1992 where N is the order in which to plot the variations, labels is what to put on
1993 the legend, and filename is the name of the YODA/ROOT file to use as input)
1994
1995 Using the information provided by the readDatabase tool (see
1996 https://twiki.cern.ch/twiki/bin/viewauth/AtlasProtected/PmgSystTool),
1997 combine individual YODA/ROOT files for each Matrix Element weights into
1998 individual YODA/ROOT files for each systematic uncertainty.
1999 """
2000 os.system("mkdir -p %s" % outdir)
2001
2002 # various holders for weights, labels, filenames
2003 weightsToProcess = []
2004 allWeights = []
2005 labels = {}
2006 fOut = {}
2007 averageErrorSize = {}
2008 weightList = copy.deepcopy(weightList)
2009
2010 # find the overall nominal file, and use the weight names to figure out the
2011 # corresponding yoda filenames, and fill holders
2012 NominalFile = ""
2013 for k, v in weightList.items():
2014 weights = []
2015 isRoot = False if 'root' not in schema else 'True'
2016 for oldWeight in v['weights']:
2017 # replace weight names by the path of the YODA files
2018 fileName = schema.replace("!NSFWEIGHTNAME", oldWeight)
2019 fileName = fileName.replace("!WEIGHTNAME", safeFileName(oldWeight, removeExtension=True))
2020 fileName = fileName.replace("!CRWEIGHTNAME", customReplacements(oldWeight, removeExtension=True, customReps=customWeightNameReplacements))
2021 fileName = fileName.replace("!INDIR", indir)
2022 if orderedWeights is not None: fileName = fileName.replace("!WEIGHTINDEX", "%d" % orderedWeights.index(oldWeight))
2023 if inFile is not None:
2024 fileName = fileName.replace("!INFILE", inFile)
2025 fileName = fileName.replace(".root.root", ".root")
2026 fileName = fileName.replace(".yoda.yoda", ".yoda")
2027 if(os.path.isfile(fileName.split(":")[0])):
2028 weights += [fileName]
2029 fOut[oldWeight] = fileName
2030 else:
2031 print("[ERROR] couldn't find file of name " + fileName + " with either .root or .yoda extension!")
2032 exit(1)
2033 v['weights'] = weights
2034 if v['type'] == 'Nominal':
2035 NominalFile = weights[0]
2036 v['nominal'] = NominalFile
2037 weightsToProcess += [k]
2038 elif v['combination'] == 'none': pass
2039 else:
2040 fileName = schema.replace("!NSFWEIGHTNAME", v['nominal'])
2041 fileName = fileName.replace("!WEIGHTNAME", safeFileName(v['nominal'], removeExtension=True))
2042 fileName = fileName.replace("!CRWEIGHTNAME", customReplacements(oldWeight, removeExtension=True, customReps=customWeightNameReplacements))
2043 fileName = fileName.replace("!INDIR", indir)
2044 if orderedWeights is not None: fileName = fileName.replace("!WEIGHTINDEX", "%d" % orderedWeights.index(v['nominal']))
2045 if inFile is not None:
2046 fileName = fileName.replace("!INFILE", inFile)
2047 fileName = fileName.replace(".root.root", ".root")
2048 fileName = fileName.replace(".yoda.yoda", ".yoda")
2049 if(os.path.isfile(fileName.split(":")[0])):
2050 v['nominal'] = fileName
2051 else:
2052 print("[ERROR] couldn't find file of name " + fileName + " with either .root or .yoda extension!")
2053 exit(1)
2054 weightsToProcess += [k]
2055 allWeights += weights
2056
2057 # get the Total uncertainty combination recipe and formula
2058 combinationRecipeFile, combinationRecipeName = None, combinationRecipe
2059 if combinationRecipe:
2060 if ":" in combinationRecipe:
2061 combinationRecipeFile, combinationRecipeName = combinationRecipeName.split(":")
2062 recipe, recipeDetails = getCombinationRecipe(weightList.keys(), combinationRecipeFile, combinationRecipeName)
2063 if recipe is not None: formula = recipeDetails['combination']
2064
2065 # for each systematic, make a new output file, process the
2066 # variation and check how large it's effect is over all AOs
2067 # so that they can be sorted by size for plotting
2068 for syst in weightsToProcess:
2069 if isRoot: fOut[syst] = '%s/%s.root' % (outdir, syst)
2070 else: fOut[syst] = '%s/%s.yoda' % (outdir, syst)
2071 labels[syst] = weightList[syst]['type']
2072 # this is the function that does the hard work!
2073 print("Doing %s with %d weights: %s" % (syst, len(weightList[syst]['weights']), repr(weightList[syst])))
2074 combineVariation(syst, weightList[syst], fOut[syst], regexFilter, regexVeto)
2075 averageErrorSize[syst] = getAverageUncertaintySizePerBin(fOut[syst], regexFilter, regexVeto)
2076 os.system("ls %s" % fOut[syst])
2077
2078 # in Syst_Database, one may define a number of 'user-defined' intermediary systematics
2079 # which need to be constructed according to some formula before the total uncertainty
2080 # is evaluated. So let's do those first
2081 if recipe is not None:
2082 if 'user_defined_components' in recipeDetails.keys():
2083 for syst, details in recipeDetails['user_defined_components'].items():
2084 if isRoot: fOut[syst] = '%s/%s.root' % (outdir, syst)
2085 else: fOut[syst] = '%s/%s.yoda' % (outdir, syst)
2086 labels[syst] = details['type']
2087
2088 # build a dummy 'weightList' object
2089 combinationInfo = {}
2090 combinationInfo['type'] = details['type']
2091 combinationInfo['nominal'] = NominalFile
2092 combinationInfo['weights'] = [fOut[i] for i in getFormulaComponents(details['combination'])]
2093 combinationInfo['combination'] = "customFunction"
2094 combinationInfo['function'] = details['combination']
2095
2096 # build the systematic according to the custom Formula and then check it's size
2097 combineVariation(syst, combinationInfo, fOut[syst], regexFilter, regexVeto)
2098 averageErrorSize[syst] = getAverageUncertaintySizePerBin(fOut[syst], regexFilter, regexVeto)
2099 os.system("ls %s" % fOut[syst])
2100
2101 # now we construct the overall uncertainty according to the recipe
2102 print("[INFO] combining uncertainties using the following recipe:" + recipe)
2103 print("[INFO] which uses formula: " + formula)
2104
2105 # build a dummy 'weightList' object
2106 combinationInfo = {}
2107 combinationInfo['type'] = 'All'
2108 combinationInfo['nominal'] = NominalFile
2109 combinationInfo['weights'] = fOut.values()
2110 combinationInfo['combination'] = "customFunction"
2111 combinationInfo['function'] = formula
2112
2113 # build the total uncertainty according to the custom Formula and then check it's size
2114 fOut['all'] = '%s/all.yoda' % outdir
2115 if isRoot: fOut['all'] = '%s/all.root' % outdir
2116 else: fOut['all'] = '%s/all.yoda' % outdir
2117 combineVariation('All', combinationInfo, fOut['all'], regexFilter, regexVeto)
2118 averageErrorSize['all'] = getAverageUncertaintySizePerBin(fOut['all'], regexFilter, regexVeto) * 10
2119 os.system("ls %s" % fOut[syst])
2120
2121 # now we want to order the output files by size of the corresponding
2122 # uncertainties, so they look nice when plotted.
2123 result = OrderedDict()
2124 counter = 0
2125 formulaComponents = averageErrorSize.keys()
2126 if recipe is not None: formulaComponents = getFormulaComponents(formula)
2127 for k, v in sorted(averageErrorSize.items(), key=lambda x: x[1], reverse=True):
2128 if k == 'all':
2129 result[fOut[k]] = "[Total Uncertainty]"
2130 else:
2131 isComponent = False
2132 for component in formulaComponents:
2133 if component == k: isComponent = True
2134 if (not isComponent) and returnOnlyVariationsInComination: continue
2135 result[fOut[k]] = '[%s]' % labels[k]
2136 counter += 1
2137 # finally, return the odered files
2138 return result
2139
2140
void print(char *figname, TCanvas *c1)

◆ combineVariation()

systematicsTool.combineVariation ( wName,
wInfo,
fOut,
regexFilter = None,
regexVeto = None )
`wName` String (Name of the systemtic weight to combine)
`wInfo` dict in output format of the readDatabase.getWeight() tool.
See https://twiki.cern.ch/twiki/bin/viewauth/AtlasProtected/PmgSystTool
for more info.
`regexFilter` String [optional] (AOs whose names match regex are processed)
`regexVeto` String [optional] (AOs whose names match regex are NOT processed)
`return` None

Produce and write a YODA file for the systematic uncertainty wName by
combining the weights listed in wInfo['weights'], according to the procedure
specified in wInfo['combination'] for each AO.

The following values of wInfo['combination'] are suppored: stat, lhapdf,
replicas, alphas, envelope, hessian, and customFunction (in this case, the
formula given in wInfo['function'] is evaluated).

Definition at line 1179 of file systematicsTool.py.

1179def combineVariation(wName, wInfo, fOut, regexFilter=None, regexVeto=None):
1180 """
1181 `wName` String (Name of the systemtic weight to combine)
1182 `wInfo` dict in output format of the readDatabase.getWeight() tool.
1183 See https://twiki.cern.ch/twiki/bin/viewauth/AtlasProtected/PmgSystTool
1184 for more info.
1185 `regexFilter` String [optional] (AOs whose names match regex are processed)
1186 `regexVeto` String [optional] (AOs whose names match regex are NOT processed)
1187 `return` None
1188
1189 Produce and write a YODA file for the systematic uncertainty wName by
1190 combining the weights listed in wInfo['weights'], according to the procedure
1191 specified in wInfo['combination'] for each AO.
1192
1193 The following values of wInfo['combination'] are suppored: stat, lhapdf,
1194 replicas, alphas, envelope, hessian, and customFunction (in this case, the
1195 formula given in wInfo['function'] is evaluated).
1196 """
1197 # if this is a PDF set, set up LHAPDF
1198 if wInfo['combination'] == 'lhapdf':
1199 if ('nominal_pdf' not in wInfo.keys()) or (len(wInfo['weights']) == 1):
1200 # sometimes a variation claims to be a PDFSet, but if there is only
1201 # one weight, it should just be taken as its own nominal.
1202 wInfo['combination'] = 'stat'
1203 else:
1204 nominalPDF = int(wInfo['nominal_pdf'])
1205 pdfSetName = lookupPDFSetName(nominalPDF)
1206 pset = lhapdf.getPDFSet(pdfSetName)
1207 pset.mkPDFs()
1208
1209 # Get the nominal and variated AOs for each weight
1210 nominalHists = readFromFile(wInfo['nominal'], regexFilter, regexVeto)
1211 variationHists_Var_AO = {}
1212 variationHists = {}
1213 for var in wInfo['weights']:
1214 if "Weight" in var: continue
1215 if ":" in var and 'yoda' in var:
1216 variationHists_Var_AO[var] = readFromFile(var, regexFilter, regexVeto)
1217 else:
1218 variationHists_Var_AO[var] = readFromFile(var, regexFilter, regexVeto)
1219 # probably a more elegant way to do this....
1220 for var, aoDict in variationHists_Var_AO.items():
1221 for ao, info in aoDict.items():
1222 if ao not in variationHists.keys():
1223 variationHists[ao] = {}
1224 variationHists[ao][var] = info
1225
1226 # now loop though AOs and process them
1227 outputHists = copy.deepcopy(nominalHists)
1228 for ao in sorted(nominalHists.keys()):
1229 noms = nominalHists[ao]
1230 y, yup, ydn = 'y', 'yup', 'ydn'
1231 if 'z' in noms.keys():
1232 y, yup, ydn = 'z', 'zup', 'zdn'
1233 variations = variationHists[ao]
1234 systCentral = np.array(noms)
1235 systUp = np.array(noms)
1236 systDn = np.array(noms)
1237
1238 # calculate the value of the combined uncertainty according to the
1239 # specified method
1240 if wInfo['type'] == 'Nominal' or wInfo['combination'] == 'stat':
1241 systCentral, systDn, systUp = combineVariationsStat(noms)
1242 elif wInfo['combination'] == 'lhapdf':
1243 res = combineVariationsLHAPDF(noms, variations, pset, asym=False)
1244 if res is None: continue
1245 systCentral, systDn, systUp = res
1246 elif wInfo['combination'] == 'replicas':
1247 systCentral, systDn, systUp = combineVariationsReplicas(noms, variations, asym=True)
1248 elif wInfo['combination'] == 'envelope':
1249 systCentral, systDn, systUp = combineVariationsEnvelope(noms, variations, asym=True, includeErrorsInEnvelope=False)
1250 elif wInfo['combination'] == 'alphaS':
1251 systCentral, systDn, systUp = combineVariationsAlphaS(noms, variations)
1252 elif wInfo['combination'] == 'hessian':
1253 systCentral, systDn, systUp = combineVariationsHessian(noms, variations, asym=True)
1254 elif wInfo['combination'] == 'norm':
1255 systCentral, systDn, systUp = combineVariationsNorm(noms, val=wInfo['value'])
1256 elif wInfo['combination'] == 'customFunction':
1257 systCentral, systDn, systUp = combineVariationsFromFormula(noms, variations, wInfo['function'])
1258 else:
1259 print("[ERROR] combination type:" + wInfo['combination'] + "is not yet supported... skipping this systematic uncertainty:" + wName)
1260
1261 systUp = (systUp)
1262 systDn = (systDn)
1263 systCentral = (systCentral)
1264
1265 outputHists[ao][y] = systCentral
1266 outputHists[ao][yup] = systUp
1267 outputHists[ao][ydn] = systDn
1268
1269 # write the outputs!
1270 writeToFile(outputHists, fOut)
1271 print("[INFO] done combining this systematic: " + wName + " using method: " + repr(wInfo['combination']))
1272 if wInfo['combination'] == 'customFunction':
1273 print("[INFO] customFunction = " + repr(wInfo['function']))
1274
1275

◆ combineVariationsAlphaS()

systematicsTool.combineVariationsAlphaS ( nom,
variations )
`nom` AnalysisObject (of the nominal variation)
`variations` dict(String, AnalysisObject)
`return` AnalysisObject

Get the alphaS variation. This is just a special case of Envelope
where the error is symmetrized!

Definition at line 710 of file systematicsTool.py.

710def combineVariationsAlphaS(nom, variations):
711 """
712 `nom` AnalysisObject (of the nominal variation)
713 `variations` dict(String, AnalysisObject)
714 `return` AnalysisObject
715
716 Get the alphaS variation. This is just a special case of Envelope
717 where the error is symmetrized!
718 """
719 return combineVariationsEnvelope(nom, variations, asym=False, includeErrorsInEnvelope=False)
720
721

◆ combineVariationsEnvelope()

systematicsTool.combineVariationsEnvelope ( nom,
variations,
asym = True,
includeErrorsInEnvelope = False )
`nom` AnalysisObject (of the nominal variation)
`variations` dict(String, AnalysisObject)
`asym` Bool [optional] (return asymmetric errors? Or symmetrize?)
`includeErrorsInEnvelope` Bool [optional]
`return` AnalysisObject

Take the min/max envelope of the arguments as the up/down errors of the
resulting AO. The central value is taken from 'nominal'.

Definition at line 667 of file systematicsTool.py.

667def combineVariationsEnvelope(nom, variations, asym=True, includeErrorsInEnvelope=False):
668 """
669 `nom` AnalysisObject (of the nominal variation)
670 `variations` dict(String, AnalysisObject)
671 `asym` Bool [optional] (return asymmetric errors? Or symmetrize?)
672 `includeErrorsInEnvelope` Bool [optional]
673 `return` AnalysisObject
674
675 Take the min/max envelope of the arguments as the up/down errors of the
676 resulting AO. The central value is taken from 'nominal'.
677 """
678 y, yup, ydn = 'y', 'yup', 'ydn'
679 if 'z' in nom.keys():
680 y, yup, ydn = 'z', 'zup', 'zdn'
681 systup = np.array(nom[y])
682 systdn = np.array(nom[y])
683 for var in variations.values():
684 if includeErrorsInEnvelope:
685 systup = map(max, zip(systup, var[yup], var[ydn]))
686 systdn = map(min, zip(systdn, var[yup], var[ydn]))
687 else:
688 systup = map(max, zip(systup, var[y]))
689 systdn = map(min, zip(systdn, var[y]))
690 if not asym:
691 var_tmp = 0.5 * (abs(nom[y] - systup) + abs(nom[y] - systdn))
692 systup = nom[y] + var_tmp
693 systdn = nom[y] - var_tmp
694 return np.array(nom[y]), np.array(systdn), np.array(systup)
695
696
STL class.

◆ combineVariationsFromFormula()

systematicsTool.combineVariationsFromFormula ( nom,
variations,
formula )
`nom` AnalysisObject (of the nominal variation)
`variations` dict(String, AnalysisObject)
`formula` String (custom formula to combine uncertainties)
`return` AnalysisObject

Combine the specified variations according to custom formula given by formula

Definition at line 746 of file systematicsTool.py.

746def combineVariationsFromFormula(nom, variations, formula):
747 """
748 `nom` AnalysisObject (of the nominal variation)
749 `variations` dict(String, AnalysisObject)
750 `formula` String (custom formula to combine uncertainties)
751 `return` AnalysisObject
752
753 Combine the specified variations according to custom formula given by formula
754 """
755 y, yup, ydn = 'y', 'yup', 'ydn'
756 if 'z' in nom.keys():
757 y, yup, ydn = 'z', 'zup', 'zdn'
758 output = resolveFormula(nom, formula, variations)
759 return output[y], output[ydn], output[yup]
760
761

◆ combineVariationsHessian()

systematicsTool.combineVariationsHessian ( nom,
variations,
asym = True )
`nom` AnalysisObject (of the nominal variation)
`variations` dict(String, AnalysisObject)
`asym` Bool [optional] (return asymmetric errors? Or symmetrize?)
`return` AnalysisObject

Combine the specified variations according to the Hessian prescription
Central value given by nom.

Definition at line 722 of file systematicsTool.py.

722def combineVariationsHessian(nom, variations, asym=True):
723 """
724 `nom` AnalysisObject (of the nominal variation)
725 `variations` dict(String, AnalysisObject)
726 `asym` Bool [optional] (return asymmetric errors? Or symmetrize?)
727 `return` AnalysisObject
728
729 Combine the specified variations according to the Hessian prescription
730 Central value given by nom.
731 """
732 y = 'y'
733 if 'z' in nom.keys(): y = 'z'
734 nom_y = nom[y]
735 systup = np.array(nom_y)
736 systdn = np.array(nom_y)
737 sumsq = np.array(nom_y) - np.array(nom_y)
738 for var in variations:
739 sumsq += (nom_y - var) ** 2
740 systup += np.sqrt(sumsq)
741 systdn -= np.sqrt(sumsq)
742
743 return nom_y, systdn, systup
744
745

◆ combineVariationsLHAPDF()

systematicsTool.combineVariationsLHAPDF ( nom,
variations,
pset,
asym = False )
`nom` AnalysisObject (of the nominal variation)
`variations` dict(String, AnalysisObject)
`pset` LHAPDF PDFSet (obtained from lhapdf.getPDFSet(pdfSetName))
`asym` Bool [optional] (return asymmetric errors? Or symmetrize?)
`return` AnalysisObject

Combines PDF variations according to the LHAPDF PDFSet prescription.

Definition at line 627 of file systematicsTool.py.

627def combineVariationsLHAPDF(nom, variations, pset, asym=False):
628 """
629 `nom` AnalysisObject (of the nominal variation)
630 `variations` dict(String, AnalysisObject)
631 `pset` LHAPDF PDFSet (obtained from lhapdf.getPDFSet(pdfSetName))
632 `asym` Bool [optional] (return asymmetric errors? Or symmetrize?)
633 `return` AnalysisObject
634
635 Combines PDF variations according to the LHAPDF PDFSet prescription.
636 """
637 y = 'y'
638 if 'z' in nom.keys(): y = 'z'
639 nom_y = nom[y]
640 pdfsyst = np.array(nom_y)
641 nbins = len(nom_y)
642 variationsArray = []
643 for k in sorted(variations.keys()):
644 var = variations[k]
645 if np.allclose(nom_y, var[y]):
646 variationsArray = [var[y]] + variationsArray
647 else:
648 variationsArray.append(var[y])
649 if len(variationsArray) != pset.size:
650 print("[WARNING], there are not enough weights (%d) to evaluate this PDFSET (should have %d), skipping." % (len(variationsArray), pset.size))
651 return None
652 pdfvars = np.asarray(variationsArray)
653 pdfcen = np.array([pset.uncertainty(pdfvars[:, i]).central for i in range(nbins)])
654 pdfup = np.array([pset.uncertainty(pdfvars[:, i]).errplus for i in range(nbins)])
655 pdfdn = np.array([pset.uncertainty(pdfvars[:, i]).errminus for i in range(nbins)])
656 pdfsym = np.array([pset.uncertainty(pdfvars[:, i]).errsymm for i in range(nbins)])
657 pdfrel = safeDiv(pdfsym, pdfcen)
658 pdfrelup = safeDiv(pdfup, pdfcen)
659 pdfreldn = safeDiv(pdfdn, pdfcen)
660 pdfsyst = pdfrel * nom_y
661 pdfsystup = pdfrelup * nom_y
662 pdfsystdn = pdfreldn * nom_y
663 if asym: return (nom_y, nom_y - pdfsystdn, nom_y + pdfsystup)
664 else: return (nom_y, nom_y - pdfsyst, nom_y + pdfsyst)
665
666

◆ combineVariationsNorm()

systematicsTool.combineVariationsNorm ( nom,
val = 0.05 )
`nom` AnalysisObject (of the nominal variation)
`val` relative size of the normalisation uncertainty
`return` AnalysisObject

Apply a k-factor normalization uncertainty

Definition at line 775 of file systematicsTool.py.

775def combineVariationsNorm(nom, val=0.05):
776 """
777 `nom` AnalysisObject (of the nominal variation)
778 `val` relative size of the normalisation uncertainty
779 `return` AnalysisObject
780
781 Apply a k-factor normalization uncertainty
782 """
783 y = 'y'
784 if 'z' in nom.keys():
785 y = 'z'
786 return nom[y], nom[y] * (1 - val), nom[y] * (1 + val)
787
788

◆ combineVariationsReplicas()

systematicsTool.combineVariationsReplicas ( nom,
variations,
asym = True )
`nom` AnalysisObject (of the nominal variation)
`variations` dict(String, AnalysisObject)
`asym` Bool [optional] (return asymmetric errors? Or symmetrize?)
`return` AnalysisObject

Takes the standard deviation of the variations... in principle
This is a placeholder for now... needs to be implemented TODO

Definition at line 697 of file systematicsTool.py.

697def combineVariationsReplicas(nom, variations, asym=True):
698 """
699 `nom` AnalysisObject (of the nominal variation)
700 `variations` dict(String, AnalysisObject)
701 `asym` Bool [optional] (return asymmetric errors? Or symmetrize?)
702 `return` AnalysisObject
703
704 Takes the standard deviation of the variations... in principle
705 This is a placeholder for now... needs to be implemented TODO
706 """
707 return np.array(nom['y']), np.array(nom['y']), np.array(nom['y'])
708
709

◆ combineVariationsStat()

systematicsTool.combineVariationsStat ( nom)
`nom` AnalysisObject (of the nominal variation)
`return` AnalysisObject

Dummy function which currently just grabs the dn/up errs as the stat errs.

Definition at line 762 of file systematicsTool.py.

762def combineVariationsStat(nom):
763 """
764 `nom` AnalysisObject (of the nominal variation)
765 `return` AnalysisObject
766
767 Dummy function which currently just grabs the dn/up errs as the stat errs.
768 """
769 y, yup, ydn = 'y', 'yup', 'ydn'
770 if 'z' in nom.keys():
771 y, yup, ydn = 'z', 'zup', 'zdn'
772 return nom[y], nom[ydn], nom[yup]
773
774

◆ customReplacements()

systematicsTool.customReplacements ( name,
removeExtension = True,
customReps = None )
`name` String
`removeExtension` Bool [optional] (remove the file extension, eg .root, or .yoda)
`OrderedDict` of replacements to make, or txt file with one replacemement per line
separated by `-->`
`return` String

Definition at line 107 of file systematicsTool.py.

107def customReplacements(name, removeExtension=True, customReps=None):
108 """
109 `name` String
110 `removeExtension` Bool [optional] (remove the file extension, eg .root, or .yoda)
111 `OrderedDict` of replacements to make, or txt file with one replacemement per line
112 separated by `-->`
113 `return` String
114 """
115 if customReps is None: return name
116 mydict = OrderedDict()
117 if type(customReps) is str:
118 f = open(customReps, 'r')
119 for line in f.readlines():
120 line = line.replace("\n", "")
121 tokens = line.split("-->")
122 mydict[str(tokens[0])] = str(tokens[1])
123 elif type(customReps) is OrderedDict:
124 mydict = customReps
125 else:
126 print("[ERROR], customReps must be either OrderedDict or path to a txt file, not ", type(OrderedDict))
127 exit(1)
128 name = name.replace("user.", "userp")
129 name = name.replace(".yoda", "pyoda")
130 name = name.replace("yoda.", "yodap")
131 name = name.replace(".root", "proot")
132 for find, rep in mydict.items():
133 name = name.replace(find, rep)
134 name = name.replace("userp", "user.")
135 name = name.replace("pyoda", ".yoda")
136 name = name.replace("yodap", "yoda.")
137 name = name.replace("proot", ".root")
138 if (removeExtension and ".root" in name): name = name.rpartition(".root")[0]
139 if (removeExtension and ".yoda" in name): name = name.rpartition(".yoda")[0]
140 return name
141
142

◆ extractTarballsFromDirectory()

systematicsTool.extractTarballsFromDirectory ( fulldir,
force = False,
verbose = False,
rootOrYoda = 'yoda',
notTGZ = False )
`fulldir` String (directory containing tarballs to unpack)
`force` Bool [optional] (re-do untarring even if already done)
`verbose` Bool [optional] (Print more output if set to True)
`rootOrYoda` String [optional] (specify file type of tarred objects)

`return` list of [sample, newfn, filepath]

This function goes through a directory and unpacks all the tarballs it finds.
By default if a matching unpacked file has been found,
a tarball is not needlessly re-unpacked unless option `force` is used.

Definition at line 2141 of file systematicsTool.py.

2141def extractTarballsFromDirectory(fulldir, force=False, verbose=False, rootOrYoda='yoda', notTGZ=False):
2142 """
2143 `fulldir` String (directory containing tarballs to unpack)
2144 `force` Bool [optional] (re-do untarring even if already done)
2145 `verbose` Bool [optional] (Print more output if set to True)
2146 `rootOrYoda` String [optional] (specify file type of tarred objects)
2147
2148 `return` list of [sample, newfn, filepath]
2149
2150 This function goes through a directory and unpacks all the tarballs it finds.
2151 By default if a matching unpacked file has been found,
2152 a tarball is not needlessly re-unpacked unless option `force` is used.
2153 """
2154 res = []
2155 if fulldir[-1] == "/": fulldir = fulldir[:-1]
2156 sample = fulldir.split("/")[-1]
2157 counter = -1
2158 os.system("cd %s" % fulldir)
2159 print("\r[INFO] Processing " + sample)
2160 nSubSamples = len([name for name in os.listdir(fulldir)])
2161 sys.stdout.flush()
2162 if force:
2163 os.system("rm %s/merged/*" % fulldir)
2164 os.system("rm %s/merged_tmp/*" % fulldir)
2165 if len(os.listdir(fulldir)) == 0:
2166 print("[ERROR] can't run over empty directory: " + fulldir)
2167 exit(1)
2168 for subsample in os.listdir(fulldir):
2169 if 'root' in subsample:
2170 newfulldir = fulldir + "/" + subsample.replace(".root.tgz", ".untarred")
2171 else:
2172 newfulldir = fulldir + "/" + subsample.replace(".yoda.tgz", ".untarred").replace(".yoda.gz.tgz", ".untarred")
2173 if (not subsample.endswith('gz') and not notTGZ): continue
2174 if ("untarred" in subsample and not force): continue
2175 if ("merged" in subsample and not force): continue
2176 counter += 1
2177 verbose = 1
2178 if ('tgz' in subsample) and not (os.path.isdir(newfulldir) and not notTGZ):
2179 os.system("mkdir -p %s" % newfulldir)
2180 if verbose: sys.stdout.write('\r Unpacking job output %s (%d/%d) ' % (subsample, counter, nSubSamples))
2181 sys.stdout.flush()
2182 with tarfile.TarFile.open(fulldir + "/" + subsample, 'r:gz') as tar:
2183 fnames = tar.getnames()
2184 prefix = os.path.commonprefix(fnames)
2185 if len(fnames) == 1: prefix = ""
2186 if len(fnames) > 0:
2187 tar.extractall(newfulldir)
2188 for fn in fnames:
2189 newfn = fn.replace(prefix, "")
2190 if ".yoda.gz" not in fn:
2191 os.rename(newfulldir + "/" + fn, newfulldir + "/" + safeFileName(newfn) + "." + rootOrYoda)
2192 res.append([fulldir, newfn, newfulldir + "/" + safeFileName(newfn, removeExtension=False)])
2193 elif (not notTGZ):
2194 for fn in os.listdir(newfulldir):
2195 res.append([fulldir, fn, newfulldir + "/" + safeFileName(fn, removeExtension=False)])
2196 else:
2197 fn = os.path.basename(newfulldir)
2198 newfulldir = os.path.dirname(newfulldir)
2199 os.system("mkdir -p %s/%s" % (newfulldir, safeFileName(fn, removeExtension=True)))
2200 renamedfn = '_Nominal.yoda'
2201 os.system("cp %s/%s %s/%s/%s" % (newfulldir, fn, newfulldir, safeFileName(fn, removeExtension=True), renamedfn))
2202 res.append([fulldir, renamedfn, newfulldir + "/" + safeFileName(fn, removeExtension=True) + "/" + renamedfn])
2203 print('\r[INFO] Done untar-ing %s' % sample)
2204 return res
2205
2206
std::string replace(std::string s, const std::string &s2, const std::string &s3)
Definition hcg.cxx:310

◆ getAverageUncertaintySizePerBin()

systematicsTool.getAverageUncertaintySizePerBin ( fIn,
regexFilter = None,
regexVeto = None )
`fIn` String (input file to evaluate)
`regexFilter` String [optional] (AOs whose names match regex are processed)
`regexVeto` String [optional] (AOs whose names match regex are NOT processed)
`return` Float

Get a rough estimate of the average symmetric relative error per bin, used to
determine what order to plot the uncertainties in.

Definition at line 1144 of file systematicsTool.py.

1144def getAverageUncertaintySizePerBin(fIn, regexFilter=None, regexVeto=None):
1145 """
1146 `fIn` String (input file to evaluate)
1147 `regexFilter` String [optional] (AOs whose names match regex are processed)
1148 `regexVeto` String [optional] (AOs whose names match regex are NOT processed)
1149 `return` Float
1150
1151 Get a rough estimate of the average symmetric relative error per bin, used to
1152 determine what order to plot the uncertainties in.
1153 """
1154 averageUncertaintySizePerBin = None
1155 nominalHists = readFromFile(fIn)
1156 for ao in nominalHists.keys():
1157 # apply filters/vetos
1158 if (regexFilter is not None):
1159 if (ao not in re.findall(regexFilter, ao)): continue
1160 if (regexVeto is not None):
1161 if (ao in re.findall(regexVeto, ao)): continue
1162 noms = nominalHists[ao]
1163 y, yup, ydn = 'y', 'yup', 'ydn'
1164 if 'z' in noms.keys():
1165 y, yup, ydn = 'z', 'zup', 'zdn'
1166 # get symmetrix, relative error
1167 relativeError = safeDiv(0.5 * (abs(noms[yup] - noms[y]) + abs(noms[ydn] - noms[y])), abs(noms[y]))
1168 if (averageUncertaintySizePerBin is None): averageUncertaintySizePerBin = relativeError
1169 else:
1170 if np.average(averageUncertaintySizePerBin) != 0:
1171 # if it's much larger than the other AOs we've processed, something has probably
1172 # gone wrong and we should ignore it...
1173 if np.average(relativeError) > 100 * np.average(averageUncertaintySizePerBin): continue
1174 averageUncertaintySizePerBin = np.append(averageUncertaintySizePerBin, relativeError)
1175
1176 return np.average(averageUncertaintySizePerBin)
1177
1178

◆ getCombinationRecipe()

systematicsTool.getCombinationRecipe ( systWeights,
combinationRecipeFile = None,
combinationRecipeName = None )
`systWeights` list(String) (list of weight types which are available
for a given dsid, to identify the correct combination recipe)
`combinationRecipeName` String [optional] (specify if you want to use a
specific combination uncertainty, otherwise, it will try to auto-
determine frim the systWeights)
`combinationRecipeFile` String [optional] (specify if the target file is in
a different location to $SYSTTOOLSPATH/data/Syst_Database.yaml
`return` String, dict{components, formula}

Each sample type should have a unique set of weight types/names which allow us
to identify in the Syst_Database.yaml what the correct combination recipe is.

Definition at line 1925 of file systematicsTool.py.

1925def getCombinationRecipe(systWeights, combinationRecipeFile=None, combinationRecipeName=None,):
1926 """
1927 `systWeights` list(String) (list of weight types which are available
1928 for a given dsid, to identify the correct combination recipe)
1929 `combinationRecipeName` String [optional] (specify if you want to use a
1930 specific combination uncertainty, otherwise, it will try to auto-
1931 determine frim the systWeights)
1932 `combinationRecipeFile` String [optional] (specify if the target file is in
1933 a different location to $SYSTTOOLSPATH/data/Syst_Database.yaml
1934 `return` String, dict{components, formula}
1935
1936 Each sample type should have a unique set of weight types/names which allow us
1937 to identify in the Syst_Database.yaml what the correct combination recipe is.
1938 """
1939 if len(systWeights) == 1:
1940 # if there is only one weight, this is not a systematics sample anyway, so skip
1941 # this and return a dummy dictionary
1942 print("[WARNING] the provided systWeights only contain one component, assuming this is nominal and moving on")
1943 return {}, {'combination': systWeights[0]}
1944 else:
1945 dataBaseDir = os.environ["SYSTTOOLSPATH"]
1946 if combinationRecipeFile is not None:
1947 data = yaml.load(open(combinationRecipeFile, 'r'))
1948 else:
1949 data = yaml.load(open('%s/data/Syst_Database.yaml' % dataBaseDir, 'r'))
1950 for recipe, details in data.items():
1951 if combinationRecipeName is not None:
1952 if recipe == combinationRecipeName:
1953 if set(details['components']).issubset(set(systWeights)):
1954 return recipe, details
1955 else:
1956 print("[Warning] requested combination recipe" + combinationRecipeName + 'uses weights ' + repr(details['components']) + 'which are not all present in ' + systWeights)
1957 return recipe, details
1958
1959 if sorted(details['components']) == sorted(systWeights):
1960 return recipe, details
1961
1962 print("[WARNING] could not find systematics recipe for samples with the following ME Weights" + repr(systWeights))
1963 return None, None
1964
1965
STL class.

◆ getCrossSectionCorrection()

systematicsTool.getCrossSectionCorrection ( xsList,
systFiles,
nomFiles,
rivetNormalised = False,
applyKFactorCorrection = False,
varWeightName = "",
nominalWeightName = "",
sumwHistName = "" )
`xsList` list[Float] (list of nominal XS values for the subsamples)
`systFiles` list[String] (list of file names for the variated subsamples
(same order as xsList)
`nomFiles` list[String] (list of file names for the nominal subsamples
`rivetNormalised` Bool [optional] (whether or not to apply the total yield
correction for variation AOs, since by default rivet normalises by sum-of-weights
for that variation rather than sum-of-weight of the nominal)
`applyKFactorCorrection` Bool [optional] (If the sample to merge has a k-factor, there
is an additional correction needed to avoid double counting the normalisation uncertainty)
`varWeightName` String [optional] (name of the variation to get weight correction for)
`nominalWeightName` String [optional] (name of the nominal variation to get weight correction with respect to)
(same order as xsList)
`sumwHistName` String [optional] (name of the TH1 with sum-of-weights in the root file output scenario)

This function takes a list of nominal and variated YODA/ROOT files, and a list of
nominal cross-sections in the same order, to calculate the on-the-fly cross-
section corrections needed for each systFile. For yoda files produced in the Rivet 2.x
series, a correction is needed to undo the fact that Rivet normalises systematics
by the varied sum of weights instead of the nominal sum of weights.
If using a k-factor, an additional correction is needed to avoid double-counting
the normalisation uncertainty. For details, see
https://twiki.cern.ch/twiki/bin/view/AtlasProtected/PmgSystematicUncertaintyRecipes#On_the_fly_systematic_variations

Definition at line 2301 of file systematicsTool.py.

2301def getCrossSectionCorrection(xsList, systFiles, nomFiles, rivetNormalised=False, applyKFactorCorrection=False, varWeightName="", nominalWeightName="", sumwHistName=""):
2302 """
2303 `xsList` list[Float] (list of nominal XS values for the subsamples)
2304 `systFiles` list[String] (list of file names for the variated subsamples
2305 (same order as xsList)
2306 `nomFiles` list[String] (list of file names for the nominal subsamples
2307 `rivetNormalised` Bool [optional] (whether or not to apply the total yield
2308 correction for variation AOs, since by default rivet normalises by sum-of-weights
2309 for that variation rather than sum-of-weight of the nominal)
2310 `applyKFactorCorrection` Bool [optional] (If the sample to merge has a k-factor, there
2311 is an additional correction needed to avoid double counting the normalisation uncertainty)
2312 `varWeightName` String [optional] (name of the variation to get weight correction for)
2313 `nominalWeightName` String [optional] (name of the nominal variation to get weight correction with respect to)
2314 (same order as xsList)
2315 `sumwHistName` String [optional] (name of the TH1 with sum-of-weights in the root file output scenario)
2316
2317 This function takes a list of nominal and variated YODA/ROOT files, and a list of
2318 nominal cross-sections in the same order, to calculate the on-the-fly cross-
2319 section corrections needed for each systFile. For yoda files produced in the Rivet 2.x
2320 series, a correction is needed to undo the fact that Rivet normalises systematics
2321 by the varied sum of weights instead of the nominal sum of weights.
2322 If using a k-factor, an additional correction is needed to avoid double-counting
2323 the normalisation uncertainty. For details, see
2324 https://twiki.cern.ch/twiki/bin/view/AtlasProtected/PmgSystematicUncertaintyRecipes#On_the_fly_systematic_variations
2325 """
2326
2327 res = []
2328 inclusive_xs_nom = 0.
2329 inclusive_xs_syst = 0.
2330 assert len(xsList) == len(nomFiles)
2331 assert len(xsList) == len(systFiles)
2332
2333 for i in range(len(xsList)):
2334 nom = nomFiles[i]
2335 syst = systFiles[i]
2336 xs = xsList[i]
2337 inclusive_xs_nom += xs
2338 if rivetNormalised: inclusive_xs_syst += xs * weightCorrection(syst, nom, varWeightName=varWeightName, nominalWeightName=nominalWeightName, sumwHistName=sumwHistName)
2339 else: inclusive_xs_syst += xs * 1.
2340
2341 for i in range(len(xsList)):
2342 nom = nomFiles[i]
2343 syst = systFiles[i]
2344 xs = xsList[i]
2345 weight = xs
2346 if rivetNormalised: weight *= weightCorrection(syst, nom, varWeightName=varWeightName, nominalWeightName=nominalWeightName, sumwHistName=sumwHistName)
2347 if applyKFactorCorrection: weight *= (inclusive_xs_nom / inclusive_xs_syst)
2348 res.append([syst, weight])
2349
2350 return res
2351
2352

◆ getFileKeys()

systematicsTool.getFileKeys ( d,
basepath = "/" )
`d` TDirectory or TFile (name of file or directory to be read)
`basepath` String [optional] (absolute path leading to `d` in the file)
`return` list[String, AnalysisObject]

Recursively gets the list of Histogram/TGraph names inside a ROOT file directory (supports nested directories)

Definition at line 815 of file systematicsTool.py.

815def getFileKeys(d, basepath="/"):
816 """
817 `d` TDirectory or TFile (name of file or directory to be read)
818 `basepath` String [optional] (absolute path leading to `d` in the file)
819 `return` list[String, AnalysisObject]
820
821 Recursively gets the list of Histogram/TGraph names inside a ROOT file directory (supports nested directories)
822 """
823 for key in d.GetListOfKeys():
824 kname = key.GetName()
825 if key.IsFolder():
826 if d.Get(kname).ClassName() == "TList": continue
827 if d.Get(kname).ClassName() == "TTree": continue
828 for i in getFileKeys(d.Get(kname), basepath + kname + "/"):
829 yield i
830 else:
831 yield basepath + kname, d.Get(kname)
832
833
An interface for getting the name of a class as a string.

◆ getFormulaComponents()

systematicsTool.getFormulaComponents ( formula)
`formula` String
`return` list(String)

Take this formula written in this string and recusively obtain a list of
needed basic components, eg:
Function1(foo, Function2(bar, foo), Function3(foo, Function4 (bar, foo)))
--> [foo, bar, foo, foo, bar, foo]

Definition at line 387 of file systematicsTool.py.

387def getFormulaComponents(formula):
388 """
389 `formula` String
390 `return` list(String)
391
392 Take this formula written in this string and recusively obtain a list of
393 needed basic components, eg:
394 Function1(foo, Function2(bar, foo), Function3(foo, Function4 (bar, foo)))
395 --> [foo, bar, foo, foo, bar, foo]
396 """
397 result = []
398 if ")" not in formula:
399 # if there are no brackets in the formula, then there is just one element,
400 # so add it to the list
401 result += [formula]
402 return result
403 else:
404 # if not, open the first level of brackets and recursively re-apply
405 # this function to the arguments
406 operation = formula.partition("(")[0]
407 operand = formula.partition("(")[-1].rpartition(")")[0]
408 if 'Value' != operation:
409 for argument in splitOperand(operand):
410 result += getFormulaComponents(argument)
411 return result
412
413

◆ getPlotInfo()

systematicsTool.getPlotInfo ( aoName,
pathInRivetEnv )
`aoName` String (name to access plot info for)
`pathInRivetEnv` String (.plot file where to get the plot info from)
`return` dict{String, String} (the list of plotting specifications from the
.plot file, as a dict)

Rivet uses a separate .plot file to format plots. We want to use the same
information to format our plots, and this function is a helper to retrieve
that info in a more usable format from a given .plot file.

Definition at line 1316 of file systematicsTool.py.

1316def getPlotInfo(aoName, pathInRivetEnv):
1317 """
1318 `aoName` String (name to access plot info for)
1319 `pathInRivetEnv` String (.plot file where to get the plot info from)
1320 `return` dict{String, String} (the list of plotting specifications from the
1321 .plot file, as a dict)
1322
1323 Rivet uses a separate .plot file to format plots. We want to use the same
1324 information to format our plots, and this function is a helper to retrieve
1325 that info in a more usable format from a given .plot file.
1326 """
1327 f = open(pathInRivetEnv, 'r')
1328 inBlock = False
1329 res = {}
1330 for line in f.readlines():
1331 line = line.strip()
1332 if "BEGIN" in line:
1333 regex = line.split()[-1].replace("..", ".*") + ".*"
1334 try:
1335 match = re.findall(regex, aoName)
1336 except Exception:
1337 print("[WARNING] this line in .plot does not provide a valid regex..: " + line)
1338 return res
1339 if len(match) > 0: inBlock = True
1340 continue
1341 elif "END" in line:
1342 inBlock = False
1343 elif inBlock:
1344 k = line.partition("=")[0]
1345 v = line.partition("=")[-1]
1346 res[k] = v
1347 return res
1348
1349

◆ getSumOfWeights()

systematicsTool.getSumOfWeights ( path,
nominalWeight = "",
sumwHistName = "" )

Definition at line 207 of file systematicsTool.py.

207def getSumOfWeights(path, nominalWeight="", sumwHistName=""):
208 if 'root' in path:
209 f = r.TFile.Open(path)
210 for i in f.GetListOfKeys():
211 if sumwHistName in i.GetName():
212 return f.Get(i.GetName()).Integral()
213 else:
214 aos = yoda.read(path, patterns='.*/_EVTCOUNT.*')
215 if '/_EVTCOUNT' not in aos.keys():
216 return 1.
217 elif '/_EVTCOUNT[' in aos.keys():
218 return aos['/_EVTCOUNT[%s]' % nominalWeight].sumW()
219 else:
220 return aos['/_EVTCOUNT'].sumW()
221
222

◆ getXS()

systematicsTool.getXS ( dsid,
campaign = 15,
userFile = None )
`dsid` Int (dataset ID of the ATLAS dataset you want to get the
cross-section for
`campaign` String [optional] (the MC campaign number 15 for mc15_13TeV
datasets, 16 for mc16_13TeV. If campaign > 16 look instead for a local
file `data/PMGxsecDB_manual.txt` where the user can input the XS info)
`return` Float

Fetch the cross-section (XS) of the dataset whose ID you specified.
This function checks for the DSID in the central PMG XS database on cvmfs
but by setting campaign = 999 you can force it to check in a local file:
PMGxsecDB_manual.txt where the user can input the XS info manually.

Definition at line 272 of file systematicsTool.py.

272def getXS(dsid, campaign=15, userFile=None):
273 """
274 `dsid` Int (dataset ID of the ATLAS dataset you want to get the
275 cross-section for
276 `campaign` String [optional] (the MC campaign number 15 for mc15_13TeV
277 datasets, 16 for mc16_13TeV. If campaign > 16 look instead for a local
278 file `data/PMGxsecDB_manual.txt` where the user can input the XS info)
279 `return` Float
280
281 Fetch the cross-section (XS) of the dataset whose ID you specified.
282 This function checks for the DSID in the central PMG XS database on cvmfs
283 but by setting campaign = 999 you can force it to check in a local file:
284 PMGxsecDB_manual.txt where the user can input the XS info manually.
285 """
286 if userFile is not None:
287 campaign = 14
288 # this triggers a search in the manual, local XS file
289 if campaign > 16 or campaign < 15:
290 dataDir = os.environ["SYSTTOOLSPATH"] + "/data/"
291 if userFile is None:
292 pmgXSFile = "%s/PMGxsecDB_manual.txt" % dataDir
293 else:
294 pmgXSFile = userFile
295 else:
296 pmgXSFile = "/cvmfs/atlas.cern.ch/repo/sw/database/GroupData/dev/PMGTools/PMGxsecDB_mc%d.txt" % campaign
297
298 # these are defined keep track of whether the desired DSID was found in the
299 # requestd XS file
300 foundDataset = 0
301 thisSampleFinalCrossSection = 0.
302 thisSampleKFactorUncertainty = 0.
303 hasKFactor = 0
304 thisSampleKFactor = 0
305
306 # use a TTree to read the file, faster/more efficient than a python loop.
307 pmgXSTree = r.TTree()
308 pmgXSTree.ReadFile(pmgXSFile)
309 for dsidLine in pmgXSTree:
310 if int(dsid) == int(dsidLine.dataset_number):
311 thisSampleCrossSection = dsidLine.crossSection
312 thisSampleFilterEff = dsidLine.genFiltEff
313 thisSampleKFactor = dsidLine.kFactor
314 try:
315 thisSampleKFactorUncertainty = (dsidLine.relUncertUP + dsidLine.relUncertDOWN) * 0.5
316 except Exception:
317 thisSampleFinalCrossSection = 0
318 thisSampleFinalCrossSection = thisSampleCrossSection * thisSampleFilterEff * thisSampleKFactor
319 foundDataset = 1
320 if dsidLine.kFactor != 1: hasKFactor = 1
321 break
322 # if we can't fine the desired DSID, recursively try elsewhere before failing
323 if not foundDataset:
324
325 print("[WARNING] could not find " + str(dsid) + " in " + pmgXSFile + " for campaign " + str(campaign))
326 if campaign > 16:
327 print("[ERROR] could not find " + str(dsid) + " in " + pmgXSFile)
328 print("--> you'll need to add it in manually before continuing")
329 return -1, -1, 0
330 else: thisSampleFinalCrossSection, hasKFactor, thisSampleKFactorUncertainty = getXS(dsid, campaign + 1, userFile)
331
332 return thisSampleFinalCrossSection, thisSampleKFactor, thisSampleKFactorUncertainty
333
334

◆ getXSFromYODA()

systematicsTool.getXSFromYODA ( path)
`path` String (path to YODA file)
`return` Float

Definition at line 261 of file systematicsTool.py.

261def getXSFromYODA(path):
262 """
263 `path` String (path to YODA file)
264 `return` Float
265 """
266 if '/_XSEC' not in yoda.read(path, patterns='.*/_XSEC.*').keys():
267 return 1.
268 else:
269 return yoda.read(path, patterns='.*/_XSEC.*')['/_XSEC'].points()[0].x()
270
271

◆ lookupPDFSetName()

systematicsTool.lookupPDFSetName ( lhapdfid)
`lhapdfid` Int
`return` String

Takes a LHAPDF ID code and figures out the name of the PDF Set
which it belongs to.

Definition at line 190 of file systematicsTool.py.

190def lookupPDFSetName(lhapdfid):
191 """
192 `lhapdfid` Int
193 `return` String
194
195 Takes a LHAPDF ID code and figures out the name of the PDF Set
196 which it belongs to.
197 """
198 lhapdfid = int(lhapdfid)
199 for a in lhapdf.availablePDFSets():
200 b = lhapdf.getPDFSet(a)
201 if (lhapdfid >= b.lhapdfID) and (lhapdfid < (b.lhapdfID + b.size)):
202 return a
203 # return empty sting if nothing is found
204 return ""
205
206

◆ main()

systematicsTool.main ( argv)
This module can also be run as a standalone executable.
For info about the options try:
systematicsTool.py -h

This is useful for unpacking large GRID job outputs, as it will do all the
book-keeping, untarring of tarballs, merging across jobs, across subsamples,
across samples weighted by DSID, and then finally processing the systematics
and plotting them. Multithreading available.

Definition at line 2353 of file systematicsTool.py.

2353def main(argv):
2354 """
2355 This module can also be run as a standalone executable.
2356 For info about the options try:
2357 systematicsTool.py -h
2358
2359 This is useful for unpacking large GRID job outputs, as it will do all the
2360 book-keeping, untarring of tarballs, merging across jobs, across subsamples,
2361 across samples weighted by DSID, and then finally processing the systematics
2362 and plotting them. Multithreading available.
2363 """
2364 parser = optparse.OptionParser(usage="%prog [options]")
2365 parser.add_option("-d", "--directory", help="Directory containing the rucio downloads.", dest="directory", default="samples")
2366 parser.add_option("-p", "--process", help="Name of process to merge. Will only apply merge to directories containing this name. Also accepts the format regex:label, where the label is used to name the output directory, and the regex to select which directories to merge. Accepts comma separated list of regex:label pairs", dest="process", default="Zee")
2367 parser.add_option("-g", "--generator", help="Name of generator to merge. Will only apply merge to directories containing this name. Also accepts the format regex:label, where the label is used to name the output directory, and the regex to select which directories to merge. Accepts comma separated list of regex:label pairs", dest="generator", default="Sherpa")
2368 parser.add_option("--force", help="By default, if a subsample has already been untarred and merged, it is not done again. This option forces the script to re-untar and re-merge. Slow, unless you use a lot of threads...", dest="force", default=False)
2369 parser.add_option("--plotsDir", help="Where to save the plots to? Default is <generator>_<process>_<label>/plots", dest="plotsDir", default=None)
2370 parser.add_option("-l", "--label", help="An additional label for the output directory of the merge and as a prefix to the plots", dest="label", default="")
2371 parser.add_option("-f", "--filter", help="Only process AOs matching this regex", dest="filter", default=".*")
2372 parser.add_option("-e", "--exclude", help="Do NOT process AOs matching this regex", dest="exclude", default=None)
2373 parser.add_option("-j", "--nThreads", help="Multithread some of the merging steps", dest="nThreads", default=4)
2374 parser.add_option("--noSyst", help="Ignore systematics, only merge/process/draw the nominal weight", dest="noSyst", default=False, action="store_true")
2375 parser.add_option("--notTGZ", help="Use if your input files were just yoda/root files not tarballs", dest="notTGZ", default=False, action="store_true")
2376 parser.add_option("--uxs", "--userXS", help="When looking up cross-sections for each DSID, check the specified file first before defaulting to CVMFS values", dest="userXS", default=None)
2377 parser.add_option("--nw", "--nominalWeight", help="Bypass the DSID_weights database, and use the specified name as the nominal. Only for us with --noSyst", dest="nominalWeight", default="")
2378 parser.add_option("--sp", "--skipPlots", help="Do the merging of files, but just skip the plotting at the end", dest="skipPlots", default=False, action="store_true")
2379 parser.add_option("--normalize", help="Normalize any output plots to unit integral", dest="normalize", default=False, action="store_true")
2380 parser.add_option("--r2n", "--rivet2Normalised", help="Are your histograms normalised by the sum of weights in each variation, as is done typically for Rivet2 analyses? specify 1/0 for True/False", dest="rivetNormalised", default=False)
2381 parser.add_option("--sw", "--sumwHistName", help="Name of the TH1 that stores the sum-of-weights in the case of root file", dest="sumwHistName", default="__EVTCOUNT")
2382 parser.add_option("--cr", "--customReplacements", help="A .txt file specifying which replacements to apply to weight names, one per line, in the format `<old string>--><new string>` (without quotes), eg ` -->` to replace spaces with nothing, and `.-->_` to replace dots with underscores. One replacement per line which will be applied sequentially in the order listed in the file.", dest="customReplacements", default=None)
2383 parser.add_option("--schema", help="Tells the tool how to access the analysis objects from the inputs provided. Format is PathToFile:ObjectName. For example, if you have one YODA file per MC Weight, and each histogram is named the same in each input file, you schema would be !INDIR/!WEIGHTNAME.yoda:!AONAME. If you instead store in a single root file all the variations, where the weight name is appended to the histo name, your schema would be !INDIR/!INFILE.yoda:!AONAME[!WEIGHTNAME]. The available identifiers are: !INDIR (path to input dir), !INFILE (name of input file), !AONAME (analysis object name), !WEIGHTNAME (the MC weight name, assuming that safeFileName was applied to changes spaces to underscores etc...), !NSFWEIGHTNAME (assuming no weight name replacements at all), or !CRWEIGHTNAME (using custom repalcements, in this case please use option --customReplacements or --cr to specify them)", dest="schema", default='!INDIR/!INFILE.yoda:!AONAME[!WEIGHTNAME]')
2384 parser.add_option("--plotInfo", help="A rivet-style .plot file to tell the tool how to format your output plots. See data/ExclusivePlusInclusive.plot as an example. Also works for plots make from root files!", dest="plotInfo", default=None)
2385 (opts, args) = parser.parse_args()
2386
2387 r.gErrorIgnoreLevel = r. kError
2388 # Loop through each process,
2389 # and get hold of the corresponding regexes
2390 for process in opts.process.split(","):
2391 if ":" in process:
2392 tokens = process.split(":")
2393 processRegex = tokens[0]
2394 process = tokens[1]
2395 else:
2396 processRegex = process
2397
2398 # Loop through each generator,
2399 # and get hold of the corresponding regexes
2400 for generator in opts.generator.split(","):
2401 if ":" in generator:
2402 tokens = generator.split(":")
2403 generatorRegex = tokens[0]
2404 generator = tokens[1]
2405 else:
2406 generatorRegex = generator
2407
2408 if opts.rivetNormalised is not None:
2409 opts.rivetNormalised = bool(int(opts.rivetNormalised))
2410
2411 # for backwards-compatibility with default rivet2-style outputs
2412 if opts.rivetNormalised is True:
2413 if "--schema" not in sys.argv:
2414 opts.schema = "!INDIR/!WEIGHTNAME.yoda:!AONAME"
2415
2416 rootOrYoda = 'root' if '.root' in opts.schema else 'yoda'
2417 if "WEIGHT" in opts.schema.split(":")[0]:
2418 structure = "OneFilePerVariation"
2419 else:
2420 structure = "AllVariationsInFile"
2421
2422 # constrct the filter for generato/process pair
2423 thisFilter = '.*%s.*%s.*' % (generatorRegex, processRegex)
2424
2425 # construct output dir
2426 if opts.plotsDir is None:
2427 opts.plotsDir = "%s_%s_%s/plots" % (process, generator, opts.label)
2428 os.system("mkdir -p %s" % opts.plotsDir)
2429
2430 infiles = {} # sample: pdftype: [files]
2431 pwd = os.getcwd()
2432 jobsMergedFiles = {}
2433 print("================================================================================")
2434 print("[INFO] Processing files which match this regex " + thisFilter)
2435 print("================================================================================")
2436 print("")
2437 print("")
2438
2439 # unatr grid outputs, merged across jobs
2440 print("-----------------------------------------------------------------------")
2441 print("[INFO] Untar Grid outputs ")
2442 print("------------------------------------------------------------------------")
2443
2444 # setup the untar jobs
2445 pool = ThreadPool(int(opts.nThreads))
2446 threadInputs = []
2447 counter = -1
2448 untarredFiles = {}
2449 samplesToProcess = []
2450 workingDir = opts.directory
2451 if '/afs/' not in workingDir: workingDir = pwd + "/" + opts.directory
2452 # loop through samples and select those passing the regex
2453 for sample in os.listdir(workingDir):
2454 counter += 1
2455 fulldir = workingDir + "/" + sample
2456 if not re.search(thisFilter, sample): continue
2457 if not re.search('user.', sample): continue
2458 # for each each job start by extracting the tarball if not already done
2459 if not os.path.isfile(fulldir):
2460 samplesToProcess += [fulldir]
2461 for fulldir in samplesToProcess:
2462 threadInputs.append([fulldir, opts.force])
2463 untarredFiles[fulldir] = {}
2464 if len(threadInputs) == 0:
2465 print("[ERROR] no directories in %s matches the requested regex! exit !" % workingDir)
2466 exit(1)
2467
2468 def threadJobFunction(inputs):
2469 return extractTarballsFromDirectory(inputs[0], inputs[1], rootOrYoda=rootOrYoda, notTGZ=opts.notTGZ)
2470
2471 results = pool.map(threadJobFunction, threadInputs)
2472 pool.close()
2473 pool.join()
2474 for jobResult in results:
2475 for res in jobResult:
2476 if structure == "OneFilePerVariation":
2477 untarredFiles[res[0]].setdefault(res[1], []).append(res[2])
2478 else:
2479 untarredFiles[res[0]].setdefault('AllVariations', []).append(res[2])
2480
2481 print("")
2482 print("")
2483 print("------------------------------------------------------------------------")
2484 print("[INFO] Merging across jobs for each DSID")
2485 print("------------------------------------------------------------------------")
2486
2487 for fulldir in sorted(untarredFiles.keys()):
2488 print("[INFO] Merging files for jobs of " + os.path.basename(fulldir))
2489
2490 infiles = untarredFiles[fulldir]
2491 nFileTypes = len(infiles.keys())
2492 if nFileTypes == 0:
2493 print("[ERROR] 0 file types found in inputs! Exit!")
2494 print(untarredFiles)
2495 exit(1)
2496
2497 # multi-threaded merge of files across jobs
2498 # fn, pathsToMerged, fulldir, progress
2499
2500 def threadJobFunction(inputs):
2501 mergeInChunks(inputs[0], inputs[1], progressDict=inputs[2], rootOrYoda=rootOrYoda)
2502
2503 # setup the threads and do merge
2504 pool = ThreadPool(int(opts.nThreads))
2505 threadInputs = []
2506 counter = -1
2507 progressDict = {}
2508 progressDict['N'] = int(opts.nThreads)
2509 for fn, paths in infiles.items():
2510 counter += 1
2511 basename = safeFileName(os.path.basename(fn))
2512 progressDict[basename] = 'pend'
2513 outfn = "%s/merged/%s.%s" % (fulldir, basename, rootOrYoda)
2514 sampleName = os.path.basename(fulldir)
2515 threadInputs.append([outfn, paths, progressDict])
2516 jobsMergedFiles.setdefault(basename, {})[sampleName] = outfn
2517 results = pool.map(threadJobFunction, threadInputs)
2518 pool.close()
2519 pool.join()
2520 print('\r[INFO] Done Merging %s' % os.path.basename(fulldir))
2521
2522 print("")
2523 print("")
2524 print("------------------------------------------------------------------------")
2525 print("[INFO] Get available MEweights from DSID database and make sure they are consistent!")
2526 print("------------------------------------------------------------------------")
2527
2528 xsDict = {}
2529
2530 def threadJobFunction(inputs):
2531 fulldir = inputs[0]
2532 noSyst = inputs[1]
2533 xsDict = inputs[2]
2534 dsid = re.findall(r"\.[0-9]{6,6}\.", fulldir)[0].replace(".", "")
2535 print("\r[INFO] Looking up XS and MEWeights for DSID = " + repr(dsid))
2536 if rootOrYoda == 'yoda' and getXSFromYODA(jobsMergedFiles.values()[0][os.path.basename(fulldir)]) != 1.0:
2537 print("\r[INFO] Histograms in YODA file of DSID =%s are already scaled by xsec. Set xsec to 1.0 to avoid double scaling!" % dsid)
2538 xsDict[dsid] = 1.0, 0, 0
2539 else:
2540 xsDict[dsid] = getXS(int(dsid), userFile=opts.userXS)
2541 availableWeights = None
2542 nominalWeight = None
2543 if not noSyst:
2544 if availableWeights is None:
2545 availableWeights = rDB.getWeights(dsid)[0]
2546 for k, v in availableWeights.items():
2547 if v['type'] == 'Nominal':
2548 nominalWeight = v['nominal']
2549
2550 else:
2551 nominalWeight = jobsMergedFiles.keys()[0]
2552 if len(opts.nominalWeight) > 0: nominalWeight = opts.nominalWeight
2553 availableWeights = {'_Nominal':
2554 {'nominal_pdf': '-1', 'type': 'Nominal', 'nominal': '%s' % nominalWeight,
2555 'weights': ['%s' % nominalWeight], 'combination': 'none'
2556 }
2557 }
2558 if xsDict[dsid][2] > 0:
2559 availableWeights["Normalization"] = {'nominal_pdf': '-1', 'type': 'Normalization', 'nominal': nominalWeight, 'weights': [nominalWeight], 'combination': 'norm', "value": xsDict[dsid][2]}
2560
2561 return [availableWeights, dsid]
2562
2563 pool = ThreadPool(int(opts.nThreads))
2564 threadInputs = []
2565 for fulldir in sorted(untarredFiles.keys()):
2566 threadInputs.append([fulldir, opts.noSyst, xsDict])
2567
2568 results = pool.map(threadJobFunction, threadInputs)
2569 pool.close()
2570 pool.join()
2571
2572 print()
2573 print("[INFO] Consistentcy Checks:")
2574 availableWeights = None
2575 uniqueWeights = []
2576 for dsid, xs in xsDict.items():
2577 if (xsDict[dsid][0] == -1):
2578 print("\r[INFO] ERROR: could not find Xs for DSID =%s. Exit." % (str(dsid)))
2579 exit(1)
2580 else:
2581 print("\r[INFO] DSID =%s has XS =%.6f pb (kFactor =%.2f, normUnvertainty =%.2f)" % (str(dsid), xsDict[dsid][0], xsDict[dsid][1], xsDict[dsid][2]) + " and found ME weights")
2582 for res in results:
2583 if availableWeights is None:
2584 availableWeights = res[0]
2585 print("[INFO] DSID = " + res[1] + " has weights" + repr(availableWeights.keys()))
2586
2587 elif(sorted(availableWeights.keys()) == sorted(res[0].keys())):
2588 print("[INFO] DSID = " + res[1] + " has weights consistent with the above")
2589 else:
2590 print("[ERROR] DSID = " + res[1] + " has weights NOT consistent with the above!!")
2591 exit(1)
2592
2593 for k, v in availableWeights.items():
2594 if "_Nominal" in k:
2595 nominalWeight = v
2596 for w in v['weights']:
2597 if w in uniqueWeights: continue
2598 else: uniqueWeights += [w]
2599
2600 if (len(uniqueWeights) == 0 and not opts.noSyst):
2601 print("[ERROR] Your sample DISD = " + res[1] + " has no weights... so it might be missing from the database!")
2602 print("[ERROR] Please contact lcorpe@cern.ch to get your sample added...")
2603 print("[ERROR] In the meantime, we will exit since no combination recipe is availale... you can still run with --noSyst")
2604 exit(1)
2605
2606 print("")
2607 print("")
2608 print("------------------------------------------------------------------------")
2609 print("[INFO] Calculating on the fly corrections to XSs for each systematic")
2610 print("------------------------------------------------------------------------")
2611
2612 if opts.rivetNormalised is None:
2613 if rootOrYoda == 'yoda': opts.rivetNormalised = True
2614 else: opts.rivetNormalised = True
2615
2616 allFilesToMerge = {}
2617
2618 def threadJobFunction(inputs):
2619 systName = safeFileName(inputs[0])
2620 progressDict = inputs[4]
2621 updateProgress(progressDict, systName, "run")
2622 res = getCrossSectionCorrection(inputs[1], inputs[2], inputs[3], inputs[5], inputs[6], inputs[7], inputs[8])
2623 updateProgress(progressDict, systName, "done")
2624 return systName, res
2625
2626 pool = ThreadPool(int(opts.nThreads))
2627 threadInputs = []
2628
2629 progressDict = {}
2630 applyKFactorCorrection = None
2631 progressDict['N'] = int(opts.nThreads)
2632 for syst in uniqueWeights:
2633 if structure == "AllVariationsInFile":
2634 sample = jobsMergedFiles.values()[0]
2635 else:
2636 sample = jobsMergedFiles[safeFileName(syst)]
2637 systSampleFiles = []
2638 xsList = []
2639 nomSampleFiles = []
2640 for sampleName, path in sample.items():
2641 dsid = re.findall("[0-9]{6,6}", sampleName)[0].replace(".", "")
2642 xsList += [xsDict[dsid][0]]
2643 if applyKFactorCorrection is None: applyKFactorCorrection = xsDict[dsid][1]
2644 elif applyKFactorCorrection != xsDict[dsid][1]:
2645 print("[ERROR], cannot merge samples with inconsistent k-factors !")
2646 print(xsDict)
2647 exit(1)
2648 else: pass
2649 systSampleFiles += [path]
2650 if structure == "AllVariationsInFile":
2651 nomSampleFiles += [jobsMergedFiles.values()[0][sampleName]]
2652 else:
2653 nomSampleFiles += [jobsMergedFiles[safeFileName(nominalWeight['nominal'])][sampleName]]
2654
2655 if structure == "AllVariationsInFile":
2656 threadInputs += [[syst, xsList, systSampleFiles, nomSampleFiles, progressDict, opts.rivetNormalised, applyKFactorCorrection, safeFileName(syst), safeFileName(nominalWeight['nominal'])]]
2657 else:
2658 threadInputs += [[syst, xsList, systSampleFiles, nomSampleFiles, progressDict, opts.rivetNormalised, applyKFactorCorrection, "", ""]]
2659 progressDict[syst] = 'pend'
2660
2661 results = pool.map(threadJobFunction, threadInputs)
2662
2663 pool.close()
2664 pool.join()
2665 for jobResult in results:
2666 syst = jobResult[0]
2667 for systFile, weight in jobResult[1]:
2668 allFilesToMerge.setdefault(syst, []).append(" %s:%f" % (systFile, weight))
2669
2670 print("")
2671 print("------------------------------------------------------------------------")
2672 print("[INFO] Merging files across DSIDs for each systematic")
2673 print("------------------------------------------------------------------------")
2674
2675 def threadJobFunction(inputs):
2676 s = inputs[0]
2677 debug = 0
2678 generator = inputs[1]
2679 process = inputs[2]
2680 updateProgress(progressDict, s, "run")
2681 os.system("mkdir -p %s_%s_%s/merged_sample" % (process, generator, opts.label))
2682 if rootOrYoda == 'root':
2683
2684 if structure == "AllVariationsInFile":
2685 mergedFile = "%s_%s_%s/merged_sample/%s.%s" % (process, generator, opts.label, safeFileName(s), rootOrYoda)
2686 thisFilter = "%s__.*" % safeFileName(s)
2687 thisFindAndReplace = "%s__:" % safeFileName(s)
2688 command = "haddw -f %s -r %s %s %s &> out.txt " % (thisFilter, thisFindAndReplace, mergedFile, " ".join(allFilesToMerge[safeFileName(s)]))
2689 if debug: print(command)
2690 os.system(command)
2691
2692 if structure == "OneFilePerVariation":
2693 mergedFile = "%s_%s_%s/merged_sample/%s.%s" % (process, generator, opts.label, safeFileName(s), rootOrYoda)
2694 command = "haddw %s %s &> out.txt " % (mergedFile, " ".join(allFilesToMerge[s]))
2695 os.system(command)
2696 else:
2697 mergedFile = "%s_%s_%s/merged_sample/%s.%s" % (process, generator, opts.label, safeFileName(s), rootOrYoda)
2698 if structure == "AllVariationsInFile":
2699 if rivetINameReplacements(s) != "" and not opts.noSyst:
2700 command = r"yodamerge_tmp.py --add -o %s %s -m r'.*\[%s\].*' &> out.txt " % (mergedFile, " ".join(allFilesToMerge[safeFileName(s)]), rivetINameReplacements(s, escapePlus=True))
2701 if debug:
2702 print(command)
2703 print(r"sed -i 's/\[%s\]//g' %s" % (rivetINameReplacements(s), mergedFile))
2704 os.system(command)
2705 os.system(r"sed -i 's/\[%s\]//g' %s" % (rivetINameReplacements(s), mergedFile))
2706 if len(allFilesToMerge[safeFileName(s)]) == 1: # to deal with issue where individual files are not scaled by yodamerge
2707 sampleToMerge = allFilesToMerge[safeFileName(s)][0].split(":")
2708 command = "yodascale -c '.* %fx' -i %s &> out.txt \n" % (float(sampleToMerge[1]), mergedFile)
2709 if debug: print(command)
2710 os.system(command)
2711
2712 else:
2713 command = r"yodamerge_tmp.py --add -o %s %s -M .*\\\[.*\\\].* &> out.txt " % (mergedFile, " ".join(allFilesToMerge[safeFileName(s)]))
2714 if debug: print(command)
2715 os.system(command)
2716 if len(allFilesToMerge[safeFileName(s)]) == 1: # to deal with issue where individual files are not scaled by yodamerge
2717 sampleToMerge = allFilesToMerge[safeFileName(s)][0].split(":")
2718 command = "yodascale -c '.* %fx' -i %s &> out.txt \n" % (float(sampleToMerge[1]), mergedFile)
2719 if debug: print(command)
2720 os.system(command)
2721 if structure == "OneFilePerVariation":
2722 if len(allFilesToMerge[s]) == 1:
2723 sampleToMerge = allFilesToMerge[s][0].split(":")
2724 command = "cp %s %s ; yodascale -c '.* %fx' -i %s &> out.txt \n" % (sampleToMerge[0], mergedFile, float(sampleToMerge[1]), mergedFile)
2725 else:
2726 command = "yodamerge_tmp.py --add -o %s %s &> out.txt " % (mergedFile, " ".join(allFilesToMerge[s]))
2727 if debug: print(command)
2728 os.system(command)
2729
2730 updateProgress(progressDict, s, "done")
2731
2732 if rootOrYoda == 'root': nThreads = 1 # for the merging of root files, IO errors if running with more than 1 thread
2733 else: nThreads = int(opts.nThreads)
2734 pool = ThreadPool(nThreads)
2735 threadInputs = []
2736 progressDict = {}
2737 progressDict['N'] = nThreads
2738 if (structure == "AllVariationsInFile"):
2739 for i, s in enumerate(uniqueWeights):
2740 progressDict[s] = 'pend'
2741 threadInputs.append([s, generator, process, progressDict])
2742 elif (structure == "OneFilePerVariation"):
2743 for i, s in enumerate(allFilesToMerge.keys()):
2744 progressDict[s] = 'pend'
2745 threadInputs.append([s, generator, process, progressDict])
2746
2747 results = pool.map(threadJobFunction, threadInputs)
2748 pool.close()
2749 pool.join()
2750
2751 print("")
2752 print("")
2753 print("------------------------------------------------------------------------")
2754 print("[INFO] Combining variations into named sources of uncertainty")
2755 print("------------------------------------------------------------------------")
2756
2757 mergedSystDict = combineAllVariations(availableWeights, "%s_%s_%s/merged_sample" % (process, generator, opts.label), "%s_%s_%s/merged_final/." % (process, generator, opts.label), opts.filter, opts.exclude, schema="!INDIR/!WEIGHTNAME.%s:!AONAME" % rootOrYoda, customWeightNameReplacements=opts.customReplacements)
2758
2759 print("")
2760 print("-----------------------------------------------")
2761 print("[INFO] Produce plots of the systematic uncertainties (with bands)")
2762 print("-----------------------------------------------")
2763
2764 if not opts.skipPlots and rootOrYoda == "yoda":
2765 plots = makeSystematicsPlotsWithRIVET(mergedSystDict, opts.plotsDir, "Nominal", label=opts.label, plotInfo=opts.plotInfo, normalize=opts.normalize)
2766 for p in plots:
2767 print("[INFO] printed: " + p)
2768
2769
std::vector< std::string > split(const std::string &s, const std::string &t=":")
Definition hcg.cxx:177
int main()
Definition hello.cxx:18

◆ makeDummyHisto()

systematicsTool.makeDummyHisto ( tg,
isLog = False,
XandYMinAndMax = None,
ratioZoom = None,
isRatio = False )
`tg` TGraphAsymmErrors (use this to build the dummy TH1D)
`isLog` Bool [optional] (Max/Min will need to be adjusted differently
in case of a log plot)
`XandYMinAndMax` [Float, Float, Float, Float] or None (if not None, use this factor as the low/high limits of the
plot axes)
`ratioZoom` [Float, Float] or None (if not None, use this factor as the low/high limits of the
ratio plot)
`isRatio` Bool [optional] (Specify whether or not this is a ratio plot)
`return` TH2D (with appropriate max/min on each axis)

In order to control better what axis ranges to use, construct a dummy TH1D
with the desired max/min on each axis to be able to show the TGraphs nicely

Definition at line 1402 of file systematicsTool.py.

1402def makeDummyHisto(tg, isLog=False, XandYMinAndMax=None, ratioZoom=None, isRatio=False):
1403 """
1404 `tg` TGraphAsymmErrors (use this to build the dummy TH1D)
1405 `isLog` Bool [optional] (Max/Min will need to be adjusted differently
1406 in case of a log plot)
1407 `XandYMinAndMax` [Float, Float, Float, Float] or None (if not None, use this factor as the low/high limits of the
1408 plot axes)
1409 `ratioZoom` [Float, Float] or None (if not None, use this factor as the low/high limits of the
1410 ratio plot)
1411 `isRatio` Bool [optional] (Specify whether or not this is a ratio plot)
1412 `return` TH2D (with appropriate max/min on each axis)
1413
1414 In order to control better what axis ranges to use, construct a dummy TH1D
1415 with the desired max/min on each axis to be able to show the TGraphs nicely
1416 """
1417 multiplierUp = 1.
1418 multiplierDn = 1.
1419 if not isRatio:
1420 multiplierUp = 2.
1421 nBins = tg.GetN()
1422 xmin = tg.GetX()[0] - tg.GetErrorXlow(0)
1423 xmax = tg.GetX()[nBins - 1] + tg.GetErrorXhigh(nBins - 1)
1424 ymin = min([tg.GetY()[i] - tg.GetErrorYlow(i) for i in range(nBins)])
1425 ymax = max([tg.GetY()[i] + tg.GetErrorYhigh(i) for i in range(nBins)])
1426 if isLog:
1427 # if it's a log plot, the multpliers need to be much larger, and ymin >0
1428 if ymin < 0: ymin = min([tg.GetY()[i] for i in range(nBins)])
1429 multiplierUp = ymax / ymin if ymin > 0 else ymax
1430 multiplierUp *= 50
1431 multiplierDn = 0.5
1432 ymin *= multiplierDn
1433 ymax *= multiplierUp
1434 else:
1435 ymax *= multiplierUp
1436 if ratioZoom is not None:
1437 # if a particular axis rangefor the ratio plot is specified, use that
1438 ymin = ratioZoom[0] + 0.001
1439 ymax = ratioZoom[1] - 0.001
1440 multiplierUp = 1.
1441 multiplierDn = 1.
1442 elif isRatio:
1443 av = (abs(1 - ymin) + abs(1 - ymax)) / 2
1444 ymin = 1 - av
1445 ymax = 1 + av
1446 multiplierUp = 1.1
1447 multiplierDn = 1. / 1.1
1448 ymin *= multiplierDn
1449 ymax *= multiplierUp
1450 if XandYMinAndMax is not None:
1451 if XandYMinAndMax[0] is not None: xmin = float(XandYMinAndMax[0])
1452 if XandYMinAndMax[1] is not None: xmax = float(XandYMinAndMax[1])
1453 if XandYMinAndMax[2] is not None: ymin = float(XandYMinAndMax[2])
1454 if XandYMinAndMax[3] is not None: ymax = float(XandYMinAndMax[3])
1455 res = r.TH2D('dummy', '', nBins, xmin, xmax, nBins, ymin, ymax)
1456 res.SetDirectory(0)
1457 return res
1458
1459
#define min(a, b)
Definition cfImp.cxx:40
#define max(a, b)
Definition cfImp.cxx:41

◆ makeSystematicsPlotsWithRIVET()

systematicsTool.makeSystematicsPlotsWithRIVET ( mergedSystDict,
plotsDir,
nominalName = "Nominal",
ratioZoom = None,
regexFilter = None,
regexVeto = None,
label = "",
plotInfo = None,
normalize = False )
`mergedSystDict` dict{String, String} (format is as follows {N!label:filename},
where N is the order in which to plot the variations, labels is what to put on
the legend, and filename is the name of the YODA/ROOT file to use as input)
`return` None

Make some ugly plots using modified rivet make-plots. 

Definition at line 1841 of file systematicsTool.py.

1841def makeSystematicsPlotsWithRIVET(mergedSystDict, plotsDir, nominalName="Nominal", ratioZoom=None, regexFilter=None, regexVeto=None, label="", plotInfo=None, normalize=False):
1842 """
1843 `mergedSystDict` dict{String, String} (format is as follows {N!label:filename},
1844 where N is the order in which to plot the variations, labels is what to put on
1845 the legend, and filename is the name of the YODA/ROOT file to use as input)
1846 `return` None
1847
1848 Make some ugly plots using modified rivet make-plots.
1849 """
1850 print("[INFO] Compare histos")
1851 fileNames = mergedSystDict.keys()
1852 print("[INFO] fileNames" + repr(fileNames))
1853 colors = ["GREEN", "PURPLE", "YELLOW", "RED", "GREY", "BLUE", "ORANGE"]
1854 cc = 0
1855 plotText = ""
1856 plotText += 'MBLUE="blue!40!white" ; '
1857 plotText += 'MGREEN="green!40!white" ; '
1858 plotText += 'MRED="red!40!white" ; '
1859 plotText += 'DBLUE="blue!60!white" ; '
1860 plotText += 'DGREEN="green!60!white" ; '
1861 plotText += 'DRED="red!60!white" ; '
1862 plotText += 'MYELLOW="yellow!40!white" ; '
1863 plotText += 'DYELLOW="yellow!60!white" ; '
1864 plotText += 'MORANGE="orange!40!white" ; '
1865 plotText += 'DORANGE="orange!60!white" ; '
1866 plotText += 'MGREY="black!60!white" ; '
1867 plotText += 'DGREY="black!40!white" ; '
1868 plotText += 'MPURPLE="purple!60!white" ; '
1869 plotText += 'DPURPLE="purple!40!white" ; '
1870 if normalize:
1871 plotText += 'COMMON_TAGS="RatioPlotSameStyle=1:ErrorBars=0:ErrorBands=1:ErrorBandOpacity=0.3:NormalizeToIntegral=1" ; '
1872 plotText += " rivet-mkhtml --errs "
1873 else:
1874 plotText += 'COMMON_TAGS="RatioPlotSameStyle=1:ErrorBars=0:ErrorBands=1:ErrorBandOpacity=0.3"; '
1875 plotText += " rivet-mkhtml --reftitle=Data --errs "
1876 for kn, lab in mergedSystDict.items():
1877 if "Nominal" in lab:
1878 plotText += ' %s:RatioPlotSameStyle=1:ErrorBars=1:LineColor=black:"Title=%s" ' % (kn, lab)
1879 elif "PDF_ME" in lab:
1880 col = "RED"
1881 plotText += ' %s:$COMMON_TAGS:ErrorBandColor=$M%s:LineColor=%s:"Title=%s" ' % (kn, col, col.lower(), lab)
1882 elif "scale_ME" in lab:
1883 col = "ORANGE"
1884 plotText += ' %s:$COMMON_TAGS:ErrorBandColor=$D%s:LineColor=%s:"Title=%s" ' % (kn, col, col.lower(), lab)
1885 elif "alphaS" in lab:
1886 col = "BLUE"
1887 plotText += ' %s:$COMMON_TAGS:ErrorBandColor=$D%s:LineColor=%s:"Title=%s" ' % (kn, col, col.lower(), lab)
1888 elif "altPDF" in lab:
1889 col = colors[cc % len(colors)]
1890 cc += 1
1891 plotText += ' %s:LineStyle=dashed:RatioPlotSameStyle=1:ErrorBars=0:LineColor=%s:"Title=%s" ' % (kn, col.lower(), lab)
1892 elif "Total" in lab:
1893 col = "GREY"
1894 plotText += ' %s:$COMMON_TAGS:ErrorBandStyle=hlines:ErrorBandColor=$D%s:LineColor=$M%s:"Title=%s" ' % (kn, col, col, lab)
1895 else:
1896 col = colors[cc % len(colors)]
1897 cc += 1
1898 plotText += ' %s:$COMMON_TAGS:ErrorBandStyle=hlines:ErrorBandColor=$D%s:LineColor=%s:"Title=%s" ' % (kn, col, col.lower(), lab)
1899 plotText = '%s -o %s \n' % (plotText, "tmp")
1900
1901 os.system("mkdir -p tmp")
1902 os.system("mkdir -p %s" % (plotsDir))
1903 os.system(plotText)
1904 res = []
1905 for dirname in os.listdir("tmp"):
1906 ananame = dirname
1907 dirname = "tmp/" + dirname
1908 if os.path.isdir(dirname):
1909 for f in os.listdir(dirname):
1910 if "pdf" not in f: continue
1911 fpng = f.replace("pdf", "png")
1912 fnew = ananame + "__" + f
1913 fnewpng = ananame + "__" + fpng
1914 f = dirname + "/" + f
1915 res += ["%s/%s" % (plotsDir, fnew)]
1916 fpng = dirname + "/" + fpng
1917 os.system("convert -flatten -density 150 %s %s &> /dev/null " % (f, fpng))
1918 os.system("mv %s %s/%s" % (f, plotsDir, fnew))
1919 os.system("mv %s %s/%s" % (fpng, plotsDir, fnewpng))
1920 os.system("rm -r tmp")
1921 print("your plots are here:" + plotsDir)
1922 return res
1923
1924

◆ makeSystematicsPlotsWithROOT()

systematicsTool.makeSystematicsPlotsWithROOT ( mergedSystDict,
outdir,
nominalName = "Nominal",
ratioZoom = None,
regexFilter = None,
regexVeto = None,
label = "",
plotInfo = None )
`mergedSystDict` dict{String, String} (format is as follows {N!label:filename},
where N is the order in which to plot the variations, labels is what to put on
the legend, and filename is the name of the YODA/ROOT file to use as input)
`outdir` String (the output dir for the plots)
`nominalName` String (which entry of mergedSystDict is the nominal? One of the
entries in mergedSystDict should have this as a label)
`ratioZoom` [Float, Float] or None (if not None, use this factor as the low/high limits of the
ratio plot)
`regexFilter` String [optional] (AOs whose names match regex are processed)
`regexVeto` String [optional](AOs whose names match regex are NOT processed)
`label` String [optional](additional label to add to the plots name)
`plotInfo` String [optional](path to plot info file (will try to find it dynamically if not specified)
`return` dict of output plots

This is a generic macro which will make the output plots of the AOs contained
in the files listed in mergedSystDict.

Definition at line 1460 of file systematicsTool.py.

1460def makeSystematicsPlotsWithROOT(mergedSystDict, outdir, nominalName="Nominal", ratioZoom=None, regexFilter=None, regexVeto=None, label="", plotInfo=None):
1461 """
1462 `mergedSystDict` dict{String, String} (format is as follows {N!label:filename},
1463 where N is the order in which to plot the variations, labels is what to put on
1464 the legend, and filename is the name of the YODA/ROOT file to use as input)
1465 `outdir` String (the output dir for the plots)
1466 `nominalName` String (which entry of mergedSystDict is the nominal? One of the
1467 entries in mergedSystDict should have this as a label)
1468 `ratioZoom` [Float, Float] or None (if not None, use this factor as the low/high limits of the
1469 ratio plot)
1470 `regexFilter` String [optional] (AOs whose names match regex are processed)
1471 `regexVeto` String [optional](AOs whose names match regex are NOT processed)
1472 `label` String [optional](additional label to add to the plots name)
1473 `plotInfo` String [optional](path to plot info file (will try to find it dynamically if not specified)
1474 `return` dict of output plots
1475
1476 This is a generic macro which will make the output plots of the AOs contained
1477 in the files listed in mergedSystDict.
1478 """
1479 r.gROOT.SetBatch()
1480 r.gStyle.SetOptStat(00000)
1481 outputPlots = []
1482 # Want to try to retrieve a .plot and reference .yoda file for the relevant analysis!
1483 # This is where to check first..
1484 RIVET_ANALYSIS_PATH = os.environ.get("RIVET_ANALYSIS_PATH")
1485 os.system("mkdir -p %s" % (outdir))
1486
1487 # sort the dict and collect <label>:<filename> pairs
1488 # fileNames = ["%s:'%s'" % (v, k.split("!")[-1]) for k, v in sorted(mergedSystDict.items())]
1489 fileNames = ["%s:'%s'" % (k, v) for k, v in sorted(mergedSystDict.items())]
1490
1491 # holders for the aos to plot, and the ratios wrt nominal
1492 aos = {}
1493 aos_ratio = {}
1494
1495 # The plots will be coloured in this order
1496 colorList = [r.kMagenta, r.kRed, r.kOrange, r.kYellow, r.kGreen, r.kBlue,
1497 r.kViolet, r.kPink, r.kSpring, r.kTeal, r.kCyan,
1498 r.kAzure]
1499
1500 # holder for the plotting info for each AO
1501 plotInfoDict = {}
1502
1503 # Get the nominal variation, with respect to which the ratios are made
1504 fnIndex = -1
1505 nominalForRatio = None
1506 for fn in fileNames:
1507 if nominalName in fn:
1508 nominalForRatio = copy.deepcopy(readFromFile(fn.split(":")[0]))
1509 if nominalForRatio is None:
1510 print("WARNING, could not identify nominal ! exit()")
1511 exit(1)
1512
1513 # Now collect the AOs for each variation/filename, in the order they were specified
1514 for fn in fileNames:
1515 fnIndex += 1
1516 aosThisFN = readFromFile(fn.split(":")[0], regexFilter, regexVeto)
1517 for aoName in aosThisFN.keys():
1518 if "superAO" in aoName: continue
1519 aoNameNoRef = aoName.replace("/REF", "")
1520 ao = aosThisFN[aoName]
1521 if aoName not in aos.keys():
1522 aos[aoNameNoRef] = {}
1523 aos_ratio[aoNameNoRef] = {}
1524
1525 # We are not interested in the error bands for alternative PDFs
1526 setYErrorsToZero = False
1527 if "altPDF" in fn:
1528 setYErrorsToZero = True
1529
1530 # fill the holding dicts with TGraphs
1531 nfr = ""
1532 if aoName in nominalForRatio.keys():
1533 nfr = nominalForRatio[aoName]
1534 elif "/REF" + aoName in nominalForRatio.keys():
1535 nfr = nominalForRatio["/REF" + aoName]
1536 aos[aoNameNoRef][fn] = arrayDictToTGraph(ao, False, setYErrorsToZero)
1537 aos_ratio[aoNameNoRef][fn] = arrayDictToTGraph(ao, False, setYErrorsToZero, nfr)
1538
1539 # this is the best guess as to where the reference data might be !
1540 dataDir = os.environ["SYSTTOOLSPATH"] + "/data/"
1541 rivetAnalysis = aoName.split("/")[1] if len(aoName.split("/")) > 1 else ""
1542 pathInRivetEnv = "%s/Rivet/%s.yoda" % (RIVET_ANALYSIS_PATH, rivetAnalysis)
1543 # for the first file, also get the .plot which will be needed to format
1544 # the output plot. Also look for any reference data!
1545 if fnIndex == len(fileNames) - 1 and 'Data' not in nominalName:
1546 # now try to get the style .plot file and save the formatting info
1547 refDataPath = ""
1548 if os.path.isfile(pathInRivetEnv):
1549 refDataPath = pathInRivetEnv
1550 elif os.path.isfile("%s.yoda" % rivetAnalysis):
1551 refDataPath = "%s.yoda" % rivetAnalysis
1552 elif os.path.isfile("%s/%s.yoda" % (dataDir, rivetAnalysis)):
1553 refDataPath = "%s/%s.yoda" % (dataDir, rivetAnalysis)
1554 else:
1555 pass
1556
1557 # if reference data was found, also produce TGraphs for that
1558 # and ratio wrt Nominal
1559 theAOs = readFromFile(refDataPath, regexFilter, regexVeto)
1560 theAO = None
1561 if "/REF" + aoNameNoRef in theAOs.keys() or aoNameNoRef in theAOs.keys():
1562 if "/REF" + aoNameNoRef in theAOs.keys():
1563 theAO = theAOs["/REF" + aoNameNoRef]
1564 elif aoNameNoRef in theAOs.keys():
1565 theAO = theAOs[aoNameNoRef]
1566 aos[aoNameNoRef][rivetAnalysis] = arrayDictToTGraph(theAO,
1567 True
1568 )
1569 aos_ratio[aoNameNoRef][rivetAnalysis] = arrayDictToTGraph(theAO,
1570 True, # isData
1571 False, # setYErrorsToZero
1572 nfr # divide by this nominal
1573 )
1574 else:
1575 print("[WARNING] could not find AO with name " + aoNameNoRef + " or " + "REF" + aoNameNoRef + " in REF file " + refDataPath)
1576 print(" Perhaps due to a version mis-match where the AO names were changed... skip this data file for now")
1577
1578 if plotInfo is not None:
1579 plotInfoDict[aoNameNoRef] = getPlotInfo(aoNameNoRef, plotInfo)
1580 elif os.path.isfile(pathInRivetEnv.replace("yoda", "plot")):
1581 plotInfoDict[aoNameNoRef] = getPlotInfo(aoNameNoRef, pathInRivetEnv.replace("yoda", "plot"))
1582 elif os.path.isfile("%s.plot" % rivetAnalysis):
1583 plotInfoDict[aoNameNoRef] = getPlotInfo(aoNameNoRef, "%s.plot" % rivetAnalysis)
1584 elif os.path.isfile("%s/%s.plot" % (dataDir, rivetAnalysis)):
1585 plotInfoDict[aoNameNoRef] = getPlotInfo(aoNameNoRef, "%s/%s.plot" % (dataDir, rivetAnalysis))
1586 else:
1587 pass
1588 for k, v in plotInfoDict.items():
1589 pass
1590 # now loop through the AOs, and plot each variation
1591 aoNames = []
1592 for aoName in aos.keys():
1593 if 'RAW' in aoName: continue
1594 aoNames += [aoName]
1595
1596 for aoName in aoNames:
1597 print("[INFO] processing ao %d/%d: %s " % (aoNames.index(aoName), len(aoNames), aoName))
1598
1599 aoNameSafe = aoName.replace("/", "__")
1600
1601 # place the legend and format
1602 nFilesToProcess = len(fileNames)
1603 leg = r.TLegend(0.17, 0.9 - 0.1 * ((2 + nFilesToProcess) / 2), 0.88, 0.80)
1604 leg.SetLineWidth(0)
1605 leg.SetTextFont(43)
1606 leg.SetTextSize(13)
1607 leg.SetNColumns(2)
1608
1609 # TLatex style and prepare holder for all the TLatex instances to write
1610 latexes = [] # format wil be [alignment, text size (wrt pixel) angle, x, y, text]
1611 latexes.append([11, 17, 0., 0.15, 0.905, "#scale[1.2]{#bf{#it{ATLAS}}} Internal"])
1612 latexes.append([33, 15, 90., 0.005, 0.35, safeRootLatex("Ratio to %s" % nominalName)])
1613 latexes.append([31, 15, 0., 0.9, 0.905, aoName])
1614 lat = r.TLatex()
1615 lat.SetTextFont(43)
1616 lat.SetNDC()
1617
1618 # Now prepare the canvas and pads for main plot and ratio plot
1619 tc = r.TCanvas(aoNameSafe, aoNameSafe, 500, 500)
1620 pad1 = r.TPad("pad1", "pad1", 0.0, 0.30, 1.0, 0.97)
1621 pad2 = r.TPad("pad2", "pad2", 0.0, 0.05, 1.0, 0.35)
1622 r.SetOwnership(pad1, False)
1623 r.SetOwnership(pad2, False)
1624 pad1.SetLeftMargin(0.15)
1625 pad2.SetLeftMargin(0.15)
1626 pad1.SetBottomMargin(0.09)
1627 pad2.SetTopMargin(0.0)
1628 pad2.SetBottomMargin(0.15)
1629 pad1.Draw()
1630 pad2.Draw()
1631 pad2.SetGridy()
1632 pad1.cd()
1633 pad1.SetLogy(0)
1634
1635 # determine which variations are to be processed, and add the ref data too
1636 # if we have it
1637 rivetAnalysis = aoName.split("/")[1] if len(aoName.split("/")) > 1 else ""
1638 averageErrorSize = {}
1639 for fn in fileNames:
1640 averageErrorSize[fn] = getAverageUncertaintySizePerBin(fn.split(":")[0], regexFilter=".*%s" % aoName)
1641
1642 # order by size of uncertainty
1643 fileNamesToProcess = []
1644 for k, v in sorted(averageErrorSize.items(), key=lambda x: x[1], reverse=True):
1645 fileNamesToProcess += [k]
1646
1647 if rivetAnalysis in aos[aoName].keys():
1648 fileNamesToProcess += [rivetAnalysis]
1649
1650 # now loop over the systematics and plot them
1651 fnIndex = -1
1652 for fn in fileNamesToProcess:
1653 fnIndex += 1
1654
1655 # prepare leged entry for this systematic
1656 # legend is usually [<label>] <PDF Set Name>, or other relevant info
1657
1658 if ":" in fn and '[Data]' not in fn:
1659 isData = False
1660 legendName = "%s" % fn.split(":")[1].replace("'", "")
1661 else:
1662 isData = True
1663 legendName = "[Data]"
1664 if 'Nominal' in fn:
1665 legendName += " MC Stat."
1666 if 'altPDF' in fn:
1667 aos[aoName][fn].SetLineStyle(2)
1668 aos_ratio[aoName][fn].SetLineStyle(2)
1669 if ("PDF" in fn) and ('Nominal' not in fn):
1670 pdfstring = ""
1671 if len(re.findall(r'PDF\d+', fn)) > 0:
1672 pdfstring = re.findall(r'PDF\d+', fn)[0].replace("PDF", "")
1673 if len(pdfstring) > 2:
1674 pdfsetName = lookupPDFSetName(int(pdfstring))
1675 legendName += " " + pdfsetName.partition("_")[0]
1676 legendName = (legendName[:20] + '...') if len(legendName) > 20 else legendName
1677 if 'Total Uncertainty' in fn:
1678 aos[aoName][fn].SetFillStyle(3004)
1679 aos[aoName][fn].SetFillColorAlpha(r.kBlack, 0.5)
1680 aos[aoName][fn].SetLineColor(r.kBlack)
1681 aos_ratio[aoName][fn].SetFillStyle(3004)
1682 aos_ratio[aoName][fn].SetFillColorAlpha(r.kBlack, 0.5)
1683 aos_ratio[aoName][fn].SetLineColor(r.kBlack)
1684 elif legendName != "[Data]":
1685 aos[aoName][fn].SetFillColorAlpha(colorList[fnIndex], 0.5)
1686 aos[aoName][fn].SetLineColor(colorList[fnIndex])
1687 aos_ratio[aoName][fn].SetFillColorAlpha(colorList[fnIndex], 0.5)
1688 aos_ratio[aoName][fn].SetLineColor(colorList[fnIndex])
1689 else:
1690 aos[aoName][fn].SetMarkerSize(1)
1691 aos[aoName][fn].SetMarkerStyle(20)
1692 aos[aoName][fn].SetLineWidth(3)
1693 aos_ratio[aoName][fn].SetMarkerSize(1)
1694 aos_ratio[aoName][fn].SetMarkerStyle(20)
1695 aos_ratio[aoName][fn].SetLineWidth(3)
1696
1697 # for the first systematic (usually Total Uncertainty) we need to apply
1698 # the .plot formatting directives, and make a dummy histo to Draw on
1699 # with nice axis ranges
1700 oldRatioZoom = ratioZoom
1701 if (fnIndex == 0):
1702 if aoName not in plotInfoDict.keys():
1703 plotInfoDict[aoName] = {}
1704 if 'LogY' not in plotInfoDict[aoName].keys():
1705 plotInfoDict[aoName]['LogY'] = 0
1706 pad1.SetLogy(0)
1707 else:
1708 pad1.SetLogy(int(plotInfoDict[aoName]['LogY']))
1709 if 'LogX' not in plotInfoDict[aoName].keys():
1710 plotInfoDict[aoName]['LogX'] = 0
1711 pad1.SetLogx(0)
1712 pad2.SetLogx(0)
1713 else:
1714 pad1.SetLogx(int(plotInfoDict[aoName]['LogX']))
1715 pad2.SetLogx(int(plotInfoDict[aoName]['LogX']))
1716
1717 # this is where the dummy TH1s are made, with nice axis ranges
1718 # and sensible Font Styles/Sizes
1719 XandYMinAndMax = [None, None, None, None]
1720 XandYMinAndMax_ratio = [None, None, None, None]
1721
1722 if "XMin" in plotInfoDict[aoName].keys():
1723 XandYMinAndMax[0] = plotInfoDict[aoName]['XMin']
1724 XandYMinAndMax_ratio[0] = plotInfoDict[aoName]['XMin']
1725
1726 if "XMax" in plotInfoDict[aoName].keys():
1727 XandYMinAndMax[1] = plotInfoDict[aoName]['XMax']
1728 XandYMinAndMax_ratio[1] = plotInfoDict[aoName]['XMax']
1729
1730 if "YMin" in plotInfoDict[aoName].keys():
1731 XandYMinAndMax[2] = plotInfoDict[aoName]['YMin']
1732 if "YMax" in plotInfoDict[aoName].keys():
1733 XandYMinAndMax[3] = plotInfoDict[aoName]['YMax']
1734
1735 if "RatioPlotYMin" in plotInfoDict[aoName].keys():
1736 if ratioZoom is None: ratioZoom = [0., 2.]
1737 ratioZoom[0] = float(plotInfoDict[aoName]['RatioPlotYMin'])
1738 if "RatioPlotYMax" in plotInfoDict[aoName].keys():
1739 if ratioZoom is None: ratioZoom = [0., 2.]
1740 ratioZoom[1] = float(plotInfoDict[aoName]['RatioPlotYMax'])
1741
1742 dummyHisto = makeDummyHisto(aos[aoName][fn], int(plotInfoDict[aoName]['LogY']), XandYMinAndMax)
1743 dummyHisto_ratio = makeDummyHisto(aos_ratio[aoName][fn], False, XandYMinAndMax, ratioZoom, True)
1744 ratioZoom = oldRatioZoom
1745 pad1.cd()
1746 dummyHisto.Draw("")
1747 pad2.cd()
1748 dummyHisto_ratio.Draw("")
1749 dummyHisto.GetYaxis().SetLabelFont(43)
1750 dummyHisto.GetYaxis().SetLabelSize(15)
1751 dummyHisto.GetXaxis().SetLabelFont(43)
1752 dummyHisto.GetXaxis().SetLabelSize(0)
1753 dummyHisto_ratio.GetXaxis().SetLabelFont(43)
1754 dummyHisto_ratio.GetXaxis().SetLabelSize(15)
1755 dummyHisto_ratio.GetYaxis().SetLabelFont(43)
1756 dummyHisto_ratio.GetYaxis().SetLabelSize(15)
1757
1758 # apply the formatting directives from the .plot file
1759 for k, v in plotInfoDict[aoName].items():
1760 if k == 'XTwosidedTicks':
1761 pad1.SetTickx(int(v))
1762 pad2.SetTickx(int(v))
1763 if k == 'YTwosidedTicks':
1764 pad1.SetTicky(int(v))
1765 pad2.SetTicky(int(v))
1766 if k == 'XCustomMajorTicks':
1767 for i in range(1, dummyHisto.GetNbinsX() + 1):
1768 dummyHisto.GetXaxis().SetBinLabel(i, "")
1769 dummyHisto_ratio.GetXaxis().SetBinLabel(i, safeRootLatex(v.split()[2 * i - 1]))
1770 if k == 'Title':
1771 latexes.append([13, 15, 0., 0.2, 0.88, safeRootLatex(v)])
1772 if k == 'XLabel':
1773 latexes.append([33, 15, 0., 0.9, 0.05, safeRootLatex(v)])
1774 if k == 'YLabel':
1775 latexes.append([33, 15, 90., 0.005, 0.9, safeRootLatex(v)])
1776
1777 # then draw the TGraph, and add entry to the legend
1778 pad1.cd()
1779 if isData:
1780 # then draw the TGraph, and add entry to the legend
1781 pad1.cd()
1782 aos[aoName][fn].Draw("pe")
1783 pad2.cd()
1784 aos_ratio[aoName][fn].Draw("pe")
1785 leg.AddEntry(aos[aoName][fn], legendName, "pe")
1786 elif aos[aoName][fn].GetN() > 1:
1787 aos[aoName][fn].Draw("l3")
1788 pad2.cd()
1789 aos_ratio[aoName][fn].Draw("l3")
1790 leg.AddEntry(aos[aoName][fn], legendName, "lf")
1791 else:
1792 aos[aoName][fn].Draw("e")
1793 aos[aoName][fn].Draw("e2")
1794 pad2.cd()
1795 aos_ratio[aoName][fn].Draw("e")
1796 aos_ratio[aoName][fn].Draw("e2")
1797 leg.AddEntry(aos[aoName][fn], legendName, "lf")
1798
1799 elif isData:
1800 # then draw the TGraph, and add entry to the legend
1801 pad1.cd()
1802 aos[aoName][fn].Draw("pe")
1803 pad2.cd()
1804 aos_ratio[aoName][fn].Draw("pe")
1805 leg.AddEntry(aos[aoName][fn], legendName, "pe")
1806
1807 else:
1808 # then draw the TGraph, and add entry to the legend
1809 pad1.cd()
1810 if aos[aoName][fn].GetN() > 1:
1811 aos[aoName][fn].Draw("l3")
1812 pad2.cd()
1813 aos_ratio[aoName][fn].Draw("l3")
1814 else:
1815 aos[aoName][fn].Draw("e")
1816 aos[aoName][fn].Draw("e2")
1817 pad2.cd()
1818 aos_ratio[aoName][fn].Draw("e")
1819 aos_ratio[aoName][fn].Draw("e2")
1820 legendMarker = "lf"
1821 if 'altPDF' in fn: legendMarker = "l"
1822 leg.AddEntry(aos[aoName][fn], legendName, legendMarker)
1823
1824 # finally, draw the TLatex instances
1825 pad1.cd()
1826 leg.Draw()
1827 tc.cd()
1828 for latex in latexes:
1829 lat.SetTextAlign(latex[0])
1830 lat.SetTextSize(latex[1])
1831 lat.SetTextAngle(latex[2])
1832 lat.DrawLatex(latex[3], latex[4], latex[5])
1833
1834 # now print(the canvas and move to next AO)
1835 tc.Print("%s/%s%s.pdf" % (outdir, label, aoNameSafe))
1836 tc.Print("%s/%s%s.png" % (outdir, label, aoNameSafe))
1837 outputPlots += ["%s/%s%s.pdf" % (outdir, label, aoNameSafe)]
1838 return outputPlots
1839
1840

◆ mergeInChunks()

systematicsTool.mergeInChunks ( outfn,
paths,
progressDict = None,
nFilesPerChunk = 100,
force = False,
rootOrYoda = 'yoda' )
`outfn` String (The name of the output Yoda file you wish to produce, with full path)
`paths` list[String] (List of Yoda files to merge, with full paths)
`progressText` String [optional] (An extra string to print(at the end of the progress message) )
`nFilesPerChunk` int [optional] (How many files to do per chunk)
`force` Bool [optional] by default, if there is already a matching merged file, this function does nothing
but `force` forces the merge again
`rootOrYoda` String [optional] (specify file type of objects to merge)
`return` None

This function safely merges multiple yoda files in chunks of nFilesToProcess at a time (if too many, yodamerge can fail!)
Recommended is nFilesPerChunk = 100, but definitely less than 300.

Definition at line 2246 of file systematicsTool.py.

2246def mergeInChunks(outfn, paths, progressDict=None, nFilesPerChunk=100, force=False, rootOrYoda='yoda'):
2247 """
2248 `outfn` String (The name of the output Yoda file you wish to produce, with full path)
2249 `paths` list[String] (List of Yoda files to merge, with full paths)
2250 `progressText` String [optional] (An extra string to print(at the end of the progress message) )
2251 `nFilesPerChunk` int [optional] (How many files to do per chunk)
2252 `force` Bool [optional] by default, if there is already a matching merged file, this function does nothing
2253 but `force` forces the merge again
2254 `rootOrYoda` String [optional] (specify file type of objects to merge)
2255 `return` None
2256
2257 This function safely merges multiple yoda files in chunks of nFilesToProcess at a time (if too many, yodamerge can fail!)
2258 Recommended is nFilesPerChunk = 100, but definitely less than 300.
2259 """
2260 fulldir = os.path.dirname(os.path.abspath(outfn))
2261 os.system("mkdir -p %s" % fulldir)
2262 os.system("mkdir -p %s/tmp" % fulldir)
2263 fnShort = os.path.basename(outfn)
2264 mergeTool = "yodamerge_tmp.py -o "
2265 gzsuffix = ""
2266 if 'root' == rootOrYoda:
2267 assert('root' in fnShort and 'yoda' not in fnShort)
2268 fnShort = fnShort.partition(".root")[0]
2269 mergeTool = "hadd -f "
2270 else:
2271 assert('root' not in fnShort and 'yoda' in fnShort)
2272 fnShort = fnShort.partition(".yoda")[0]
2273 gzsuffix = fnShort.partition(".yoda")[2]
2274 if progressDict is not None:
2275 updateProgress(progressDict, fnShort, "run")
2276 if os.path.isfile(outfn) and not force:
2277 pass
2278 else:
2279 time.sleep(0.1 * randint(1, 50))
2280 filesToMerge = " "
2281 if len(paths) > nFilesPerChunk:
2282 for iChunk in range(0, len(paths), nFilesPerChunk):
2283 # if progressDict is not None:
2284 chunkPaths = paths[iChunk:iChunk + nFilesPerChunk]
2285 chunkFilesToMerge = ' '.join(chunkPaths)
2286 chunkMergedFile = "%s/tmp/%s.chunk%d.%s" % (fulldir, fnShort, iChunk, rootOrYoda + gzsuffix)
2287 os.system("%s %s %s &> out1.%d.log " % (mergeTool, chunkMergedFile, chunkFilesToMerge, iChunk))
2288 print("%s %s %s &> out1.%d.log " % (mergeTool, chunkMergedFile, chunkFilesToMerge, iChunk))
2289 filesToMerge += chunkMergedFile + " "
2290 else:
2291 filesToMerge = ' '.join(paths)
2292 if 'yoda' == rootOrYoda: # this breaks for root files
2293 for p in paths:
2294 if ".yoda.gz" not in p: # breaks for .yoda.gz files
2295 os.system("sed -i 's/|//g' %s" % p)
2296 os.system("%s %s %s &> out2.log " % (mergeTool, outfn, filesToMerge))
2297 if progressDict is not None:
2298 updateProgress(progressDict, fnShort, "done")
2299
2300

◆ printProgress()

systematicsTool.printProgress ( progress)
`progress` dict (a dictionary of the job labels:status)
`return` Void

This is a helper function print(the progress update in multi-)
threaded processes

Definition at line 2221 of file systematicsTool.py.

2221def printProgress(progress):
2222 """
2223 `progress` dict (a dictionary of the job labels:status)
2224 `return` Void
2225
2226 This is a helper function print(the progress update in multi-)
2227 threaded processes
2228 """
2229 printString = ""
2230 nChar = 160
2231 pend = 0
2232 run = 0
2233 done = 0
2234 printString = "--> Running with %d threads: " % progress['N']
2235 for k, v in sorted(progress.items(), key=lambda x: x[0]):
2236 if v == 'done': done += 1
2237 if v == 'pend': pend += 1
2238 if v == 'run': run += 1
2239 printString += " DONE =%d, RUN =%d, PENDING =%d " % (done, run, pend)
2240 if len(printString) > nChar:
2241 printString = printString[:nChar] + "..."
2242 sys.stdout.write('\r %s' % printString)
2243 sys.stdout.flush()
2244
2245

◆ readFromFile()

systematicsTool.readFromFile ( filename,
regexFilter = None,
regexVeto = None )
`filename` String (path to file which is to be read)
`regexFilter` String [optional] (AOs whose names match regex are processed)
`regexVeto` String [optional](AOs whose names match regex are NOT processed)
`return` dict{String, AnalysisObject}

decides whether to process a file as ROOT or YODA depending on the file extension

Definition at line 789 of file systematicsTool.py.

789def readFromFile(filename, regexFilter=None, regexVeto=None):
790 """
791 `filename` String (path to file which is to be read)
792 `regexFilter` String [optional] (AOs whose names match regex are processed)
793 `regexVeto` String [optional](AOs whose names match regex are NOT processed)
794 `return` dict{String, AnalysisObject}
795
796 decides whether to process a file as ROOT or YODA depending on the file extension
797 """
798 rootExtensions = ['.root', '.root1', '.root2']
799 yodaExtensions = ['.yoda', '.yoda1', '.yoda.gz']
800 if filename is None: return {}
801 if filename == "": return {}
802 filenameSplit = filename.split(":")
803 for rex in rootExtensions:
804 if filenameSplit[0].endswith(rex):
805 return readFromROOT(filename, regexFilter, regexVeto)
806 for ye in yodaExtensions:
807 if filenameSplit[0].endswith(ye):
808 return readFromYODA(filename, regexFilter, regexVeto)
809 print("[ERROR] could not identify input file type of %s ! please name your files with one of the following extensions:" % repr(filenameSplit))
810 print("[ERROR]" + repr(rootExtensions) + " for ROOT files")
811 print("[ERROR]" + repr(yodaExtensions) + " for YODA files")
812 exit(1)
813
814
bool readFromROOT(std::vector< std::string > &config)

◆ readFromROOT()

systematicsTool.readFromROOT ( filename,
regexFilter = None,
regexVeto = None )
`filename` String (path to file which is to be read)
`regexFilter` String [optional] (AOs whose names match regex are processed)
`regexVeto` String [optional](AOs whose names match regex are NOT processed)
`return` dict{String, AnalysisObject}

Open a ROOT file, read the contents and return them as AnalysisObjects.
Control which AOs to select/reject using the optional regexFilter/regexVeto
arguments.
Only supports TH1D and TGraphAsymmErrors types for now. TODO Support other types.

Definition at line 834 of file systematicsTool.py.

834def readFromROOT(filename, regexFilter=None, regexVeto=None):
835 """
836 `filename` String (path to file which is to be read)
837 `regexFilter` String [optional] (AOs whose names match regex are processed)
838 `regexVeto` String [optional](AOs whose names match regex are NOT processed)
839 `return` dict{String, AnalysisObject}
840
841 Open a ROOT file, read the contents and return them as AnalysisObjects.
842 Control which AOs to select/reject using the optional regexFilter/regexVeto
843 arguments.
844 Only supports TH1D and TGraphAsymmErrors types for now. TODO Support other types.
845 """
846 result = {}
847 filename = filename.split(":")
848 rootfile = r.TFile.Open(filename[0])
849 tfd = rootfile
850 path = ""
851 debug = 0
852 if len(filename) > 1:
853 if '/' in filename[1]:
854 path = filename[1].split("/")[0]
855 tfd = rootfile.Get(path)
856 for aoName in tfd.GetListOfKeys():
857 if debug: print("DEBUG open ao:", aoName)
858 if path == "":
859 aoName = aoName.GetName()
860 else:
861 aoName = path + "/" + aoName.GetName()
862 if debug: print("DEBUG modified name for ao:", aoName)
863 # apply the filters and vetos
864 if debug: print("DEBUG regex filte =%s, veto=%s" % (regexFilter, regexVeto))
865 if (regexFilter is not None):
866 if debug: print("DEBUG re.findall(regexFilter, aoName))", re.findall(regexFilter, aoName))
867 if (aoName not in re.findall(regexFilter, aoName)):
868 if debug: print("DEBUG ao is not in finall: continue")
869 continue
870 if (regexVeto is not None):
871 if debug: print("DEBUG re.findall(regexVeto, aoName))", re.findall(regexVeto, aoName))
872 if (aoName in re.findall(regexVeto, aoName)):
873 if debug: print("DEBUG ao is in finall: continue")
874 continue
875 rep = ""
876 newAoName = ""
877 if debug: print("DEBUG start AONAME replacement, filename", filename)
878 if len(filename) > 1:
879 rep = filename[1].replace("!AONAME", "")
880 filt = filename[1].replace("!AONAME", ".*")
881 capt = filename[1].replace("!AONAME", "(.*)")
882 if debug: print("DEBUG AONAME replacement. rep=", rep, "filt=", filt, "capt=", capt)
883 if debug: print("DEBUG AONAME re.findall(filt, aoName)", re.findall(filt, aoName))
884 if (aoName not in re.findall(filt, aoName)):
885 if debug: print("DEBUG AONAME not in re.findall(filt, aoName): continue")
886 continue
887
888 newAoName = re.findall(capt, aoName)[0]
889 else:
890 newAoName = aoName.replace(rep, "")
891 ao = rootfile.Get(aoName)
892 aoName = newAoName
893
894 aoResult = {}
895 aoResult['name'] = aoName
896 aoResult['path'] = filename[0] + '/' + aoName
897 if (type(r.TH1D()) == type(ao)) or (type(r.TH1F()) == type(ao)):
898 binRange = range(1, ao.GetNbinsX() + 1)
899 aoResult['info'] = 'Was_a_TH1'
900 aoResult['bw'] = np.array([ao.GetBinWidth(b) for b in binRange])
901 aoResult['y'] = np.array([ao.GetBinContent(b) for b in binRange])
902 stat = np.array([ao.GetBinError(b) for b in binRange])
903 aoResult['yup'] = (aoResult['y'] + stat)
904 aoResult['ydn'] = (aoResult['y'] - stat)
905 aoResult['x'] = np.array([ao.GetBinCenter(b) for b in binRange])
906 aoResult['xup'] = np.array([ao.GetBinLowEdge(b + 1) for b in binRange])
907 aoResult['xdn'] = np.array([ao.GetBinLowEdge(b) for b in binRange])
908 aoResult['nbins'] = len(binRange)
909 elif (type(r.TH2D()) == type(ao)) or (type(r.TH2F()) == type(ao)):
910 xbinRange = range(1, ao.GetNbinsX() + 1)
911 ybinRange = range(1, ao.GetNbinsY() + 1)
912 aoResult['info'] = 'Was_a_TH2'
913 aoResult['xbw'] = np.array([ao.GetXaxis().GetBinWidth(b) for b in xbinRange for c in ybinRange])
914 aoResult['ybw'] = np.array([ao.GetYaxis().GetBinWidth(c) for b in xbinRange for c in ybinRange])
915 aoResult['z'] = np.array([ao.GetBinContent(b, c) for b in xbinRange for c in ybinRange])
916 stat = np.array([ao.GetBinError(b, c) for b in xbinRange for c in ybinRange])
917 aoResult['zup'] = (aoResult['z'] + stat)
918 aoResult['zdn'] = (aoResult['z'] - stat)
919 aoResult['x'] = np.array([ao.GetXaxis().GetBinCenter(b) for b in xbinRange for c in ybinRange])
920 aoResult['xup'] = np.array([ao.GetXaxis().GetBinLowEdge(b + 1) for b in xbinRange for c in ybinRange])
921 aoResult['xdn'] = np.array([ao.GetXaxis().GetBinLowEdge(b) for b in xbinRange for c in ybinRange])
922 aoResult['y'] = np.array([ao.GetYaxis().GetBinCenter(c) for b in xbinRange for c in ybinRange])
923 aoResult['yup'] = np.array([ao.GetYaxis().GetBinLowEdge(c + 1) for b in xbinRange for c in ybinRange])
924 aoResult['ydn'] = np.array([ao.GetYaxis().GetBinLowEdge(c) for b in xbinRange for c in ybinRange])
925 aoResult['nbinsx'] = len(xbinRange)
926 aoResult['nbinsy'] = len(ybinRange)
927 elif type(r.TGraphAsymmErrors()) == type(ao):
928 binRange = range(0, ao.GetN())
929 aoResult['info'] = 'Was_a_TG'
930 aoResult['bw'] = np.array([ao.GetErrorXlow(b) + ao.GetErrorXhigh(b) for b in binRange]) # dummy
931 aoResult['y'] = np.array([ao.GetY()[b] for b in binRange])
932 aoResult['yup'] = np.array([ao.GetY()[b] + ao.GetErrorYhigh(b) for b in binRange])
933 aoResult['ydn'] = np.array([ao.GetY()[b] - ao.GetErrorYlow(b) for b in binRange])
934 aoResult['x'] = np.array([ao.GetX()[b] for b in binRange])
935 aoResult['xup'] = np.array([ao.GetX()[b] + ao.GetErrorXhigh(b) for b in binRange])
936 aoResult['xdn'] = np.array([ao.GetX()[b] - ao.GetErrorXlow(b) for b in binRange])
937 aoResult['nbins'] = len(binRange)
938 elif type(r.TTree) is type(ao):
939 aoResult['tree'] = ao.GetTree()
940 else:
941 continue
942 result[aoName] = aoResult
943 if len(result) == 0:
944 print("[ERROR] no analysis objects passed your filter, or your input files are empty of TH1 or TGraph objects. Exiting")
945 exit(1)
946 return result
947
948

◆ readFromYODA()

systematicsTool.readFromYODA ( filename,
regexFilter = None,
regexVeto = None )
`filename` String (path to file which is to be read)
`regexFilter` String [optional] (AOs whose names match regex are processed)
`regexVeto` String [optional](AOs whose names match regex are NOT processed)
`return` dict{String, AnalysisObject}

Open a YODA file, read the contents and return them as AnalysisObjects.
Control which AOs to select/reject using the optional regexFilter/regexVeto
arguments.

Definition at line 949 of file systematicsTool.py.

949def readFromYODA(filename, regexFilter=None, regexVeto=None):
950 """
951 `filename` String (path to file which is to be read)
952 `regexFilter` String [optional] (AOs whose names match regex are processed)
953 `regexVeto` String [optional](AOs whose names match regex are NOT processed)
954 `return` dict{String, AnalysisObject}
955
956 Open a YODA file, read the contents and return them as AnalysisObjects.
957 Control which AOs to select/reject using the optional regexFilter/regexVeto
958 arguments.
959 """
960 result = {}
961 filename = filename.split(":")
962 patterns = "" if regexFilter is None else '.*' + regexFilter + '.*'
963 unpatterns = [] if regexVeto is None else ['.*' + regexVeto + '.*']
964 if patterns == "" and len(filename) > 1:
965 patterns = filename[1].replace("!AONAME", ".*").replace("[", r"\[").replace("]", r"\]")
966 unpatterns += ["RAW/.*", "/REF/.*"]
967 histList = yoda.read(filename[0], patterns=patterns, unpatterns=unpatterns)
968 for aoName, ao in histList.items():
969 # apply the filters and vetos
970 if (regexFilter is not None):
971 if (aoName not in re.findall(regexFilter, aoName)): continue
972 if (regexVeto is not None):
973 if (aoName in re.findall(regexVeto, aoName)): continue
974
975 rep = ""
976 if len(filename) > 1:
977 rep = filename[1].replace("!AONAME", "")
978 filt = filename[1].replace("!AONAME", ".*")
979 filt = filename[1].replace("!AONAME", ".*").replace("[", r"\[").replace("]", r"\]")
980 if (aoName not in re.findall(filt, aoName)): continue
981 aoName = aoName.replace(rep, "")
982
983 aoResult = {}
984 aoResult['name'] = ao.name().replace(rep, "")
985 aoResult['path'] = ao.path().replace(rep, "")
986 aoResult['fname'] = filename
987 aoResult['annotations'] = {ann: ao.annotation(ann) for ann in ao.annotations()}
988 aoResult['annotations']['Path'] = aoResult['path']
989 if type(yoda.Histo1D()) == type(ao):
990 aoResult['bw'] = np.array([b.xWidth() for b in ao.bins()])
991 aoResult['y'] = np.array([b.sumW() for b in ao.bins()]) / aoResult['bw']
992 stat = np.array([b.sumW2() for b in ao.bins()]) / (aoResult['bw']) ** 2
993 aoResult['yup'] = (aoResult['y'] + np.sqrt(stat))
994 aoResult['ydn'] = (aoResult['y'] - np.sqrt(stat))
995 aoResult['x'] = np.array([b.xMid() for b in ao.bins()])
996 aoResult['xup'] = np.array([b.xMax() for b in ao.bins()])
997 aoResult['xdn'] = np.array([b.xMin() for b in ao.bins()])
998 aoResult['nbins'] = len(ao.bins())
999 elif type(yoda.Scatter2D()) == type(ao):
1000 aoResult['bw'] = np.array([b.yErrs()[1] + b.yErrs()[0] for b in ao.points()]) # dummy
1001 aoResult['y'] = np.array([b.y() for b in ao.points()])
1002 aoResult['yup'] = np.array([b.y() + b.yErrs()[1] for b in ao.points()])
1003 aoResult['ydn'] = np.array([b.y() - b.yErrs()[0] for b in ao.points()])
1004 aoResult['x'] = np.array([b.x() for b in ao.points()])
1005 aoResult['xup'] = np.array([b.x() + b.xErrs()[1] for b in ao.points()])
1006 aoResult['xdn'] = np.array([b.x() - b.xErrs()[0] for b in ao.points()])
1007 aoResult['nbins'] = len(ao.points())
1008 elif type(yoda.Histo2D()) == type(ao):
1009 aoResult['xbw'] = np.array([b.xWidth() for b in ao.bins()])
1010 aoResult['ybw'] = np.array([b.yWidth() for b in ao.bins()])
1011 aoResult['z'] = np.array([b.sumW() for b in ao.bins()]) / (aoResult['xbw'] * aoResult['ybw'])
1012 stat = np.array([b.sumW2() for b in ao.bins()]) / ((aoResult['xbw'] * aoResult['ybw'])) ** 2
1013 aoResult['zup'] = (aoResult['z'] + np.sqrt(stat))
1014 aoResult['zdn'] = (aoResult['z'] - np.sqrt(stat)) # / aoResult['bw']
1015 aoResult['x'] = np.array([b.xMid() for b in ao.bins()])
1016 aoResult['xup'] = np.array([b.xMax() for b in ao.bins()])
1017 aoResult['xdn'] = np.array([b.xMin() for b in ao.bins()])
1018 aoResult['y'] = np.array([b.yMid() for b in ao.bins()])
1019 aoResult['yup'] = np.array([b.yMax() for b in ao.bins()])
1020 aoResult['ydn'] = np.array([b.yMin() for b in ao.bins()])
1021 aoResult['nbinsx'] = ao.numBinsX()
1022 aoResult['nbinsy'] = ao.numBinsY()
1023 else:
1024 continue
1025 result[aoName] = aoResult
1026
1027 if len(result) == 0:
1028 print("[ERROR] no analysis objects passed your filter, or your input files are empty of Histo1D or Scatter2D objects. Exiting")
1029 print("[ERROR] FYI: filename =%s, filter =%s, veto =%s " % (filename, regexFilter, regexVeto))
1030 exit(1)
1031
1032 return result
1033
1034

◆ renameFilesWithoutPrefix()

systematicsTool.renameFilesWithoutPrefix ( directory)
`directory` String
`return` None

Renames the files in a given directory, such that the longest common prefix
which occurs in all filenames is omitted. Useful if your GRID jobs were
submitted with the format <prefix>.<weights_names>.yoda and you want to
lose the <prefix>. bit !

Definition at line 85 of file systematicsTool.py.

85def renameFilesWithoutPrefix(directory):
86 """
87 `directory` String
88 `return` None
89
90 Renames the files in a given directory, such that the longest common prefix
91 which occurs in all filenames is omitted. Useful if your GRID jobs were
92 submitted with the format <prefix>.<weights_names>.yoda and you want to
93 lose the <prefix>. bit !
94 """
95
96 fileList = []
97 for fn in os.listdir(directory):
98 fileList += [fn]
99
100 if len(fileList) == 1: return
101
102 prefix = os.path.commonprefix(fileList)
103 for fn in os.listdir(directory):
104 os.system("mv %s/%s %s/%s" % (directory, fn, directory, fn.replace(prefix, "")))
105
106

◆ resolveFormula()

systematicsTool.resolveFormula ( nominal,
formula,
componentsMap,
level = 0,
verbose = 0 )
`nominal` AnalysisObject (nominal object, which gives us the central values
The type is a dict of np.arrays() encoding information equivalent
to a Scatter2D, Histo1D, TGraphAsymmErrors or TH1D. See module description
for more information about this format. TODO: dedicated class for this?)
`formula` String (The formula with which to combine the components)
`componentsMap` dict(filenames, AnalysisObjects) (this is a map between the
file which the component corresponds to and the AnalysisObject which it
corresponds to.
`level` Int [optional] (keeps track of how deep in the recursion we have gone)
`verbose` Int [optional] (0 or 1, whether or not to print(a lot of debug messages) )
`return` AnalysisObject

Resolves the formula iteratively, using the AnalysisObjects listed in the
componentsMap. resolveFormula supports the following functions:

`Central(arg1)`: Use the central value of arg1, and set errors to 0

`DownUpNominal(arg0, arg1, arg2)`: Produce a new AO where ydn is taken from
the central value ('y') of arg0, yup is taken from the central value of arg1,
and y is taken from the central value of arg2.

`Envelope(arg1,..., argN)`: Take the min/max envelope of the arguments as the
up/down errors of the resulting AO. The central value is taken from 'nominal'.

`Inverse(arg1)`: Take the inverse of the AO, so y--> 1/y, and the errors are
propagated as yerr --> |yerr/y **2|

`Product(arg1,..., argN)`: Take the product of the arguments of the function.
The 'y' value is the product of the 'y' values of the arguments, the errors
are propagated according the relative errors in quadrature.


`Average(arg1,..., argN)`: Take the product of the arguments of the function.
The 'y' value is the avergae of the 'y' values of the arguments, the errors
are taken from the first argument.

`QuadSum(arg1,.., argN)`: Sum in quadrature of the errors of the arguments,
and central value taken from nominal AO.

`Value(arg1)`: Used to scan an AO and its errors by the float specified in arg1.

`CopyError(arg1, arg2)`: Copy the up/down errors.

Definition at line 414 of file systematicsTool.py.

414def resolveFormula(nominal, formula, componentsMap, level=0, verbose=0):
415 """
416 `nominal` AnalysisObject (nominal object, which gives us the central values
417 The type is a dict of np.arrays() encoding information equivalent
418 to a Scatter2D, Histo1D, TGraphAsymmErrors or TH1D. See module description
419 for more information about this format. TODO: dedicated class for this?)
420 `formula` String (The formula with which to combine the components)
421 `componentsMap` dict(filenames, AnalysisObjects) (this is a map between the
422 file which the component corresponds to and the AnalysisObject which it
423 corresponds to.
424 `level` Int [optional] (keeps track of how deep in the recursion we have gone)
425 `verbose` Int [optional] (0 or 1, whether or not to print(a lot of debug messages) )
426 `return` AnalysisObject
427
428 Resolves the formula iteratively, using the AnalysisObjects listed in the
429 componentsMap. resolveFormula supports the following functions:
430
431 `Central(arg1)`: Use the central value of arg1, and set errors to 0
432
433 `DownUpNominal(arg0, arg1, arg2)`: Produce a new AO where ydn is taken from
434 the central value ('y') of arg0, yup is taken from the central value of arg1,
435 and y is taken from the central value of arg2.
436
437 `Envelope(arg1,..., argN)`: Take the min/max envelope of the arguments as the
438 up/down errors of the resulting AO. The central value is taken from 'nominal'.
439
440 `Inverse(arg1)`: Take the inverse of the AO, so y--> 1/y, and the errors are
441 propagated as yerr --> |yerr/y **2|
442
443 `Product(arg1,..., argN)`: Take the product of the arguments of the function.
444 The 'y' value is the product of the 'y' values of the arguments, the errors
445 are propagated according the relative errors in quadrature.
446
447
448 `Average(arg1,..., argN)`: Take the product of the arguments of the function.
449 The 'y' value is the avergae of the 'y' values of the arguments, the errors
450 are taken from the first argument.
451
452 `QuadSum(arg1,.., argN)`: Sum in quadrature of the errors of the arguments,
453 and central value taken from nominal AO.
454
455 `Value(arg1)`: Used to scan an AO and its errors by the float specified in arg1.
456
457 `CopyError(arg1, arg2)`: Copy the up/down errors.
458 """
459 # for debug statements, prepare a prefix which is proportional to the level
460 # of iteration
461 prefix = ""
462 y, yup, ydn = 'y', 'yup', 'ydn'
463 if 'z' in nominal.keys():
464 y, yup, ydn = 'z', 'zup', 'zdn'
465 for i in range(level): prefix += "-->"
466 if (verbose): print(prefix + " processing " + formula)
467
468 # if there is no bracket in the formula, this means we've reached a
469 # basic component of the formula. In this case just return the relevant AO!
470 if ")" not in formula:
471 result = None
472
473 # now find the relevant AO from the name of the compoenent
474 for k, v in componentsMap.items():
475 componentName = k.split("/")[-1].replace(".yoda", "").replace(".root", "")
476 if componentName == safeFileName(formula):
477 result = v
478 if result is None:
479 if componentName.split(":")[0] == safeFileName(formula):
480 result = v
481 # print(error and quit if no matching AO is found!)
482 if result is None:
483 print("[ERROR] could not find a match for " + formula)
484 print(" (%s) " % safeFileName(formula) + " in list of available weights:")
485 for k, v in componentsMap.items():
486 print(k.split("/")[-1])
487 exit(1)
488
489 # if all went well, return the AO
490 if (verbose): print(prefix + "--> result:" + result)
491 return result
492
493 # If there are brackets in the formula then it either needs to be evaluated
494 # of brocket down further
495 else:
496
497 # get the operation and the operand of the formula
498 operation = formula.partition("(")[0]
499 operand = formula.partition("(")[-1].rpartition(")")[0]
500 arguments = []
501 argumentNames = []
502
503 # Split the operand into chunks (arguments). If the chunk is itself a
504 # a formula, it will need to be evluated first
505 if operation == "Value":
506 arguments = [float(operand)]
507 else:
508 for argument in splitOperand(operand):
509
510 # evaluate each argument, recursively
511 arg = resolveFormula(nominal, argument, componentsMap, level + 1)
512 argumentNames += [argument]
513 arguments += [arg]
514 if (verbose): print(prefix + "argument of" + operation + ": " + arg)
515
516 # Now that we have evluated the arguments, we can combine them according to
517 # the specified operation.
518 result = None
519
520 # Perform a sum in quadrature of the errors of the arguments, and the
521 # centreak value is taken from the nominal
522 if operation == "QuadSum":
523 result = None
524 for argument in arguments:
525 if result is None: result = copy.deepcopy(argument)
526 else:
527 if not (np.allclose(nominal[y], argument[y])):
528 print("[ERROR] trying to take the quadratic sum of uncertainties which don't have the same central value!")
529 print("If you are using an alternative PDF directly int his function, please take the difference with the nominal and use that instead!")
530 exit(1)
531 result[yup] = nominal[y] + np.sqrt((argument[yup] - nominal[y]) ** 2 + (result[yup] - nominal[y]) ** 2)
532 result[ydn] = nominal[y] - np.sqrt((argument[ydn] - nominal[y]) ** 2 + (result[ydn] - nominal[y]) ** 2)
533
534 # Get the central value of this AO, setting the errors to 0
535 elif operation == "Central":
536 result = copy.deepcopy(arguments[0])
537 result[yup] = result[y]
538 result[ydn] = result[y]
539
540 # Produce a new AO where ydn is taken from the central value ('y') of arg0,
541 # yup is taken from the central value of arg1, and y is taken from the
542 # central value of arg2.
543 elif operation == "DownUpNominal":
544 if len(arguments) != 3:
545 print("[ERROR] DownUpNominal() must take exactly three arguments, found " + len(arguments))
546 exit(1)
547 result = copy.deepcopy(arguments[2])
548 result[yup] = copy.deepcopy(arguments[1][y])
549 result[ydn] = copy.deepcopy(arguments[0][y])
550
551 # Take the product of the arguments of the function. The 'y' value is the
552 # product of the 'y' values of the arguments, the errors are propagated
553 # according the relative errors in quadrature.
554 elif operation == "Product":
555 result = None
556 for arg in arguments:
557 if result is None: result = copy.deepcopy(arg)
558 else:
559 centralProduct = result[y] * arg[y]
560 upError = centralProduct * np.sqrt((safeDiv(result[y] - result[yup], result[y])) ** 2 + (safeDiv(arg[y] - arg[yup], arg[y])) ** 2)
561 dnError = centralProduct * np.sqrt((safeDiv(result[y] - result[ydn], result[y])) ** 2 + (safeDiv(arg[y] - arg[ydn], arg[y])) ** 2)
562 result[y] = centralProduct
563 result[yup] = centralProduct + upError
564 result[ydn] = centralProduct - dnError
565 elif operation == "Average":
566 result = None
567 N = len(arguments)
568 for arg in arguments:
569 if result is None: result = copy.deepcopy(arg)
570 else:
571 result[y] = result[y] + arg[y]
572 result[y] = result[y] / N
573
574 # Take the min/max envelope of the arguments as the up/down errors of the
575 # resulting AO. The central value is taken from 'nominal'
576 elif operation == "Envelope":
577 argumentDict = {}
578 for i in range(len(arguments)):
579 argumentDict[argumentNames[i]] = arguments[i]
580 cen, dn, up = combineVariationsEnvelope(nominal, argumentDict, asym=True, includeErrorsInEnvelope=True)
581 result = copy.deepcopy(nominal)
582 result[y] = cen
583 result[yup] = up
584 result[ydn] = dn
585 elif operation == "EnvelopeIgnoreErrors":
586 argumentDict = {}
587 for i in range(len(arguments)):
588 argumentDict[argumentNames[i]] = arguments[i]
589 cen, dn, up = combineVariationsEnvelope(nominal, argumentDict, asym=True, includeErrorsInEnvelope=False)
590 result = copy.deepcopy(nominal)
591 result[y] = cen
592 result[yup] = up
593 result[ydn] = dn
594
595 # Take the inverse of the AO, so y--> 1/ y, and the errors are propagated
596 # as yerr --> |yerr/ y ** 2|
597 elif operation == "Inverse":
598 if len(arguments) != 1:
599 print("[ERROR] Inverse() must take exactly 1 argument, found " + len(arguments))
600 exit(1)
601 arg = arguments[0]
602 result = copy.deepcopy(arg)
603 result[y] = safeDiv(np.ones(len(arg[y])), arg[y])
604 result[yup] = result[y] + np.abs(safeDiv(arg[y] - arg[yup], arg[y] ** 2))
605 result[ydn] = result[y] - np.abs(safeDiv(arg[y] - arg[ydn], arg[y] ** 2))
606
607 elif operation == "Value":
608 if len(arguments) != 1:
609 print("[ERROR] Value() must take exactly 1 argument, found " + len(arguments))
610 exit(1)
611 arg = arguments[0]
612 result = copy.deepcopy(nominal)
613 result[y] = arg * np.ones(len(result[y]))
614 result[yup] = arg * np.zeros(len(result[y]))
615 result[ydn] = arg * np.zeros(len(result[y]))
616
617 # Anything else needs to be implemented here:)
618 # elif operation == "FooBar":...
619 else:
620 print("[ERROR] this operation" + operation + " is not yet supported! Please implement it in resolveFormula() before continuing")
621 exit(1)
622
623 # return the result of the operation on the arguments
624 return result
625
626

◆ rivetINameReplacements()

systematicsTool.rivetINameReplacements ( name,
escapePlus = False )

Definition at line 171 of file systematicsTool.py.

171def rivetINameReplacements(name, escapePlus=False):
172 reps = OrderedDict()
173 reps[" nominal "] = ""
174 reps[" set = "] = "_"
175 reps[" = "] = "_"
176 reps["="] = ""
177 if escapePlus: reps["+"] = r"\+"
178 reps[","] = ""
179 reps["."] = ""
180 reps[":"] = ""
181 reps[" "] = "_"
182 reps["#"] = "num"
183 reps["\n"] = "_"
184 reps["/"] = "over"
185 for find, rep in reps.items():
186 name = name.replace(find, rep)
187 return name
188
189

◆ safeDiv()

systematicsTool.safeDiv ( numerator,
denominator )
`numerator` np.array()
`denominator` np.array()
`return` np.array

Old:Avoid NaNs when doing division. Replaces NaN with 0 in the array.
New:Does the division 'where' denominator does not equal zero. When denominator is zero for an indice, 0 is put in the array

Definition at line 335 of file systematicsTool.py.

335def safeDiv(numerator, denominator):
336 """
337 `numerator` np.array()
338 `denominator` np.array()
339 `return` np.array
340
341 Old:Avoid NaNs when doing division. Replaces NaN with 0 in the array.
342 New:Does the division 'where' denominator does not equal zero. When denominator is zero for an indice, 0 is put in the array
343 """
344 ratio = np.divide(numerator, denominator, out=np.zeros_like(numerator), where=denominator != 0)
345 return ratio
346
347

◆ safeFileName()

systematicsTool.safeFileName ( name,
removeExtension = True,
reverse = False )
`name` String
`removeExtension` Bool [optional] (remove the file extension, eg .root, or .yoda)
`return` String

Converts an input string (eg a weight name) which may contain non-standard
characters into a string which is safe to use as a filename.
In particular, the following substitutions are made:
'.' --> 'p'
' ' --> '_'
':' --> '_'

Definition at line 143 of file systematicsTool.py.

143def safeFileName(name, removeExtension=True, reverse=False):
144 """
145 `name` String
146 `removeExtension` Bool [optional] (remove the file extension, eg .root, or .yoda)
147 `return` String
148
149 Converts an input string (eg a weight name) which may contain non-standard
150 characters into a string which is safe to use as a filename.
151 In particular, the following substitutions are made:
152 '.' --> 'p'
153 ' ' --> '_'
154 ':' --> '_'
155 """
156 name = name.replace("user.", "userp")
157 name = name.replace(".yoda", "pyoda")
158 name = name.replace("yoda.", "yodap")
159 name = name.replace(".root", "proot")
160 name = rivetINameReplacements(name)
161 name = name.replace(r"\+", "")
162 name = name.replace("userp", "user.")
163 name = name.replace("pyoda", ".yoda")
164 name = name.replace("yodap", "yoda.")
165 name = name.replace("proot", ".root")
166 if (removeExtension and ".root" in name): name = name.rpartition(".root")[0]
167 if (removeExtension and ".yoda" in name): name = name.rpartition(".yoda")[0]
168 return name
169
170

◆ safeRootLatex()

systematicsTool.safeRootLatex ( unsafeLatex)
`unsafeLatex` String (unsafe Latex string to be converted)
`return` String (safe TLatex string which can be used on ROOT plots)

TLatex is not quite the same as regular latex, and won't compile properly
out of the box unless a few changes are made. This function does that
hopefully in the majority of cases! No promises though... *sigh*

Definition at line 1350 of file systematicsTool.py.

1350def safeRootLatex(unsafeLatex):
1351 """
1352 `unsafeLatex` String (unsafe Latex string to be converted)
1353 `return` String (safe TLatex string which can be used on ROOT plots)
1354
1355 TLatex is not quite the same as regular latex, and won't compile properly
1356 out of the box unless a few changes are made. This function does that
1357 hopefully in the majority of cases! No promises though... *sigh*
1358 """
1359 unsafeLatex = unsafeLatex.replace("$", "")
1360 unsafeLatex = unsafeLatex.replace("\\", "#")
1361 unsafeLatex = unsafeLatex.replace("^#text", "^")
1362 while "#text" in unsafeLatex:
1363 operand = unsafeLatex.partition("#text{")[-1].partition("}")[0]
1364 unsafeLatex = unsafeLatex.replace("#text{%s}" % operand, operand)
1365 while "#mathrm" in unsafeLatex:
1366 operand = unsafeLatex.partition("#mathrm{")[-1].partition("}")[0]
1367 unsafeLatex = unsafeLatex.replace("#mathrm{%s}" % operand, operand)
1368 while "#dfrac" in unsafeLatex:
1369 unsafeLatex = unsafeLatex.replace("}{", " / ")
1370 right = unsafeLatex.partition("#dfrac{")[-1]
1371 text = []
1372 bracketLeunsafeLatexel = 0
1373 for item in right:
1374 if item == "{": bracketLeunsafeLatexel += 1
1375 if item == "}":
1376 if bracketLeunsafeLatexel == 0:
1377 break
1378 bracketLeunsafeLatexel -= 1
1379 text += [item]
1380 text = "".join(text)
1381 unsafeLatex = unsafeLatex.replace("#dfrac{" + text + "}", text)
1382
1383 missingBrackets = re.findall(r"\^.", unsafeLatex)
1384 for mb in missingBrackets:
1385 if "{" in mb: continue
1386 if "#" in mb: continue
1387 unsafeLatex = unsafeLatex.replace(mb, mb.replace("^", "^{") + "}")
1388 missingBrackets = re.findall(r"\_.", unsafeLatex)
1389 for mb in missingBrackets:
1390 if "{" in mb: continue
1391 if "#" in mb: continue
1392 unsafeLatex = unsafeLatex.replace(mb, mb.replace("_", "_{") + "}")
1393 if "#ge" in unsafeLatex and "#geq" not in unsafeLatex:
1394 unsafeLatex = unsafeLatex.replace("#ge", "#geq")
1395 unsafeLatex = unsafeLatex.replace("#ell", "l") # not supported in TLatex
1396 unsafeLatex = unsafeLatex.replace("#cos", "cos ") # not supported in TLatex
1397 unsafeLatex = unsafeLatex.replace("#sin", "sin ") # not supported in TLatex
1398
1399 return unsafeLatex
1400
1401

◆ splitOperand()

systematicsTool.splitOperand ( operand,
bracket = "()" )
`operand` String (The operand which you want to decompose into chunks)
`bracket` String [optional] (The type of bracket you want to avoid splitting,
eg (), {}, []...)
`return` list(String)

Splits the operand of a formula into comma-separated chunks without splitting
operands of nested functions.
eg: Function1(foo, Function2(bar, foo), Function3(foo, Function4 (bar, foo)))
--> [foo, Function2(bar, foo), Function3(foo, Function4 (bar, foo))]

Definition at line 348 of file systematicsTool.py.

348def splitOperand(operand, bracket="()"):
349 """
350 `operand` String (The operand which you want to decompose into chunks)
351 `bracket` String [optional] (The type of bracket you want to avoid splitting,
352 eg (), {}, []...)
353 `return` list(String)
354
355 Splits the operand of a formula into comma-separated chunks without splitting
356 operands of nested functions.
357 eg: Function1(foo, Function2(bar, foo), Function3(foo, Function4 (bar, foo)))
358 --> [foo, Function2(bar, foo), Function3(foo, Function4 (bar, foo))]
359 """
360 parts = []
361 bracketLevel = 0
362 current = []
363 isQuote = False
364 # trick to remove special-case of trailing chars
365 for c in (operand + ","):
366 if ((c == "," and not isQuote) or c == bracket[1]) and bracketLevel == 0:
367 parts.append("".join(current).strip('"'))
368 current = []
369 else:
370 if c == bracket[0]:
371 bracketLevel += 1
372 current.append(c)
373 elif c == bracket[1]:
374 bracketLevel -= 1
375 current.append(c)
376 elif c == '"':
377 isQuote = not isQuote
378 current.append(c)
379 elif c == ' ':
380 if isQuote: current.append(c)
381 else:
382 current.append(c)
383
384 return parts
385
386

◆ updateProgress()

systematicsTool.updateProgress ( progress,
key,
message )
`progress` dict (a dictionary of the job labels:status)
`key` String (the job label to update)
`message` String (the status to update)
`return` Void

This is a helper function to update and print(a progress update)
when multi-threading

Definition at line 2207 of file systematicsTool.py.

2207def updateProgress(progress, key, message):
2208 """
2209 `progress` dict (a dictionary of the job labels:status)
2210 `key` String (the job label to update)
2211 `message` String (the status to update)
2212 `return` Void
2213
2214 This is a helper function to update and print(a progress update)
2215 when multi-threading
2216 """
2217 progress[key] = message
2218 printProgress(progress)
2219
2220

◆ weightCorrection()

systematicsTool.weightCorrection ( var,
nom,
sampleDir = "",
varWeightName = "",
nominalWeightName = "",
sumwHistName = "" )
`var` String (name of YODA file for variation to get the correction for)
`nom` String (name of YODA file for nominal to get correct away from)
`sampleDir` String [optional] (directory path for a given subsample)
`varWeightName` String [optional] (name of the variation to get weight correction for)
`nominalWeightName` String [optional] (name of the nominal variation to get weight correction with respect to)
`sumwHistName` String [optional] (name of the sum-of-weights TH1 in the case of root files)
`return` Float

Computes the on-the-fly weight correction of the systematic variation with
respect to the nominal. This is needed for YODA files because by default,
Rivet normalises by the sum of weights for a given instance (ie variation)
rather than by the nominal. This weight is used to fix the issue.

Definition at line 223 of file systematicsTool.py.

223def weightCorrection(var, nom, sampleDir="", varWeightName="", nominalWeightName="", sumwHistName=""):
224 """
225 `var` String (name of YODA file for variation to get the correction for)
226 `nom` String (name of YODA file for nominal to get correct away from)
227 `sampleDir` String [optional] (directory path for a given subsample)
228 `varWeightName` String [optional] (name of the variation to get weight correction for)
229 `nominalWeightName` String [optional] (name of the nominal variation to get weight correction with respect to)
230 `sumwHistName` String [optional] (name of the sum-of-weights TH1 in the case of root files)
231 `return` Float
232
233 Computes the on-the-fly weight correction of the systematic variation with
234 respect to the nominal. This is needed for YODA files because by default,
235 Rivet normalises by the sum of weights for a given instance (ie variation)
236 rather than by the nominal. This weight is used to fix the issue.
237 """
238 w_var = 1.
239 w_nom = 1.
240 # Construct the filenames for variation and nominal
241 fn = {}
242 fn['var'] = '%s' % (var)
243 fn['nom'] = '%s' % (nom)
244 for k, v in fn.items():
245 if not os.path.isfile(fn[k]):
246 fn[k] = fn[k] + '.yoda'
247 if not os.path.isfile(fn[k]):
248 fn[k] = fn[k].replace('.yoda', '.root')
249 if not os.path.isfile(fn[k]):
250 print("\n[ERROR] Could not open the file:" + repr(fn[k]))
251 print("[ERROR] Available files in " + repr(sampleDir))
252 for varFile in os.listdir(sampleDir):
253 print(varFile)
254 exit(1)
255 # get the sum of weights for nom and var, using _EVTCOUNT
256 w_var = getSumOfWeights(fn['var'], varWeightName, sumwHistName)
257 w_nom = getSumOfWeights(fn['nom'], nominalWeightName, sumwHistName)
258 return w_var / w_nom
259
260

◆ writeToFile()

systematicsTool.writeToFile ( histDict,
fOut )
`histDict` dict{String, AnalysisObject} (AOs to write out)
`fOut` String (output file to write to)
`return` None

Write AOs out to a file, auto-determines root or yoda from file extension of fOut.

Definition at line 1035 of file systematicsTool.py.

1035def writeToFile(histDict, fOut):
1036 """
1037 `histDict` dict{String, AnalysisObject} (AOs to write out)
1038 `fOut` String (output file to write to)
1039 `return` None
1040
1041 Write AOs out to a file, auto-determines root or yoda from file extension of fOut.
1042 """
1043 rootExtensions = ['.root', '.root1', '.root2']
1044 yodaExtensions = ['.yoda', '.yoda1', '.yoda.gz']
1045 for rex in rootExtensions:
1046 if fOut.endswith(rex):
1047 writeToROOT(histDict, fOut)
1048 return
1049 for ye in yodaExtensions:
1050 if fOut.endswith(ye):
1051 writeToYODA(histDict, fOut)
1052 return
1053 print("[ERROR] could not identify input file type to write to ! Assuming YODA.")
1054 writeToYODA(histDict, fOut)
1055 return
1056
1057

◆ writeToROOT()

systematicsTool.writeToROOT ( histDict,
fOut )
`histDict` dict{String, AnalysisObject} (AOs to write out)
`fOut` String (output file to write to)
`return` None

Write AOs out to a ROOT file.

Definition at line 1084 of file systematicsTool.py.

1084def writeToROOT(histDict, fOut):
1085 """
1086 `histDict` dict{String, AnalysisObject} (AOs to write out)
1087 `fOut` String (output file to write to)
1088 `return` None
1089
1090 Write AOs out to a ROOT file.
1091 """
1092 f = r.TFile.Open(fOut, "RECREATE")
1093 for aoKey, inAO in histDict.items():
1094 if 'z' not in inAO.keys(): # no 'z' filled for 1D graphs and TH1Ds
1095 outAO = r.TGraphAsymmErrors()
1096 outAO.SetName(aoKey)
1097 outAO.SetTitle(aoKey)
1098 if len(inAO['xdn']): binningInfo = [inAO['xdn'][0]]
1099 else: continue
1100 for xup in inAO['xup']: binningInfo += [xup]
1101 binArray = array('d', binningInfo)
1102 outAO_up = r.TH1D(aoKey + "__1up", aoKey + "__1up", len(binArray) - 1, binArray)
1103 outAO_down = r.TH1D(aoKey + "__1down", aoKey + "__1down", len(binArray) - 1, binArray)
1104 outAO.SetName(aoKey)
1105 outAO.SetTitle(aoKey)
1106 for i in range(inAO['nbins']):
1107 outAO.SetPoint(i, inAO['x'][i], inAO['y'][i])
1108 outAO.SetPointError(i, inAO['x'][i] - inAO['xdn'][i], inAO['xup'][i] - inAO['x'][i], abs(inAO['y'][i] - inAO['ydn'][i]), abs(inAO['yup'][i] - inAO['y'][i]))
1109 outAO_up.SetBinContent(i + 1, inAO['yup'][i])
1110 outAO_up.SetBinError(i + 1, 0.)
1111 outAO_down.SetBinContent(i + 1, inAO['ydn'][i])
1112 outAO_down.SetBinError(i + 1, 0.)
1113 outAO.Write()
1114 outAO_up.Write()
1115 outAO_down.Write()
1116
1117 else:
1118 xbins = []
1119 ybins = []
1120 for y in inAO['ydn']:
1121 while y not in ybins:
1122 ybins.append(y)
1123 for x in inAO['xdn']:
1124 while x not in xbins:
1125 xbins.append(x)
1126 xbins.append(inAO['xup'][np.where(inAO['xdn'] == x)][0])
1127 ybins.append(inAO['yup'][np.where(inAO['ydn'] == y)][0])
1128 xbinArray = array('d', xbins)
1129 ybinArray = array('d', ybins)
1130 outAO = r.TH2D(aoKey, "histo", len(xbinArray) - 1, xbinArray, len(ybinArray) - 1, ybinArray)
1131 outAO.Sumw2()
1132 outAO.SetName(aoKey)
1133 outAO.SetTitle(aoKey)
1134 for iBin in range(len(inAO['z'])):
1135 ix = inAO['x'][iBin]
1136 iy = inAO['y'][iBin]
1137 xybin = outAO.GetBin(outAO.GetXaxis().FindBin(ix), outAO.GetYaxis().FindBin(iy))
1138 outAO.SetBinContent(xybin, inAO['z'][iBin])
1139 outAO.SetBinError(xybin, abs(inAO['z'][iBin] - inAO['zup'][iBin]))
1140 outAO.Write()
1141 f.Close()
1142
1143
STL class.

◆ writeToYODA()

systematicsTool.writeToYODA ( histDict,
fOut )
`histDict` dict{String, AnalysisObject} (AOs to write out)
`fOut` String (output file to write to)
`return` None

Write AOs out to a YODA file.

Definition at line 1058 of file systematicsTool.py.

1058def writeToYODA(histDict, fOut):
1059 """
1060 `histDict` dict{String, AnalysisObject} (AOs to write out)
1061 `fOut` String (output file to write to)
1062 `return` None
1063
1064 Write AOs out to a YODA file.
1065 """
1066 f = open(fOut, 'w')
1067 for aoKey, inAO in histDict.items():
1068 if 'z' in inAO.keys(): continue # TODO... for now, need to rewrite this function for 2D histos
1069 outAO = yoda.Scatter2D()
1070 for k, v in inAO['annotations'].items():
1071 if k == "Type": continue
1072 outAO.setAnnotation(k, v)
1073 for i in range(inAO['nbins']):
1074 p = yoda.Point2D()
1075 p.setX(inAO['x'][i])
1076 p.setY(inAO['y'][i])
1077 p.setErrs(1, (abs(inAO['xdn'][i] - inAO['x'][i]), abs(inAO['xup'][i] - inAO['x'][i])))
1078 p.setErrs(2, (abs(inAO['ydn'][i] - inAO['y'][i]), abs(inAO['yup'][i] - inAO['y'][i])))
1079 outAO.addPoint(p)
1080 yoda.writeYODA(outAO, f)
1081 f.close()
1082
1083