for s in signalsPP:
    counters[s] = Counter('Hpp3l')
    counters[s].addProcess(s,sigMap[s],signal=True)

for s in signalsPPR:
    counters[s] = Counter('Hpp3l')
    counters[s].addProcess(s,sigMap[s],signal=True)

counters['data'] = Counter('Hpp3l')
counters['data'].addProcess('data',sigMap['data'])
for mode in modes:
    for mass in masses:
        logging.info('Producing datacard for {0} - {1} GeV'.format(mode,mass))
        results = {}
        limits = Limits()
    
        limits.addEra('Era13TeV2016')
        limits.addAnalysis('Hpp3l')
        limits.addAnalysis('Hpp3lAP')
        limits.addAnalysis('Hpp3lPP')
        limits.addAnalysis('Hpp3lPPR')
        
        recoChans = getRecoChans(mode)
        for reco in recoChans:
            limits.addChannel(reco)
            limits.addChannel(reco+'_SB')

        signalsAP = ['HppHm{0}GeV'.format(mass)]
        signalsPP = ['HppHmm{0}GeV'.format(mass)]
        signalsPPR = ['HppHmmR{0}GeV'.format(mass)]
    for proc in samples:
        hists += [histMap[proc]]
    hist = sumHists('obs',*hists)
    for b in range(hist.GetNbinsX()+1):
        val = int(hist.GetBinContent(b))
        if val<0: val = 0
        err = val**0.5
        hist.SetBinContent(b,val)
        #hist.SetBinError(b,err)
    histMap['data'] = hist
else:
    hist = getBinned('data')
    histMap['data'] = hist

# create limit object
limits = Limits(wsname)

limits.addEra('Run2016')
limits.addAnalysis('ThreePhoton')
limits.addChannel('ggg')

if doParametric:
    limits.addMH(*binning[1:])
    limits.addX(*binning[1:])

if doParametric:
    limits.addProcess('sig',signal=True)
    limits.addProcess('bg')
else:
    for signal in signals:
        limits.addProcess(signal,signal=True)
for s in signals:
    counters[s] = Counter('Hpp4l')
    counters[s].addProcess(s, sigMap[s], signal=True)

for s in signalsR:
    counters[s] = Counter('Hpp4l')
    counters[s].addProcess(s, sigMap[s], signal=True)

counters['data'] = Counter('Hpp4l')
counters['data'].addProcess('data', sigMap['data'])
for mode in modes:
    for mass in masses:
        logging.info('Producing datacard for {0} - {1} GeV'.format(mode, mass))
        results = {}
        limits = Limits()

        limits.addEra('Era13TeV2016')
        limits.addAnalysis('Hpp4l')

        recoChans = getRecoChans(mode)
        for reco in recoChans:
            limits.addChannel(reco)
            limits.addChannel(reco + '_SB')

        signals = ['HppHmm{0}GeV'.format(mass)]
        signalsR = ['HppHmmR{0}GeV'.format(mass)]
        for sig in signals + signalsR:
            limits.addProcess(sig, signal=True)

        for background in backgrounds:
Beispiel #4
0
for s in signalsPP:
    counters[s] = Counter('Hpp3l')
    counters[s].addProcess(s, sigMap[s], signal=True)

for s in signalsPPR:
    counters[s] = Counter('Hpp3l')
    counters[s].addProcess(s, sigMap[s], signal=True)

counters['data'] = Counter('Hpp3l')
counters['data'].addProcess('data', sigMap['data'])
for mode in modes:
    for mass in masses:
        logging.info('Producing datacard for {0} - {1} GeV'.format(mode, mass))
        results = {}
        limits = Limits()

        limits.addEra('13TeV80X')
        limits.addAnalysis('Hpp3l')
        limits.addAnalysis('Hpp3lAP')
        limits.addAnalysis('Hpp3lPP')
        limits.addAnalysis('Hpp3lPPR')

        recoChans = getRecoChans(mode)
        for reco in recoChans:
            limits.addChannel(reco)
            limits.addChannel(reco + '_SB')

        signalsAP = ['HppHm{0}GeV'.format(mass)]
        signalsPP = ['HppHmm{0}GeV'.format(mass)]
        signalsPPR = ['HppHmmR{0}GeV'.format(mass)]
    for proc in samples:
        hists += [histMap[proc]]
    hist = sumHists('obs', *hists)
    for b in range(hist.GetNbinsX() + 1):
        val = int(hist.GetBinContent(b))
        if val < 0: val = 0
        err = val**0.5
        hist.SetBinContent(b, val)
        #hist.SetBinError(b,err)
    histMap['data'] = hist
else:
    hist = getBinned('data')
    histMap['data'] = hist

# create limit object
limits = Limits(wsname)

limits.addEra('Run2016')
limits.addAnalysis('ThreePhoton')
limits.addChannel('ggg')

if doParametric:
    limits.addMH(*binning[1:])
    limits.addX(*binning[1:])

if doParametric:
    limits.addProcess('sig', signal=True)
    limits.addProcess('bg')
else:
    for signal in signals:
        limits.addProcess(signal, signal=True)
def create_datacard(args):
    doMatrix = False
    doParametric = args.parametric
    doUnbinned = args.unbinned
    do2D = len(args.fitVars) == 2
    blind = not args.unblind
    addSignal = args.addSignal
    signalParams = {'h': args.higgs, 'a': args.pseudoscalar}
    wsname = 'w'
    var = args.fitVars

    if do2D and doParametric:
        logging.error('Parametric 2D fits are not yet supported')
        raise

    if doUnbinned and not doParametric:
        logging.error('Unbinned only supported with parametric option')
        raise

    #############
    ### Setup ###
    #############
    sampleMap = getSampleMap()

    backgrounds = ['datadriven']
    data = ['data']

    signals = [signame.format(h=h, a=a) for h in hmasses for a in amasses]
    signalToAdd = signame.format(**signalParams)
    signalSplines = [splinename.format(h=h) for h in hmasses]

    wrappers = {}
    for proc in backgrounds + signals + data:
        if proc == 'datadriven': continue
        for sample in sampleMap[proc]:
            wrappers[sample] = NtupleWrapper('MuMuTauTau',
                                             sample,
                                             new=True,
                                             version='80X')
            for shift in shifts:
                wrappers[sample + shift] = NtupleWrapper('MuMuTauTau',
                                                         sample,
                                                         new=True,
                                                         version='80X',
                                                         shift=shift)

    ##############################
    ### Create/read histograms ###
    ##############################

    histMap = {}
    # The definitons of which regions match to which arguments
    # PP can take a fake rate datadriven estimate from PF, but PF can only take the observed values
    regionArgs = {
        'PP': {
            'region': 'A',
            'fakeRegion': 'B',
            'source': 'B',
            'sources': ['A', 'C'],
            'fakeSources': ['B', 'D'],
        },
        'PF': {
            'region': 'B',
            'sources': ['B', 'D'],
        },
    }
    for mode in ['PP', 'PF']:
        histMap[mode] = {}
        for shift in [''] + shifts:
            histMap[mode][shift] = {}
            for proc in backgrounds + signals:
                logging.info('Getting {} {}'.format(proc, shift))
                if proc == 'datadriven':
                    # TODO: unbinned, get the RooDataHist from flattenener first
                    if mode == 'PP':
                        if doMatrix:
                            histMap[mode][shift][
                                proc] = getMatrixDatadrivenHist(
                                    var=var,
                                    wrappers=wrappers,
                                    shift=shift,
                                    do2D=do2D,
                                    **regionArgs[mode])
                        else:
                            histMap[mode][shift][proc] = getDatadrivenHist(
                                var=var,
                                wrappers=wrappers,
                                shift=shift,
                                do2D=do2D,
                                **regionArgs[mode])
                    else:
                        if doMatrix:
                            histMap[mode][shift][proc] = getMatrixHist(
                                'data',
                                var=var,
                                wrappers=wrappers,
                                shift=shift,
                                do2D=do2D,
                                **regionArgs[mode])
                        else:
                            histMap[mode][shift][proc] = getHist(
                                'data',
                                var=var,
                                wrappers=wrappers,
                                shift=shift,
                                do2D=do2D,
                                **regionArgs[mode])
                else:
                    if doMatrix:
                        histMap[mode][shift][proc] = getMatrixHist(
                            proc,
                            var=var,
                            wrappers=wrappers,
                            shift=shift,
                            do2D=do2D,
                            **regionArgs[mode])
                    else:
                        histMap[mode][shift][proc] = getHist(
                            proc,
                            var=var,
                            wrappers=wrappers,
                            shift=shift,
                            do2D=do2D,
                            **regionArgs[mode])
                if do2D:
                    pass  # TODO, figure out how to rebin 2D
                else:
                    histMap[mode][shift][proc].Rebin(rebinning[var[0]])
            if shift: continue
            logging.info('Getting observed')
            if blind:
                samples = backgrounds
                if addSignal: samples = backgrounds + [signalToAdd]
                hists = []
                for proc in samples:
                    hists += [histMap[mode][shift][proc]]
                hist = sumHists('obs', *hists)
                #for b in range(hist.GetNbinsX()+1):
                #    val = int(hist.GetBinContent(b))
                #    if val<0: val = 0
                #    err = val**0.5
                #    hist.SetBinContent(b,val)
                #    #hist.SetBinError(b,err)
                histMap[mode][shift]['data'] = hist
            else:
                hist = getHist('data',
                               var=var,
                               wrappers=wrappers,
                               do2D=do2D,
                               **regionArgs[mode])
                histMap[mode][shift]['data'] = hist
                if do2D:
                    pass
                else:
                    histMap[mode][shift]['data'].Rebin(rebinning[var[0]])

    #####################
    ### Create Limits ###
    #####################
    limits = Limits(wsname)

    limits.addEra('Run2016')
    limits.addAnalysis('HAA')

    era = 'Run2016'
    analysis = 'HAA'
    reco = 'mmmt'

    for mode in ['PP', 'PF']:
        limits.addChannel(mode)
        if doParametric:
            binning = varBinning[var[0]]
            limits.addMH(*binning[1:])
            limits.addX(*binning[1:], unit='GeV', label='m_{#mu#mu}')
            for h in hmasses:
                limits.addProcess(splinename.format(h=h), signal=True)
            for background in backgrounds:
                limits.addProcess(background)

            # add models
            for h in hmasses:
                model = getSpline(histMap[mode][''], h, tag=mode)
                limits.setExpected(splinename.format(h=h), era, analysis, mode,
                                   model)

            if doUnbinned:
                bg = buildModel(limits, tag=mode)
                limits.setExpected('datadriven', era, analysis, mode, bg)
            else:
                # add histograms for background if not using an unbinned model
                for bg in backgrounds:
                    limits.setExpected(bg, era, analysis, mode,
                                       histMap[mode][''][bg])

            # get roodatahist
            limits.setObserved(era, analysis, mode, histMap[mode]['']['data'])

        else:

            for signal in signals:
                limits.addProcess(signal, signal=True)
            for background in backgrounds:
                limits.addProcess(background)

            for proc in backgrounds:
                limits.setExpected(proc, era, analysis, mode,
                                   histMap[mode][''][proc])
            for proc in signals:
                limits.setExpected(proc, era, analysis, mode,
                                   histMap[mode][''][proc])

            limits.setObserved(era, analysis, mode, histMap[mode]['']['data'])

    #########################
    ### Add uncertainties ###
    #########################

    systproc = tuple(
        [proc for proc in signals + backgrounds if 'datadriven' not in proc])
    allproc = tuple([proc for proc in signals + backgrounds])
    systsplineproc = tuple([
        proc for proc in signalSplines + backgrounds
        if 'datadriven' not in proc
    ])
    allsplineproc = tuple([proc for proc in signalSplines + backgrounds])
    bgproc = tuple([proc for proc in backgrounds])
    sigsplineproc = tuple([proc for proc in signalSplines])
    sigproc = tuple([proc for proc in signals])

    ############
    ### stat ###
    ############
    def getStat(hist, direction):
        newhist = hist.Clone('{0}{1}'.format(hist.GetName(), direction))
        nb = hist.GetNbinsX() * hist.GetNbinsY()
        for b in range(nb + 1):
            val = hist.GetBinContent(b + 1)
            err = hist.GetBinError(b + 1)
            newval = val + err if direction == 'Up' else val - err
            if newval < 0: newval = 0
            newhist.SetBinContent(b + 1, newval)
            newhist.SetBinError(b + 1, 0)
        return newhist

    logging.info('Adding stat systematic')
    statMapUp = {}
    statMapDown = {}
    for proc in backgrounds + signals:
        statMapUp[proc] = getStat(histMap[mode][''][proc], 'Up')
        statMapDown[proc] = getStat(histMap[mode][''][proc], 'Down')
    statsyst = {}

    for mode in ['PP', 'PF']:
        # background
        if doUnbinned:
            # TODO: add errors on params
            pass
        else:
            for proc in bgproc:
                statsyst[((proc, ), (era, ), (analysis, ),
                          (mode, ))] = (statMapUp[proc], statMapDown[proc])

        # signal
        if doParametric:
            for h in hmasses:
                statsyst[((splinename.format(h=h), ), (era, ), (analysis, ),
                          (mode, ))] = (getSpline(statMapUp,
                                                  h,
                                                  tag=mode + 'StatUp'),
                                        getSpline(statMapDown,
                                                  h,
                                                  tag=mode + 'StatDown'))
        else:
            for proc in sigproc:
                statsyst[((proc, ), (era, ), (analysis, ),
                          (mode, ))] = (statMapUp[proc], statMapDown[proc])

    limits.addSystematic('stat_{process}_{channel}',
                         'shape',
                         systematics=statsyst)

    ##############
    ### shifts ###
    ##############
    for shift in shiftTypes:
        logging.info('Adding {} systematic'.format(shift))
        shiftsyst = {}
        for mode in ['PP', 'PF']:

            # background
            if doUnbinned:
                # TODO rateParams on bg model
                pass
            else:
                for proc in bgproc:
                    shiftsyst[((proc, ), (era, ), (analysis, ),
                               (mode, ))] = (histMap[mode][shift + 'Up'][proc],
                                             histMap[mode][shift +
                                                           'Down'][proc])

            # signal
            if doParametric:
                for h in hmasses:
                    shiftsyst[((splinename.format(h=h), ), (era, ),
                               (analysis, ),
                               (mode, ))] = (getSpline(
                                   histMap[mode][shift + 'Up'],
                                   h,
                                   tag=mode + shift + 'Up'),
                                             getSpline(
                                                 histMap[mode][shift + 'Down'],
                                                 h,
                                                 tag=mode + shift + 'Down'))
            else:
                for proc in sigproc:
                    shiftsyst[((proc, ), (era, ), (analysis, ),
                               (mode, ))] = (histMap[mode][shift + 'Up'][proc],
                                             histMap[mode][shift +
                                                           'Down'][proc])

        limits.addSystematic(shift, 'shape', systematics=shiftsyst)

    ############
    ### Lumi ###
    ############
    # lumi 2.3% for 2015 and 2.5% for 2016
    # https://twiki.cern.ch/twiki/bin/view/CMS/TWikiLUM#CurRec
    logging.info('Adding lumi systematic')
    lumiproc = systsplineproc if doParametric else systproc
    lumisyst = {
        (lumiproc, (era, ), ('all', ), ('all', )): 1.025,
    }
    limits.addSystematic('lumi', 'lnN', systematics=lumisyst)

    ############
    ### muon ###
    ############
    # from z: 1 % + 0.5 % + 0.5 % per muon for id + iso + trig (pt>20)
    logging.info('Adding mu id+iso systematic')
    muproc = systsplineproc if doParametric else systproc
    musyst = {
        (muproc, (era, ), ('all', ), ('all', )):
        1 + math.sqrt(sum([0.01**2, 0.005**2] * 2 +
                          [0.01**2])),  # 2 lead have iso, tau_mu doesnt
    }
    limits.addSystematic('muid', 'lnN', systematics=musyst)

    logging.info('Adding mu trig systematic')
    musyst = {
        (muproc, (era, ), ('all', ), ('all', )): 1.005,  # 1 triggering muon
    }
    limits.addSystematic('mutrig', 'lnN', systematics=musyst)

    ###########
    ### tau ###
    ###########
    # 5% on sf 0.99 (VL/L) or 0.97 (M)
    logging.info('Adding mu id+iso systematic')
    tauproc = systsplineproc if doParametric else systproc
    tausyst = {
        (tauproc, (era, ), ('all', ), ('all', )): 1.05,
    }
    limits.addSystematic('tauid', 'lnN', systematics=tausyst)

    ######################
    ### Print datacard ###
    ######################
    directory = 'datacards_shape/{0}'.format('MuMuTauTau')
    python_mkdir(directory)
    datacard = '{0}/mmmt_{1}'.format(
        directory, args.tag) if args.tag else '{}/mmmt'.format(directory)
    processes = {}
    if doParametric:
        for h in hmasses:
            processes[signame.format(
                h=h, a='X')] = [splinename.format(h=h)] + backgrounds
    else:
        for signal in signals:
            processes[signal] = [signal] + backgrounds
    limits.printCard(datacard,
                     processes=processes,
                     blind=False,
                     saveWorkspace=doParametric)
Beispiel #7
0
def create_datacard(args):
    doMatrix = False
    doParametric = args.parametric
    doUnbinned = args.unbinned
    do2D = len(args.fitVars)==2
    blind = not args.unblind
    addSignal = args.addSignal
    signalParams = {'h': args.higgs, 'a': args.pseudoscalar}
    wsname = 'w'
    var = args.fitVars
    
    if do2D and doParametric:
       logging.error('Parametric 2D fits are not yet supported')
       raise

    if doUnbinned and not doParametric:
        logging.error('Unbinned only supported with parametric option')
        raise
    

    #############
    ### Setup ###
    #############
    sampleMap = getSampleMap()
    
    backgrounds = ['datadriven']
    data = ['data']
    
    signals = [signame.format(h=h,a=a) for h in hmasses for a in amasses]
    signalToAdd = signame.format(**signalParams)
    signalSplines = [splinename.format(h=h) for h in hmasses]

    
    wrappers = {}
    for proc in backgrounds+signals+data:
        if proc=='datadriven': continue
        for sample in sampleMap[proc]:
            wrappers[sample] = NtupleWrapper('MuMuTauTau',sample,new=True,version='80X')
            for shift in shifts:
                wrappers[sample+shift] = NtupleWrapper('MuMuTauTau',sample,new=True,version='80X',shift=shift)
    
    ##############################
    ### Create/read histograms ###
    ##############################
    
    histMap = {}
    # The definitons of which regions match to which arguments
    # PP can take a fake rate datadriven estimate from PF, but PF can only take the observed values
    regionArgs = {
        'PP': {'region':'A','fakeRegion':'B','source':'B','sources':['A','C'],'fakeSources':['B','D'],},
        'PF': {'region':'B','sources':['B','D'],},
    }
    for mode in ['PP','PF']:
        histMap[mode] = {}
        for shift in ['']+shifts:
            histMap[mode][shift] = {}
            for proc in backgrounds+signals:
                logging.info('Getting {} {}'.format(proc,shift))
                if proc=='datadriven':
                    # TODO: unbinned, get the RooDataHist from flattenener first
                    if mode=='PP':
                        if doMatrix:
                            histMap[mode][shift][proc] = getMatrixDatadrivenHist(var=var,wrappers=wrappers,shift=shift,do2D=do2D,**regionArgs[mode])
                        else:
                            histMap[mode][shift][proc] = getDatadrivenHist(var=var,wrappers=wrappers,shift=shift,do2D=do2D,**regionArgs[mode])
                    else:
                        if doMatrix:
                            histMap[mode][shift][proc] = getMatrixHist('data',var=var,wrappers=wrappers,shift=shift,do2D=do2D,**regionArgs[mode])
                        else:
                            histMap[mode][shift][proc] = getHist('data',var=var,wrappers=wrappers,shift=shift,do2D=do2D,**regionArgs[mode])
                else:
                    if doMatrix:
                        histMap[mode][shift][proc] = getMatrixHist(proc,var=var,wrappers=wrappers,shift=shift,do2D=do2D,**regionArgs[mode])
                    else:
                        histMap[mode][shift][proc] = getHist(proc,var=var,wrappers=wrappers,shift=shift,do2D=do2D,**regionArgs[mode])
                if do2D:
                    pass # TODO, figure out how to rebin 2D
                else:
                    histMap[mode][shift][proc].Rebin(rebinning[var[0]])
            if shift: continue
            logging.info('Getting observed')
            if blind:
                samples = backgrounds
                if addSignal: samples = backgrounds + [signalToAdd]
                hists = []
                for proc in samples:
                    hists += [histMap[mode][shift][proc]]
                hist = sumHists('obs',*hists)
                #for b in range(hist.GetNbinsX()+1):
                #    val = int(hist.GetBinContent(b))
                #    if val<0: val = 0
                #    err = val**0.5
                #    hist.SetBinContent(b,val)
                #    #hist.SetBinError(b,err)
                histMap[mode][shift]['data'] = hist
            else:
                hist = getHist('data',var=var,wrappers=wrappers,do2D=do2D,**regionArgs[mode])
                histMap[mode][shift]['data'] = hist
                if do2D:
                    pass
                else:
                    histMap[mode][shift]['data'].Rebin(rebinning[var[0]])
    
    #####################
    ### Create Limits ###
    #####################
    limits = Limits(wsname)
    
    limits.addEra('Run2016')
    limits.addAnalysis('HAA')
    
    era = 'Run2016'
    analysis = 'HAA'
    reco = 'mmmt'
    
    for mode in ['PP','PF']:
        limits.addChannel(mode)
        if doParametric:
            binning = varBinning[var[0]]
            limits.addMH(*binning[1:])
            limits.addX(*binning[1:],unit='GeV',label='m_{#mu#mu}')
            for h in hmasses:
                limits.addProcess(splinename.format(h=h),signal=True)
            for background in backgrounds:
                limits.addProcess(background)
            
            # add models
            for h in hmasses:
                model = getSpline(histMap[mode][''],h,tag=mode)
                limits.setExpected(splinename.format(h=h),era,analysis,mode,model)

            if doUnbinned:
                bg = buildModel(limits,tag=mode)
                limits.setExpected('datadriven', era, analysis, mode, bg)
            else:
                # add histograms for background if not using an unbinned model
                for bg in backgrounds:
                    limits.setExpected(bg,era,analysis,mode,histMap[mode][''][bg])
            
            # get roodatahist
            limits.setObserved(era,analysis,mode,histMap[mode]['']['data'])
        
        else:
        
            for signal in signals:
                limits.addProcess(signal,signal=True)
            for background in backgrounds:
                limits.addProcess(background)
            
            for proc in backgrounds:
                limits.setExpected(proc,era,analysis,mode,histMap[mode][''][proc])
            for proc in signals:
                limits.setExpected(proc,era,analysis,mode,histMap[mode][''][proc])
            
            limits.setObserved(era,analysis,mode,histMap[mode]['']['data'])
        
    #########################
    ### Add uncertainties ###
    #########################
    
    systproc = tuple([proc for proc in signals + backgrounds if 'datadriven' not in proc])
    allproc = tuple([proc for proc in signals + backgrounds])
    systsplineproc = tuple([proc for proc in signalSplines + backgrounds if 'datadriven' not in proc])
    allsplineproc = tuple([proc for proc in signalSplines + backgrounds])
    bgproc = tuple([proc for proc in backgrounds])
    sigsplineproc = tuple([proc for proc in signalSplines])
    sigproc = tuple([proc for proc in signals])
    
    
    ############
    ### stat ###
    ############
    def getStat(hist,direction):
        newhist = hist.Clone('{0}{1}'.format(hist.GetName(),direction))
        nb = hist.GetNbinsX()*hist.GetNbinsY()
        for b in range(nb+1):
            val = hist.GetBinContent(b+1)
            err = hist.GetBinError(b+1)
            newval = val+err if direction=='Up' else val-err
            if newval<0: newval = 0
            newhist.SetBinContent(b+1,newval)
            newhist.SetBinError(b+1,0)
        return newhist
    
    logging.info('Adding stat systematic')
    statMapUp = {}
    statMapDown = {}
    for proc in backgrounds+signals:
        statMapUp[proc] = getStat(histMap[mode][''][proc],'Up')
        statMapDown[proc] = getStat(histMap[mode][''][proc],'Down')
    statsyst = {}

    for mode in ['PP','PF']:
        # background
        if doUnbinned:
            # TODO: add errors on params
            pass
        else:
            for proc in bgproc:
                statsyst[((proc,),(era,),(analysis,),(mode,))] = (statMapUp[proc],statMapDown[proc])

        # signal
        if doParametric:
            for h in hmasses:
                statsyst[((splinename.format(h=h),),(era,),(analysis,),(mode,))] = (getSpline(statMapUp,h,tag=mode+'StatUp'),getSpline(statMapDown,h,tag=mode+'StatDown'))
        else:
            for proc in sigproc:
                statsyst[((proc,),(era,),(analysis,),(mode,))] = (statMapUp[proc],statMapDown[proc])

    limits.addSystematic('stat_{process}_{channel}','shape',systematics=statsyst)

    ##############
    ### shifts ###
    ##############
    for shift in shiftTypes:
        logging.info('Adding {} systematic'.format(shift))
        shiftsyst = {}
        for mode in ['PP','PF']:

            # background
            if doUnbinned:
                # TODO rateParams on bg model
                pass
            else:
                for proc in bgproc:
                    shiftsyst[((proc,),(era,),(analysis,),(mode,))] = (histMap[mode][shift+'Up'][proc], histMap[mode][shift+'Down'][proc])

            # signal
            if doParametric:
                for h in hmasses:
                    shiftsyst[((splinename.format(h=h),),(era,),(analysis,),(mode,))] = (getSpline(histMap[mode][shift+'Up'],h,tag=mode+shift+'Up'),getSpline(histMap[mode][shift+'Down'],h,tag=mode+shift+'Down'))
            else:
                for proc in sigproc:
                    shiftsyst[((proc,),(era,),(analysis,),(mode,))] = (histMap[mode][shift+'Up'][proc], histMap[mode][shift+'Down'][proc])

        limits.addSystematic(shift,'shape',systematics=shiftsyst)
        
    ############
    ### Lumi ###
    ############
    # lumi 2.3% for 2015 and 2.5% for 2016
    # https://twiki.cern.ch/twiki/bin/view/CMS/TWikiLUM#CurRec
    logging.info('Adding lumi systematic')
    lumiproc = systsplineproc if doParametric else systproc
    lumisyst = {
        (lumiproc,(era,),('all',),('all',)): 1.025,
    }
    limits.addSystematic('lumi','lnN',systematics=lumisyst)

    ############
    ### muon ###
    ############
    # from z: 1 % + 0.5 % + 0.5 % per muon for id + iso + trig (pt>20)
    logging.info('Adding mu id+iso systematic')
    muproc = systsplineproc if doParametric else systproc
    musyst = {
        (muproc,(era,),('all',),('all',)): 1+math.sqrt(sum([0.01**2,0.005**2]*2+[0.01**2])), # 2 lead have iso, tau_mu doesnt
    }
    limits.addSystematic('muid','lnN',systematics=musyst)

    logging.info('Adding mu trig systematic')
    musyst = {
        (muproc,(era,),('all',),('all',)): 1.005, # 1 triggering muon
    }
    limits.addSystematic('mutrig','lnN',systematics=musyst)

    ###########
    ### tau ###
    ###########
    # 5% on sf 0.99 (VL/L) or 0.97 (M)
    logging.info('Adding mu id+iso systematic')
    tauproc = systsplineproc if doParametric else systproc
    tausyst = {
        (tauproc,(era,),('all',),('all',)): 1.05,
    }
    limits.addSystematic('tauid','lnN',systematics=tausyst)

    ######################
    ### Print datacard ###
    ######################
    directory = 'datacards_shape/{0}'.format('MuMuTauTau')
    python_mkdir(directory)
    datacard = '{0}/mmmt_{1}'.format(directory, args.tag) if args.tag else '{}/mmmt'.format(directory)
    processes = {}
    if doParametric:
        for h in hmasses:
            processes[signame.format(h=h,a='X')] = [splinename.format(h=h)] + backgrounds
    else:
        for signal in signals:
            processes[signal] = [signal]+backgrounds
    limits.printCard(datacard,processes=processes,blind=False,saveWorkspace=doParametric)
    for proc in samples:
        hists += [histMap[proc]]
    hist = sumHists('obs',*hists)
    for b in range(hist.GetNbinsX()+1):
        val = int(hist.GetBinContent(b))
        if val<0: val = 0
        err = val**0.5
        hist.SetBinContent(b,val)
        #hist.SetBinError(b,err)
    histMap['data'] = hist
else:
    hist = getBinned('data')
    histMap['data'] = hist

# create limit object
limits = Limits(wsname)

limits.addEra('Run2016')
limits.addAnalysis('ThreePhoton')
limits.addChannel('ggg')

if doParametric:
    limits.addMH(*binning[1:])
    limits.addX(*binning[1:])

if doParametric:
    limits.addProcess('sig',signal=True)
    limits.addProcess('bg')
else:
    for signal in signals:
        limits.addProcess(signal,signal=True)
    for proc in samples:
        hists += [histMap[proc]]
    hist = sumHists('obs', *hists)
    for b in range(hist.GetNbinsX() + 1):
        val = int(hist.GetBinContent(b))
        if val < 0: val = 0
        err = val**0.5
        hist.SetBinContent(b, val)
        #hist.SetBinError(b,err)
    histMap['data'] = hist
else:
    hist = getBinned('data')
    histMap['data'] = hist

# create limit object
limits = Limits(wsname)

limits.addEra('Run2016')
limits.addAnalysis('ThreePhoton')
limits.addChannel('ggg')

if doParametric:
    limits.addMH(*binning[1:])
    limits.addX(*binning[1:])

if doParametric:
    limits.addProcess('sig', signal=True)
    limits.addProcess('bg')
else:
    for signal in signals:
        limits.addProcess(signal, signal=True)