def initMET(process, isData): """ Parts of MET re-reconstruction that are independent of PF candidate collection. """ sequence = cms.Sequence() addattr = AddAttr(process, sequence) addattr('caloMet', cms.EDProducer("RecoMETExtractor", metSource = cms.InputTag("slimmedMETs", processName = cms.InputTag.skipCurrentProcess()), correctionLevel = cms.string('rawCalo') ) ) if not isData: addattr('genMetTrue', cms.EDProducer("GenMETExtractor", metSource= cms.InputTag("slimmedMETs", processName = cms.InputTag.skipCurrentProcess()) ) ) addattr('patCaloMet', patMETs.clone( metSource = 'caloMet', addMuonCorrections = False, addGenMET = False ) ) return sequence
def initBTag(process, vsuffix, candidates='particleFlow', primaryVertex='offlinePrimaryVertices'): if vsuffix in vertexingConfig: if (candidates, primaryVertex) != vertexingConfig[vsuffix]: raise RuntimeError('Duplicate vertexing configuration name') return cms.Sequence() if len(vertexingConfig) == 0: # ESProducers and vertexing sequences process.load('RecoBTag.ImpactParameter.impactParameter_EventSetup_cff') process.load('RecoBTag.CTagging.cTagging_EventSetup_cff') process.load('RecoBTag.SecondaryVertex.secondaryVertex_EventSetup_cff') process.load('RecoBTag.Combined.combinedMVA_EventSetup_cff') process.load('RecoBTag.SoftLepton.softLepton_EventSetup_cff') vertexingConfig[vsuffix] = (candidates, primaryVertex) sequence = cms.Sequence() addattr = AddAttr(process, sequence, vsuffix) vertexFinder = addattr( 'inclusiveCandidateVertexFinder', vertexing.inclusiveCandidateVertexFinder.clone( primaryVertices=primaryVertex, tracks=candidates)) vertexMerger = addattr( 'candidateVertexMerger', vertexing.candidateVertexMerger.clone(secondaryVertices=vertexFinder)) vertexArbitrator = addattr( 'candidateVertexArbitrator', vertexing.candidateVertexArbitrator.clone( primaryVertices=primaryVertex, tracks=candidates, secondaryVertices=vertexMerger)) secondaryVertices = addattr( 'inclusiveCandidateSecondaryVertices', vertexing.inclusiveCandidateSecondaryVertices.clone( secondaryVertices=vertexArbitrator)) vertexFinderCvsL = addattr( 'inclusiveCandidateVertexFinderCvsL', vertexing.inclusiveCandidateVertexFinderCvsL.clone( primaryVertices=primaryVertex, tracks=candidates)) vertexMergerCvsL = addattr( 'candidateVertexMergerCvsL', vertexing.candidateVertexMergerCvsL.clone( secondaryVertices=vertexFinderCvsL)) vertexArbitratorCvsL = addattr( 'candidateVertexArbitratorCvsL', vertexing.candidateVertexArbitratorCvsL.clone( primaryVertices=primaryVertex, tracks=candidates, secondaryVertices=vertexMergerCvsL)) secondaryVerticesCvsL = addattr( 'inclusiveCandidateSecondaryVerticesCvsL', vertexing.inclusiveCandidateSecondaryVerticesCvsL.clone( secondaryVertices='candidateVertexArbitratorCvsL' + vsuffix)) return sequence
def initFatJets(process, isData, labels): """ @labels: ['AK8', 'CA15', ...] """ ######################################## ## INITIAL SETUP ## ######################################## sequence = cms.Sequence() addattr = AddAttr(process, sequence) if not isData and not hasattr(process, 'packedGenParticlesForJetsNoNu'): genParticlesNoNu = addattr( 'packedGenParticlesForJetsNoNu', cms.EDFilter( "CandPtrSelector", src=cms.InputTag(finalStateGenParticles), cut=cms.string( "abs(pdgId) != 12 && abs(pdgId) != 14 && abs(pdgId) != 16") )) for label in labels: matches = re.match('(AK|CA)([0-9]+)$', label) if not matches: raise RuntimeError('Unknown algo label ' + label) # set up radius and algoName from the input label radius = float(matches.group(2)) * 0.1 if matches.group(1) == 'CA': algoName = 'CambridgeAachen' elif matches.group(1) == 'AK': algoName = 'AntiKt' # gen jets addattr( 'genJetsNoNu' + label, ak4GenJets.clone(jetAlgorithm=cms.string(algoName), rParam=cms.double(radius), src=genParticlesNoNu)) genJetsMod = addattr.last sdZcut, sdBeta = sdParams(radius) # gen jets soft drop for subjet gen matching addattr( 'genJetsNoNuSoftDrop' + label, genJetsMod.clone(R0=cms.double(radius), useSoftDrop=cms.bool(True), zcut=cms.double(sdZcut), beta=cms.double(sdBeta), writeCompound=cms.bool(True), useExplicitGhosts=cms.bool(True), jetCollInstanceName=cms.string("SubJets"))) # Charged hadron subtraction addattr( 'pfCHS', cms.EDFilter("CandPtrSelector", src=cms.InputTag(pfSource), cut=cms.string("fromPV"))) # Initialize btag inputs sequence += initBTag(process, '', pfSource, pvSource) return sequence
def makeFatJets(process, isData, label, candidates, ptMin=100.): """ @param label: AK8PFchs, CA15PFPuppi, etc. """ matches = re.match('(AK|CA)([0-9]+)PF(.+)', label) if not matches: raise RuntimeError('Unknown algo label ' + label) algo = matches.group(1) + matches.group(2) # set up radius and algoName from the input label radius = float(matches.group(2)) * 0.1 if matches.group(1) == 'CA': algoName = 'CambridgeAachen' elif matches.group(1) == 'AK': algoName = 'AntiKt' pu = matches.group(3) if pu == 'chs': jecLabel = 'AK8PFchs' # regardless of jet algo elif pu == 'Puppi': jecLabel = 'AK8PFPuppi' # regardless of jet algo else: raise RuntimeError('Unknown PU mitigation ' + pu) sdZcut, sdBeta = sdParams(radius) sequence = cms.Sequence() # Callable object that adds the second argument to process and sequence with label attached as suffix addattr = AddAttr(process, sequence, label) ######################################## ## REMAKE JETS ## ######################################## pfJets = addattr( 'pfJets', ak4PFJets.clone(jetAlgorithm=cms.string(algoName), rParam=cms.double(radius), src=cms.InputTag(candidates), jetPtMin=cms.double(ptMin))) pfJetsSoftDrop = addattr( 'pfJetsSoftDrop', ak4PFJets.clone(jetAlgorithm=cms.string(algoName), rParam=cms.double(radius), src=cms.InputTag(candidates), jetPtMin=cms.double(ptMin), useSoftDrop=cms.bool(True), R0=cms.double(radius), zcut=cms.double(sdZcut), beta=cms.double(sdBeta), writeCompound=cms.bool(True), useExplicitGhosts=cms.bool(True), jetCollInstanceName=cms.string("SubJets"))) subjets = cms.InputTag(pfJetsSoftDrop.getModuleLabel(), 'SubJets') pfJetsPruned = addattr( 'pfJetsPruned', ak4PFJets.clone( jetAlgorithm=cms.string(algoName), rParam=cms.double(radius), src=cms.InputTag(candidates), jetPtMin=cms.double(ptMin), usePruning=cms.bool(True), useExplicitGhosts=cms.bool(True), writeCompound=cms.bool(True), zcut=cms.double(0.1), # no idea if these parameters are correct rcut_factor=cms.double(0.5), nFilt=cms.int32(2), jetCollInstanceName=cms.string("SubJets"))) ######################################## ## SUBSTRUCTURE ## ####################################### Njettiness = addattr( 'Njettiness', nJettinessAdder_cfi.Njettiness.clone(src=pfJets, R0=cms.double(radius), Njets=cms.vuint32(1, 2, 3, 4))) sdKinematics = addattr( 'sdKinematics', cms.EDProducer('RecoJetDeltaRValueMapProducer', src=pfJets, matched=pfJetsSoftDrop, distMax=cms.double(radius), values=cms.vstring('mass'), valueLabels=cms.vstring('Mass'))) prunedKinematics = addattr( 'prunedKinematics', cms.EDProducer('RecoJetDeltaRValueMapProducer', src=pfJets, matched=pfJetsPruned, distMax=cms.double(radius), values=cms.vstring('mass'), valueLabels=cms.vstring('Mass'))) ### subjet qg-tagging ### subQGTag = addattr( 'subQGTag', QGTagger.clone(srcJets=subjets, jetsLabel=cms.string('QGL_AK4PFchs'))) ### subjet b-tagging ### # sets up process.pfCombinedInclusiveSecondaryVertexV2BJetTags(label)Subjets (and necessary inputs) sequence += setupBTag( process, jetCollection=subjets, suffix=label + 'Subjets', vsuffix='', tags=['pfCombinedInclusiveSecondaryVertexV2BJetTags']) ######################################## ## MAKE PAT JETS ## ######################################## ## MAIN JET ## jecLevels = ['L1FastJet', 'L2Relative', 'L3Absolute'] if isData: jecLevels.append('L2L3Residual') jetCorrFactors = addattr( 'jetCorrFactors', jetUpdater_cff.patJetCorrFactors.clone(src=pfJets, payload=jecLabel, levels=jecLevels, primaryVertices=pvSource)) if not isData: genJetMatch = addattr( 'genJetMatch', patJetGenJetMatch.clone(src=pfJets, maxDeltaR=radius, matched='genJetsNoNu' + algo)) patJets = addattr( 'patJets', jetProducer_cfi.patJets.clone(jetSource=pfJets, addJetCorrFactors=True, addBTagInfo=False, addAssociatedTracks=False, addJetCharge=False, addGenPartonMatch=False, addGenJetMatch=(not isData), getJetMCFlavour=False, addJetFlavourInfo=False)) patJetsMod = addattr.last patJetsMod.jetCorrFactorsSource = [jetCorrFactors] if not isData: patJetsMod.genJetMatch = genJetMatch for tau in ['tau1', 'tau2', 'tau3', 'tau4']: patJetsMod.userData.userFloats.src.append(Njettiness.getModuleLabel() + ':' + tau) patJetsMod.userData.userFloats.src.append(sdKinematics.getModuleLabel() + ':Mass') patJetsMod.userData.userFloats.src.append( prunedKinematics.getModuleLabel() + ':Mass') selectedPatJets = addattr( 'selectedPatJets', jetSelector_cfi.selectedPatJets.clone(src=patJets, cut='abs(eta) < 2.5')) ## SOFT DROP ## patJetsSoftDrop = addattr( 'patJetsSoftDrop', jetProducer_cfi._patJets.clone(jetSource=pfJetsSoftDrop, addJetCorrFactors=False, addBTagInfo=False, addAssociatedTracks=False, addJetCharge=False, addGenPartonMatch=False, addGenJetMatch=False, getJetMCFlavour=False, addJetFlavourInfo=False)) selectedPatJetsSoftDrop = addattr( 'selectedPatJetsSoftDrop', jetSelector_cfi.selectedPatJets.clone(src=patJetsSoftDrop, cut='abs(eta) < 2.5')) if not isData: genSubjetsMatch = addattr( 'genSubjetMatch', patJetGenJetMatch.clone(src=subjets, maxDeltaR=0.4, matched='genJetsNoNuSoftDrop' + algo + ':SubJets')) patSubjets = addattr( 'patSubjets', jetProducer_cfi._patJets.clone( jetSource=subjets, addJetCorrFactors=False, addBTagInfo=True, discriminatorSources=[ 'pfCombinedInclusiveSecondaryVertexV2BJetTags' + label + 'Subjets' ], addAssociatedTracks=False, addJetCharge=False, addGenPartonMatch=False, addGenJetMatch=(not isData), getJetMCFlavour=False, addJetFlavourInfo=False)) patSubjetsMod = addattr.last patSubjetsMod.userData.userFloats.src.append( cms.InputTag(subQGTag.getModuleLabel(), 'qgLikelihood')) if not isData: patSubjetsMod.genJetMatch = genSubjetsMatch ## MERGE SUBJETS BACK ## jetMerger = addattr( 'jetMerger', cms.EDProducer("BoostedJetMerger", jetSrc=selectedPatJetsSoftDrop, subjetSrc=patSubjets)) ## PACK ## addattr( 'packedPatJets', cms.EDProducer("JetSubstructurePacker", jetSrc=selectedPatJets, distMax=cms.double(radius), algoTags=cms.VInputTag(jetMerger), algoLabels=cms.vstring('SoftDrop'), fixDaughters=cms.bool(False))) return sequence
def makeJets(process, isData, label, candidates, suffix): """ Light-weight version of pat addJetCollection. @labels: e.g. 'AK4PFPuppi' """ sequence = cms.Sequence() addattr = AddAttr(process, sequence, suffix) ak4PFJets = addattr('ak4PFJets', ak4PFJetsPuppi.clone( src = candidates, doAreaFastjet = True ) ) jecLevels= ['L1FastJet', 'L2Relative', 'L3Absolute'] if isData: jecLevels.append('L2L3Residual') jetCorrFactors = addattr('jetCorrFactors', patJetCorrFactors.clone( src = ak4PFJets, payload = label, levels = jecLevels, primaryVertices = pvSource ) ) # btag should always use standard PF collection sequence += initBTag(process, '', pfSource, pvSource) sequence += setupBTag( process, jetCollection = ak4PFJets, suffix = suffix, vsuffix = '', muons = muons, electrons = electrons, tags = ['pfCombinedInclusiveSecondaryVertexV2BJetTags'] ) if not isData: genJetMatch = addattr('genJetMatch', patJetGenJetMatch.clone( src = ak4PFJets, maxDeltaR = 0.4, matched = genJets ) ) allPatJets = addattr('patJets', patJets.clone( jetSource = ak4PFJets, addJetCorrFactors = True, jetCorrFactorsSource = [jetCorrFactors], addBTagInfo = True, discriminatorSources = [cms.InputTag('pfCombinedInclusiveSecondaryVertexV2BJetTags' + suffix)], addAssociatedTracks = False, addJetCharge = False, addGenPartonMatch = False, addGenJetMatch = (not isData), getJetMCFlavour = False, addJetFlavourInfo = False ) ) if not isData: addattr.last.genJetMatch = genJetMatch selectedJets = addattr('selectedJets', selectedPatJets.clone( src = allPatJets, cut = 'pt > 15' ) ) addattr('slimmedJets', slimmedJets.clone( src = selectedJets, rekeyDaughters = '0' ) ) return sequence
def makeJets(process, isData, label, candidates, suffix): """ Light-weight version of pat addJetCollection. @labels: e.g. 'AK4PFPuppi' """ sequence = cms.Sequence() addattr = AddAttr(process, sequence, suffix) jets = addattr('ak4PFJets', ak4PFJets.clone(src=candidates, doAreaFastjet=True)) jecLevels = ['L1FastJet', 'L2Relative', 'L3Absolute'] if isData: jecLevels.append('L2L3Residual') jetCorrFactors = addattr( 'jetCorrFactors', patJetCorrFactors.clone(src=jets, payload=label, levels=jecLevels, primaryVertices=pvSource)) # btag should always use standard PF collection sequence += initBTag(process, '', pfSource, pvSource) sequence += setupBTag(process, jetCollection=jets, suffix=suffix, vsuffix='', muons=muons, electrons=electrons, tags=[ 'pfCombinedInclusiveSecondaryVertexV2BJetTags', 'pfCombinedMVAV2BJetTags', 'pfDeepCSVJetTags', 'pfDeepCMVAJetTags' ]) qgTagger = addattr('QGTagger', QGTagger.clone(srcJets=jets)) if not isData: genJetMatch = addattr( 'genJetMatch', patJetGenJetMatch.clone(src=jets, maxDeltaR=0.4, matched=genJets)) allPatJets = addattr('patJets', patJets.clone( jetSource = jets, addJetCorrFactors = True, jetCorrFactorsSource = [jetCorrFactors], addBTagInfo = True, discriminatorSources = [ cms.InputTag('pfCombinedInclusiveSecondaryVertexV2BJetTags' + suffix), cms.InputTag('pfCombinedMVAV2BJetTags' + suffix), ] + \ sum([[cms.InputTag('pfDeepCSVJetTags' + suffix, 'prob' + prob), cms.InputTag('pfDeepCMVAJetTags' + suffix, 'prob' + prob)] # for prob in ['udsg', 'b', 'c', 'bb', 'cc']], for prob in ['udsg', 'b', 'c', 'bb']], []), addAssociatedTracks = False, addJetCharge = False, addGenPartonMatch = False, addGenJetMatch = (not isData), getJetMCFlavour = False, addJetFlavourInfo = False ) ) addattr.last.userData.userFloats.src = [ qgTagger.getModuleLabel() + ':qgLikelihood' ] addattr.last.userData.userFloats.labelPostfixesToStrip = cms.vstring( suffix) if not isData: addattr.last.genJetMatch = genJetMatch selectedJets = addattr( 'selectedJets', selectedPatJets.clone(src=allPatJets, cut='pt > 15')) addattr('slimmedJets', slimmedJets.clone(src=selectedJets, rekeyDaughters='0')) return sequence
def makeMET(process, isData, pfCandidates, jetSource, jetFlavor, postfix = ''): """ @jetFlavor: e.g. 'ak4PFchs' Additional information (such as gen and calo mets) are added only if postfix is empty. """ sequence = cms.Sequence() # postfix is automatically added to the module names addattr = AddAttr(process, sequence, postfix) if postfix == '': # default MET - extract from input slimmedMETs pfMet = addattr('pfMet', cms.EDProducer("RecoMETExtractor", metSource = cms.InputTag("slimmedMETs", processName = cms.InputTag.skipCurrentProcess()), correctionLevel = cms.string('raw') ) ) else: pfMet = addattr('pfMet', PFMET_cfi.pfMet.clone( src = pfCandidates, calculateSignificance = False # done in PAT ) ) cleanedJets = addattr('cleanedJetsForMET', cms.EDProducer("PATJetCleanerForType1MET", src = cms.InputTag(jetSource), jetCorrEtaMax = cms.double(9.9), jetCorrLabel = cms.InputTag("L3Absolute"), jetCorrLabelRes = cms.InputTag("L2L3Residual"), offsetCorrLabel = cms.InputTag("L1FastJet"), skipEM = cms.bool(True), skipEMfractionThreshold = cms.double(0.9), skipMuonSelection = cms.string('isGlobalMuon | isStandAloneMuon'), skipMuons = cms.bool(True), type1JetPtThreshold = cms.double(15.0) ) ) selectedJets = addattr('selectedJetsForMET', selectedPatJets.clone( src = cleanedJets, cut = 'pt > 15 && abs(eta) < 9.9' ) ) crossCleanedJets = addattr('crossCleanedJetsForMET', cleanPatJets.clone( src = selectedJets ) ) ccJetsMod = addattr.last ccJetsMod.checkOverlaps.muons.src = muons ccJetsMod.checkOverlaps.electrons.src = electrons del ccJetsMod.checkOverlaps.photons del ccJetsMod.checkOverlaps.taus # not used at all and electrons are already cleaned del ccJetsMod.checkOverlaps.tkIsoElectrons patPFMet = addattr('patPFMet', patMET_cff.patPFMet.clone( metSource = pfMet, genMETSource = 'genMetTrue', srcPFCands = pfCandidates, computeMETSignificance = True, parameters = (METSignificanceParams_Data if isData else METSignificanceParams), srcJets = crossCleanedJets, srcLeptons = [electrons, muons, photons], addGenMET = (not isData and postfix == '') ) ) patPFMetT1Corr = addattr('patPFMetT1Corr', patMET_cff.patPFMetT1T2Corr.clone( src = crossCleanedJets ) ) patPFMetT1 = addattr('patPFMetT1', patMET_cff.patPFMetT1.clone( src = patPFMet, srcCorrections = [cms.InputTag(patPFMetT1Corr.getModuleLabel(), 'type1')] ) ) pfCandsNoEle = addattr('pfCandsNoEle', cms.EDProducer("CandPtrProjector", src = cms.InputTag(pfCandidates), veto = electrons ) ) pfCandsNoEleMu = addattr('pfCandsNoEleMu', cms.EDProducer("CandPtrProjector", src = pfCandsNoEle, veto = muons ) ) pfCandsNoEleMuTau = addattr('pfCandsNoEleMuTau', cms.EDProducer("CandPtrProjector", src = pfCandsNoEleMu, veto = taus ) ) pfCandsNoEleMuTauGamma = addattr('pfCandsNoEleMuTauGamma', cms.EDProducer("CandPtrProjector", src = pfCandsNoEleMuTau, veto = photons ) ) pfCandsForUnclusteredUnc = addattr('pfCandsForUnclusteredUnc', cms.EDProducer("CandPtrProjector", src = pfCandsNoEleMuTauGamma, veto = crossCleanedJets ) ) for vsign, vname in [(1, 'Up'), (-1, 'Down')]: shiftConf = [ ('MuonEn', muons.value(), '((x<100)?(0.002+0*y):(0.05+0*y))'), ('ElectronEn', electrons.value(), '((abs(y)<1.479)?(0.006+0*x):(0.015+0*x))'), ('PhotonEn', photons.value(), '((abs(y)<1.479)?(0.01+0*x):(0.025+0*x))'), ('TauEn', taus.value(), '0.03+0*x*y'), ('UnclusteredEn', pfCandsForUnclusteredUnc.value(), ''), ('JetEn', crossCleanedJets.value(), '') ] for part, coll, formula in shiftConf: if part == 'UnclusteredEn': shifted = addattr('shifted' + part + vname, cms.EDProducer("ShiftedParticleProducer", src = pfCandsForUnclusteredUnc, binning = cms.VPSet( # charged PF hadrons - tracker resolution cms.PSet( binSelection = cms.string('charge!=0'), binUncertainty = cms.string('sqrt(pow(0.00009*x,2)+pow(0.0085/sqrt(sin(2*atan(exp(-y)))),2))') ), # neutral PF hadrons - HCAL resolution cms.PSet( binSelection = cms.string('pdgId==130'), energyDependency = cms.bool(True), binUncertainty = cms.string('((abs(y)<1.3)?(min(0.25,sqrt(0.64/x+0.0025))):(min(0.30,sqrt(1.0/x+0.0016))))') ), # photon - ECAL resolution cms.PSet( binSelection = cms.string('pdgId==22'), energyDependency = cms.bool(True), binUncertainty = cms.string('sqrt(0.0009/x+0.000001)+0*y') ), # HF particules - HF resolution cms.PSet( binSelection = cms.string('pdgId==1 || pdgId==2'), energyDependency = cms.bool(True), binUncertainty = cms.string('sqrt(1./x+0.0025)+0*y') ), ), shiftBy = cms.double(float(vsign)) ) ) elif part == 'JetEn': shifted = addattr('shifted' + part + vname, cms.EDProducer('PandaShiftedPATJetProducer', src = crossCleanedJets, jetCorrPayloadName = cms.string(jetFlavor), jetCorrUncertaintyTag = cms.string('Uncertainty'), addResidualJES = cms.bool(isData), jetCorrLabelUpToL3 = cms.InputTag('L3Absolute'), # use embedded correction factors jetCorrLabelUpToL3Res = cms.InputTag('L2L3Residual'), shiftBy = cms.double(float(vsign)) ) ) else: shifted = addattr('shifted' + part + vname, cms.EDProducer("ShiftedParticleProducer", src = cms.InputTag(coll), uncertainty = cms.string(formula), shiftBy = cms.double(float(vsign)) ) ) metCorrShifted = addattr('metCorrShifted' + part + vname, cms.EDProducer("ShiftedParticleMETcorrInputProducer", srcOriginal = cms.InputTag(coll), srcShifted = shifted ) ) addattr('patPFMetT1' + part + vname, patMET_cff.patPFMetT1.clone( src = patPFMetT1, srcCorrections = [metCorrShifted] ) ) # Dummy JetResUp and JetResDown modules because PATJetSlimmer requires them # Jet smearing should be propagated to MET simply by using ptSmear(|Up|Down) branches at the ntuples level addattr('patPFMetT1JetResUp', getattr(process, 'patPFMetT1JetEnUp' + postfix).clone()) addattr('patPFMetT1JetResDown', getattr(process, 'patPFMetT1JetEnDown' + postfix).clone()) addattr('slimmedMETs', slimmedMETs.clone( src = patPFMetT1, rawVariation = patPFMet, t1Uncertainties = "patPFMetT1%s" + postfix, runningOnMiniAOD = True ) ) slimmed = addattr.last if postfix == '': # default MET slimmed.caloMET = 'patCaloMet' else: del slimmed.caloMET del slimmed.t01Variation del slimmed.t1SmearedVarsAndUncs del slimmed.tXYUncForT1 del slimmed.tXYUncForRaw del slimmed.tXYUncForT01 del slimmed.tXYUncForT1Smear del slimmed.tXYUncForT01Smear return sequence