Skip to content

Commit

Permalink
class to put systematics in the datacard with linear interpolation in…
Browse files Browse the repository at this point in the history
… case that's needed

(it is for my case because it is very easy to get negative probability if you're not careful)
  • Loading branch information
hroskes committed Aug 19, 2019
1 parent fcaf67f commit d8862da
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 4 deletions.
121 changes: 119 additions & 2 deletions python/PhysicsModel.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from abc import ABCMeta, abstractmethod
import re
import collections, itertools, re

### Class that takes care of building a physics model by combining individual channels and processes together
### Things that it can do:
Expand Down Expand Up @@ -27,7 +27,7 @@ def preProcessNuisances(self,nuisances):
pass # do nothing by default
def tellAboutProcess(self,bin,process):
"tell the physics model to expect this process, in case it needs to know about all the processes"
"before it gets the yield and scale"
"before it gets the yield scale"
pass # do nothing by default
def getYieldScale(self,bin,process):
"Return the name of a RooAbsReal to scale this yield by or the two special values 1 and 0 (don't scale, and set to zero)"
Expand Down Expand Up @@ -802,6 +802,123 @@ def getHiggsSignalYieldScale(self,production,decay, energy):
return 'r_V_'+decay
raise RuntimeError, "Unknown production mode '%s'" % production

class LinearSystematicsByBin(PhysicsModelBase_NiceSubclasses):
def __init__(self):
self.__linearsystematics = []
self.__knownprocesses = collections.defaultdict(lambda: collections.defaultdict(lambda: []))
self.__processeswithsystematic = collections.defaultdict(lambda: collections.defaultdict(lambda: []))
self.__systematicsforprocess = collections.defaultdict(lambda: collections.defaultdict(lambda: []))
self.__checkedprocesses = False
super(LinearSystematicsByBin, self).__init__()

def processPhysicsOptions(self, physOptions):
processed = []

for po in physOptions:
if po.startswith("linearsystematic:"):
self.__linearsystematics += po.replace("linearsystematic:", "", 1).split(",")
processed.append(po)

for syst in self.__linearsystematics:
if syst.endswith("Up") or syst.endswith("Down"):
raise ValueError("systematic name ends with Up or Down: " + syst)
for syst1, syst2 in itertools.combinations(self.__linearsystematics, 2):
if syst1.endswith(syst2) or syst2.endswith(syst1):
raise ValueError("systematic names are too similar: {}, {}".format(syst1, syst2))

processed += super(LinearSystematicsByBin, self).processPhysicsOptions(physOptions)
return processed

def tellAboutProcess(self, bin, process):
assert not self.__checkedprocesses
for systematic, direction in itertools.product(self.__linearsystematics, ("Up", "Down")):
if process.endswith("_"+systematic+direction):
nominal = process.replace("_"+systematic+direction, "")
self.__knownprocesses[bin][systematic+direction].append(nominal)
break
else:
if process.endswith("Up") or process.endswith("Down"):
raise ValueError("process name ends with Up or Down but is not a known systematic: " + process)
self.__knownprocesses[bin]["nominal"].append(process)

super(LinearSystematicsByBin, self).tellAboutProcess(bin, process)

def __checkProcesses(self):
if self.__checkedprocesses: return
self.__checkedprocesses = True

for bin in self.__knownprocesses:
for process in self.__knownprocesses[bin]["nominal"]:
for systematic in self.__linearsystematics:
if process in self.__knownprocesses[bin][systematic+"Up"] and process in self.__knownprocesses[bin][systematic+"Down"]:
self.__processeswithsystematic[bin][systematic].append(process)
self.__systematicsforprocess[bin][process].append(systematic)
self.__knownprocesses[bin][systematic+"Up"].remove(process)
self.__knownprocesses[bin][systematic+"Down"].remove(process)
elif process in self.__knownprocesses[bin][systematic+"Up"] or process in self.__knownprocesses[bin][systematic+"Down"]:
raise ValueError("Found either {0}_{1}Up or {0}_{1}Down in bin {2}, but not both".format(process, systematic, bin))

for systematic, direction in itertools.product(self.__linearsystematics, ("Up", "Down")):
for process in self.__knownprocesses[bin][systematic+direction]:
raise ValueError("Found {0}_{1}{2} in bin {3}, but not {0}".format(process, systematic, direction, bin))

for bin, dct in self.__systematicsforprocess.iteritems():
for process, lst in dct.iteritems():
lst.sort()

import pprint
pprint.pprint({k: dict(v) for k, v in self.__processeswithsystematic.iteritems()})

def getPOIList(self):
return super(LinearSystematicsByBin, self).getPOIList()

def getYieldScale(self, bin, process):
self.__checkProcesses()

for systematic, direction in itertools.product(self.__linearsystematics, ("Up", "Down")):
if process.endswith("_"+systematic+direction):
nominal = process.replace("_"+systematic+direction, "")
assert nominal in self.__processeswithsystematic[bin][systematic], (bin, systematic, nominal)
break
else:
nominal = process
systematic = direction = None

nominalyield = super(LinearSystematicsByBin, self).getYieldScale(bin, nominal)
if nominalyield in ("0", 0): return nominalyield
if not self.__systematicsforprocess[bin][nominal]: assert systematic is None; return nominalyield

if systematic is None:
formula = "1 - " + " - ".join("abs(@{})".format(i) for i, syst in enumerate(self.__systematicsforprocess[bin][nominal]))
variables = self.__systematicsforprocess[bin][nominal][:]
append = "_with_systematics_" + "_".join(self.__systematicsforprocess[bin][nominal])

else:
append = "_" + systematic + direction
formula = {"Up": "@0 > 0 ? @0 : 0", "Down": "@0 < 0 ? -@0 : 0"}[direction]
variables = [systematic]

if nominalyield in ("1", 1):
name = "one" + append
else:
name = nominalyield + append
formula = "(" + formula + ") * @{}".format(len(variables))
variables.append(nominalyield)

if isinstance(nominalyield, str) and name.count(nominalyield) > 1:
print name
print formula
print variables
print bin
print nominal
print self.__systematicsforprocess[bin][nominal]
assert False

if not self.modelBuilder.out.function(name):
self.modelBuilder.doVar('expr::{}("{}", {})'.format(name, formula, ",".join(variables)))

return name

defaultModel = PhysicsModel()
multiSignalModel = MultiSignalModel()
strictSMLikeHiggs = StrictSMLikeHiggsModel()
Expand Down
6 changes: 4 additions & 2 deletions python/SpinZeroStructure.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import collections, itertools, math, re
import numpy as np
from HiggsAnalysis.CombinedLimit.PhysicsModel import CanTurnOffBkgModel, MultiSignalModel, PhysicsModelBase_NiceSubclasses
from HiggsAnalysis.CombinedLimit.PhysicsModel import CanTurnOffBkgModel, LinearSystematicsByBin, MultiSignalModel, PhysicsModelBase_NiceSubclasses

### This is the base python class to study the SpinZero structure

Expand Down Expand Up @@ -726,6 +726,8 @@ def getYieldScale(self,bin,process):

return result

class HZZAnomalousCouplingsFromHistograms_LinearSystematics(LinearSystematicsByBin, HZZAnomalousCouplingsFromHistograms): pass

spinZeroHiggs = SpinZeroHiggs()
multiSignalSpinZeroHiggs = MultiSignalSpinZeroHiggs()
hzzAnomalousCouplingsFromHistograms = HZZAnomalousCouplingsFromHistograms()
hzzAnomalousCouplingsFromHistograms = HZZAnomalousCouplingsFromHistograms_LinearSystematics()

0 comments on commit d8862da

Please sign in to comment.