#!/usr/bin/env python3
"""
.. module:: coverage
:synopsis: Definitions of classes used to find, format missing topologies
.. moduleauthor:: Ursula Laa <ursula.laa@lpsc.in2p3.fr>
.. moduleauthor:: Suchita Kulkarni <suchita.kulkarni@gmail.com>
"""
import copy
from smodels.tools.physicsUnits import fb
from smodels.theory.element import Element
[docs]class Uncovered(object):
"""
Object collecting all information of non-tested/covered elements
:ivar topoList: sms topology list
:ivar sumL: if true, sum up electron and muon to lepton, for missing topos
:ivar sumJet: if true, sum up jets, for missing topos
:ivar sqrts: Center of mass energy. If defined it will only consider cross-sections
for this value. Otherwise the highest sqrts value will be used.
"""
def __init__(self, topoList, sumL=True, sumJet=True, sqrts=None):
if sqrts is None:
self.sqrts = max([xsec.info.sqrts for xsec in topoList.getTotalWeight()])
else:
self.sqrts = sqrts
self.missingTopos = UncoveredList(sumL, sumJet, self.sqrts)
self.outsideGrid = UncoveredList(sumL, sumJet, self.sqrts) # FIXME change this to derived objects for printout
self.longCascade = UncoveredClassifier()
self.asymmetricBranches = UncoveredClassifier()
self.motherIDs = []
self.prevMothers = []
self.outsideGridMothers = []
self.uncompressedEls = []
self.getAllMothers(topoList)
self.fill(topoList)
self.asymmetricBranches.combine()
self.longCascade.combine()
[docs] def getAllMothers(self, topoList):
"""
Find all IDs of mother elements, only most compressed element can be missing topology
:ivar topoList: sms topology list
"""
for el in topoList.getElements():
for mEl in el.motherElements:
motherID = mEl[-1].elID
if not motherID in self.motherIDs: self.motherIDs.append(motherID)
[docs] def fill(self, topoList):
"""
Check all elements, categorise those not tested / missing, classify long cascade decays and asymmetric branches
Fills all corresponding objects
:ivar topoList: sms topology list
"""
for el in topoList.getElements(): # loop over all elements, by construction we start with the most compressed
if not el.motherElements:
self.uncompressedEls.append(el) #Store all compressed elements
if self.inPrevMothers(el):
missing = False # cannot be missing if element with same mothers has already appeared
# this is because it can certainly be compressed further to the smaller element already seen in the loop
else: #if not the case, we add mothers to previous mothers and test if the topology is missin
self.addPrevMothers(el)
missing = self.isMissingTopo(el) #missing topo only if not covered, and counting only weights of non-covered mothers
# in addition, mother elements cannot be missing, we consider only the most compressed one
if not missing: # any element that is not missing might be outside the grid
# outside grid should be smalles covered but not tested in compression
if el.covered and not el.tested: # verify first that element is covered but not tested
if not el.weight.getXsecsFor(self.sqrts): continue # remove elements that only have weight at higher sqrts
if self.inOutsideGridMothers(el): continue # if daughter element of current element is already counted skip this element
# in this way we do not double count, but always count the smallest compression that is outside the grid
outsideX = self.getOutsideX(el) # as for missing topo, recursively find untested cross section
if outsideX: # if all mothers are tested, this is no longer outside grid contribution, otherwise add to list
el.missingX = outsideX # for combined printing function, call outside grid weight missingX as well
self.outsideGrid.addToTopos(el) # add to list of outsideGrid topos
continue
self.missingTopos.addToTopos(el) #keep track of all missing topologies
if self.hasLongCascade(el): self.longCascade.addToClasses(el)
elif self.hasAsymmetricBranches(el): self.asymmetricBranches.addToClasses(el) # if no long cascade, check for asymmetric branches
[docs] def inPrevMothers(self, el): #check if smaller element with same mother has already been checked
for mEl in el.motherElements:
if mEl[-1].elID in self.prevMothers: return True
return False
[docs] def inOutsideGridMothers(self, el): #check if this element or smaller element with same mother has already been checked
if el.elID in self.outsideGridMothers: return True
for mEl in el.motherElements:
if mEl[-1].elID in self.outsideGridMothers: return True
return False
[docs] def addPrevMothers(self, el): #add mother elements of currently tested element to previous mothers
for mEl in el.motherElements:
self.prevMothers.append(mEl[-1].elID)
[docs] def hasLongCascade(self, el):
"""
Return True if element has more than 3 particles in the decay chain
:ivar el: Element
"""
if el._getLength() > 3: return True
return False
[docs] def hasAsymmetricBranches(self, el):
"""
Return True if Element branches are not equal
:ivar el: Element
"""
if el.branches[0] == el.branches[1]: return False
return True
[docs] def isMissingTopo(self, el):
"""
A missing topology is not a mother element, not covered, and does not have mother which is covered
:ivar el: Element
"""
if el.elID in self.motherIDs: return False # mother element can not be missing
if el.covered: return False # covered = not missing
missingX = self.getMissingX(el) # find total missing cross section by checking if mothers are covered
if not missingX: return False # if all mothers covered, element is not missing
el.missingX = missingX # missing cross section is found by adding up cross section of mothers not covered
return True
[docs] def getMissingX(self,el):
"""
Calculate total missing cross section of element, by recursively checking if mothers are covered
:ivar el: Element
:returns: missing cross section in fb as number
"""
mothers = el.motherElements
alreadyChecked = [] # for sanity check
# if element has no mothers, the full cross section is missing
if not el.weight.getXsecsFor(self.sqrts): return 0.
missingX = el.weight.getXsecsFor(self.sqrts)[0].value.asNumber(fb)
if not mothers: return missingX
while mothers: # recursive loop to check all mothers
newmothers = []
for mother in mothers:
if mother[-1].elID in alreadyChecked: continue # sanity check, to avoid double counting
alreadyChecked.append(mother[-1].elID) # now checking, so adding to alreadyChecked
if mother[-1].covered:
if not mother[-1].weight.getXsecsFor(self.sqrts): continue
missingX -= mother[-1].weight.getXsecsFor(self.sqrts)[0].value.asNumber(fb)
continue # do not count cross section if mother is covered, do not continue recursion for this contribution
if not mother[-1].motherElements: continue # end of recursion if element has no mothers, we keep its cross section in missingX
else: newmothers += mother[-1].motherElements # if element has mother element, check also if those are covered before adding the cross section contribution
mothers = newmothers # all new mothers will be checked until we reached the end of all recursions
return missingX
[docs] def getOutsideX(self,el):
"""
Calculate total outside grid cross section of element, by recursively checking if mothers are covered
:ivar el: Element
:returns: missing cross section in fb as number
"""
#same as getMissingX, but we also keep track of the mother elements of outsideGrid contributions
# this is so we can find the smallest covered but not tested element in a chain of compressed elements
mothers = el.motherElements
alreadyChecked = []
if not el.weight.getXsecsFor(self.sqrts): return 0.
missingX = el.weight.getXsecsFor(self.sqrts)[0].value.asNumber(fb)
if not mothers: return missingX
while mothers:
newmothers = []
for mother in mothers:
if mother[-1].elID in alreadyChecked: continue # sanity check, to avoid double counting
alreadyChecked.append(mother[-1].elID) # now checking, so adding to alreadyChecked
if mother[-1].tested:
if not mother[-1].weight.getXsecsFor(self.sqrts): continue
missingX -= mother[-1].weight.getXsecsFor(self.sqrts)[0].value.asNumber(fb)
continue
self.outsideGridMothers.append(mother[-1].elID) # mother element is not tested, but should no longer be considered as outside grid, because we already count its contribution here
if not mother[-1].motherElements: continue
else: newmothers += mother[-1].motherElements
mothers = newmothers
return missingX
[docs] def getTotalXsec(self,sqrts=None):
"""
Calculate total cross-section from decomposition (excluding compressed elements)
:ivar sqrts: sqrts
"""
xsec = 0.
if not sqrts:
sqrts = self.sqrts
for el in self.uncompressedEls:
xsec += el.weight.getXsecsFor(sqrts).getMaxXsec().asNumber(fb)
return xsec
[docs] def getMissingXsec(self, sqrts=None):
"""
Calculate total missing topology cross section at sqrts. If no sqrts is given use self.sqrts
:ivar sqrts: sqrts
"""
xsec = 0.
if not sqrts: sqrts = self.sqrts
for topo in self.missingTopos.topos:
for el in topo.contributingElements:
xsec += el.missingX
return xsec
[docs] def getOutOfGridXsec(self, sqrts=None): #FIXME same as getMissingXsec but different object, should not be separate functions
xsec = 0.
if not sqrts: sqrts = self.sqrts
for topo in self.outsideGrid.topos:
for el in topo.contributingElements:
xsec += el.missingX
return xsec
[docs] def getLongCascadeXsec(self, sqrts=None):
xsec = 0.
if not sqrts: sqrts = self.sqrts
for uncovClass in self.longCascade.classes:
for el in uncovClass.contributingElements:
xsec += el.missingX
return xsec
[docs] def getAsymmetricXsec(self, sqrts=None):
xsec = 0.
if not sqrts: sqrts = self.sqrts
for uncovClass in self.asymmetricBranches.classes:
for el in uncovClass.contributingElements:
xsec += el.missingX
return xsec
[docs]class UncoveredClassifier(object):
"""
Object collecting elements with long cascade decays or asymmetric branches.
Objects are grouped according to the initially produced particle PID pair.
"""
def __init__(self):
self.classes = []
[docs] def addToClasses(self, el):
"""
Add Element in corresponding UncoveredClass, defined by mother PIDs.
If no corresponding class in self.classes, add new UncoveredClass
:ivar el: Element
"""
motherPIDs = self.getMotherPIDs(el)
for entry in self.classes:
if entry.add(motherPIDs, el): return
self.classes.append(UncoveredClass(motherPIDs, el))
[docs] def getMotherPIDs(self, el):
allPIDs = []
for pids in el.getMothers():
cPIDs = []
for pid in pids:
cPIDs.append(abs(pid))
cPIDs.sort()
if not cPIDs in allPIDs:
allPIDs.append(cPIDs)
allPIDs.sort()
return allPIDs
[docs] def combine(self):
for ecopy in copy.deepcopy(self.classes):
for e in self.classes:
if e.isSubset(ecopy):
e.combine(ecopy)
self.remove(ecopy)
[docs] def remove(self, cl):
"""
Remove element where mother pids match exactly
"""
for i, o in enumerate(self.classes):
if o.motherPIDs == cl.motherPIDs:
del self.classes[i]
break
[docs] def getSorted(self,sqrts):
"""
Returns list of UncoveredClass objects in self.classes, sorted by weight
:ivar sqrts: sqrts for weight lookup
"""
return sorted(self.classes, key=lambda x: x.getWeight(), reverse=True)
[docs]class UncoveredClass(object):
"""
Object collecting all elements contributing to the same uncovered class, defined by the mother PIDs.
:ivar motherPIDs: PID of initially produces particles, sorted and without charge information
:ivar el: Element
"""
def __init__(self, motherPIDs, el):
self.motherPIDs = motherPIDs # holds nested list of mother PIDs as given by element.getMothers
self.contributingElements = [el] # collect all contributing elements, to keep track of weights as well
[docs] def add(self, motherPIDs, el):
"""
Add Element to this UncoveredClass object if motherPIDs match and return True, else return False
:ivar motherPIDs: PID of initially produces particles, sorted and without charge information
:ivar el: Element
"""
if not motherPIDs == self.motherPIDs: return False
self.contributingElements.append(el)
return True
[docs] def combine(self, other):
for el in other.contributingElements:
self.contributingElements.append(el)
[docs] def getWeight(self):
"""
Calculate weight at sqrts
:ivar sqrts: sqrts
"""
xsec = 0.
for el in self.contributingElements:
xsec += el.missingX
return xsec
[docs] def isSubset(self, other):
"""
True if motherPIDs of others are subset of the motherPIDs of this UncoveredClass
"""
if len(other.motherPIDs) >= len(self.motherPIDs): return False
for mothers in other.motherPIDs:
if not mothers in self.motherPIDs: return False
return True
[docs]class UncoveredTopo(object):
"""
Object to describe one missing topology result / one topology outside the mass grid
:ivar topo: topology description
:ivar weights: weights dictionary
"""
def __init__(self, topo, contributingElements=[]):
self.topo = topo
self.contributingElements = contributingElements
self.value = 0. # weight for sqrts set in uncoveredList, only set this in printout
[docs]class UncoveredList(object):
"""
Object to find and collect UncoveredTopo objects, plus printout functionality
:ivar sumL: if true sum electrons and muons to leptons
:ivar sumJet: if true, sum up jets
:ivar sqrts: sqrts, for printout
"""
def __init__(self, sumL, sumJet, sqrts):
self.topos = []
self.sumL = sumL
self.sumJet = sumJet
self.sqrts = sqrts
[docs] def addToTopos(self, el):
"""
adds an element to the list of missing topologies
if the element contributes to a missing topology that is already
in the list, add weight to topology
:parameter el: element to be added
"""
#Create a new element with the general name in order to fix the branch ordering:
newEl = Element(self.generalName(str(el)),finalState=el.getFinalStates())
newEl.sortBranches()
name = str(newEl) + ' (%s)'%(str(newEl.getFinalStates()).replace('[','').replace(']',''))
name = name.replace("'","").replace(' ','')
for topo in self.topos:
if name == topo.topo:
topo.contributingElements.append(el)
return
self.topos.append(UncoveredTopo(name, [el]))
return
[docs] def generalName(self, instr):
"""
generalize by summing over charges
e, mu are combined to l
:parameter instr: element as string
:returns: string of generalized element
"""
# 180318 mat: BUG? #############################
from smodels.theory.particleNames import ptcDic
if self.sumL: exch = ["W", "l", "t", "ta"]
else: exch = ["W", "e", "mu", "t", "ta"]
if self.sumJet: exch.append("jet")
for pn in exch:
for on in ptcDic[pn]:
instr = instr.replace(on, pn).replace("hijetjets","higgs")
return instr