ATLAS Offline Software
Loading...
Searching...
No Matches
EventSelectionConfig.py
Go to the documentation of this file.
1# Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
2
3import re
4import warnings
5from copy import deepcopy
6from dataclasses import dataclass
7from functools import partial
8
9from AnalysisAlgorithmsConfig.ConfigBlock import ConfigBlock
10from AnalysisAlgorithmsConfig.ConfigSequence import groupBlocks
11from AsgAnalysisAlgorithms.AsgAnalysisConfig import EventCutFlowBlock
12from AnalysisAlgorithmsConfig.ConfigAccumulator import DataType, ConfigDeprecationWarning
13
14
15class UnavailableFeatureError(ValueError):
16 """Raised when an EXPR cut uses a syntactically valid but unimplemented
17 feature (an unknown variable or collection). Subclasses ValueError so the
18 framework's existing tolerance and `except ValueError` still apply."""
19
20
21class InconsistentSettingsError(ValueError):
22 """Raised when an EXPR cut is internally inconsistent or ill-typed (wrong
23 operand count, an operation undefined for the given object, etc.)."""
24
25
26@dataclass(frozen=True)
28 option: str
29 apply_btag: bool = False
30 is_met: bool = False
31
32
33@dataclass(frozen=True)
35 min_operands: int
36 max_operands: int | None
37 separator: str | None
38 needs_eta: bool
39 met_ok: bool
40
41
43 """Small recursive-descent parser for EXPR selector expressions."""
44
45 def __init__(self, token_re):
46 self._token_re = token_re
47 self._tokens = []
48 self._pos = 0
49
50 def tokenize(self, text):
51 tokens, pos = [], 0
52 while pos < len(text):
53 match = self._token_re.match(text, pos)
54 if not match:
56 f"[EventSelectionConfig] EXPR: cannot parse near '{text[pos:]}'")
57 pos = match.end()
58 if match.lastgroup != "WS":
59 tokens.append((match.lastgroup, match.group()))
60 tokens.append(("END", ""))
61 return tokens
62
63 def _peek(self):
64 return self._tokens[self._pos]
65
66 def _advance(self):
67 token = self._tokens[self._pos]
68 self._pos += 1
69 return token
70
71 def _expect(self, kind):
72 token = self._advance()
73 if token[0] != kind:
75 f"[EventSelectionConfig] EXPR: expected {kind}, got '{token[1]}'")
76 return token
77
78 def parse(self, tokens):
79 self._tokens = tokens
80 self._pos = 0
81 variable, separator, operands = self._parse_funcall()
82 sign = self._parse_sign()
83 ref_value = self._parse_value()
84 self._expect("END")
85 return variable, separator, operands, sign, ref_value
86
87 def _parse_funcall(self):
88 variable = self._expect("ID")[1]
89 self._expect("LP")
90 operands = [self._parse_operand()]
91 separator = None
92 while self._peek()[0] in ("COMMA", "PLUS"):
93 sep = "," if self._advance()[0] == "COMMA" else "+"
94 if separator is None:
95 separator = sep
96 elif sep != separator:
98 "[EventSelectionConfig] EXPR: cannot mix ',' and '+' separators")
99 operands.append(self._parse_operand())
100 self._expect("RP")
101 return variable, separator, operands
102
103 def _parse_operand(self):
104 coll = self._expect("ID")[1]
105 index = None
106 if self._peek()[0] == "LB":
107 self._advance()
108 index = int(self._expect("NUM")[1])
109 self._expect("RB")
110 return coll, index
111
112 def _parse_sign(self):
113 token = self._advance()
114 if token[0] not in ("LT", "GT", "EQ", "GE", "LE"):
116 f"[EventSelectionConfig] EXPR: expected a comparison operator, got '{token[1]}'")
117 return token[0]
118
119 def _parse_value(self):
120 negative = False
121 if self._peek()[0] == "MINUS":
122 self._advance()
123 negative = True
124 value = float(self._expect("NUM")[1])
125 return -value if negative else value
126
127
128
129class EventSelectionMergerConfig(ConfigBlock):
130 """ConfigBlock for merging the output of various selection streams"""
131
132 def __init__(self):
133 super(EventSelectionMergerConfig, self).__init__()
134 self._instance_number = EventSelectionMergerConfig.get_instance_count()
135 self.setBlockName('EventSelectionMerger')
136 self.addDependency('EventSelection', required=True)
137 self.addOption('noFilter', False, type=bool,
138 info="do not apply an event filter, i.e. setting it to `False` "
139 "removes events not passing the full list of selection cuts.")
140
141 def instanceName (self) :
142 """Return the instance name for this block"""
143 return '' # There is only ever one instance of this block
144
145 def makeAlgs(self, config):
146 # Only the first instance runs; all others are no-ops
147 if self._instance_number != 1:
148 return
149
150 selections = config.getContainerMeta('EventInfo', 'eventSelectionNames',
151 failOnMiss=True)
152 selections = [sel for sel in selections if not sel.startswith("pass_SUB")]
153
154 alg = config.createAlgorithm('CP::SaveFilterAlg',
155 'EventSelectionMerger' + selections[0].split("_%SYS%")[0])
156 alg.FilterDescription = 'events passing at least one EventSelection'
157 alg.eventDecisionOutputDecoration = 'ignore_anySelection_%SYS%'
158 alg.selection = '||'.join([sel + ',as_char' for sel in selections])
159 alg.noFilter = self.noFilter
160 alg.selectionName = 'pass_anySelection_%SYS%'
161 alg.decorationName = 'ntuplepass_anySelection_%SYS%'
162
163
164class EventSelectionConfig(ConfigBlock):
165 """ConfigBlock for interpreting text-based event selections"""
166
167 # N-object pT selectors that differ only by source container.
168 # keyword -> (container option, algorithm-name tag, plural noun)
169 _NOBJECT = {
170 "EL_N": ("electrons", "NEL", "electrons"),
171 "MU_N": ("muons", "NMU", "muons"),
172 "JET_N": ("jets", "NJET", "jets"),
173 "PH_N": ("photons", "NPH", "photons"),
174 "TAU_N": ("taus", "NTAU", "tau-jets"),
175 "LJET_N": ("largeRjets", "NLJET", "large-R jets"),
176 }
177 _KEYWORD_SPECS = None
178
179 def __init__(self):
180 super(EventSelectionConfig, self).__init__()
181 self.setBlockName('EventSelection')
182 self.addOption('selectionName', '', type=str,
183 noneAction='error',
184 info="the name of the event selection, used to uniquely identify "
185 "the `EventSelectionConfig` block.",
186 meta={'role':'region'})
187 self.addOption('electrons', "", type=str,
188 info="the input electron container, with a possible selection, in "
189 "the format `container` or `container.selection`.",
190 meta={'role':'containerRef'})
191 self.addOption('muons', "", type=str,
192 info="the input muon container, with a possible selection, in the "
193 "format `container` or `container.selection`.",
194 meta={'role':'containerRef'})
195 self.addOption('jets', "", type=str,
196 info="the input jet container, with a possible selection, in the "
197 "format `container` or `container.selection`.",
198 meta={'role':'containerRef'})
199 self.addOption('largeRjets', "", type=str,
200 info="the large-R jet container, with a possible selection, in "
201 "the format `container` or `container.selection`.",
202 meta={'role':'containerRef'})
203 self.addOption('photons', "", type=str,
204 info="the input photon container, with a possible selection, in "
205 "the format `container` or `container.selection`.",
206 meta={'role':'containerRef'})
207 self.addOption('taus', "", type=str,
208 info="the input tau-jet container, with a possible selection, in "
209 "the format `container` or `container.selection`.",
210 meta={'role':'containerRef'})
211 self.addOption('met', "", type=str,
212 info="the input MET container.",
213 meta={'role':'containerRef'})
214 self.addOption('metTerm', "Final", type=str,
215 info="the MET term to use when computing MET-based quantities.")
216 self.addOption('btagDecoration', "", type=str,
217 info="the b-tagging decoration to use when defining b-jets.")
218 self.addOption('preselection', "", type=str,
219 info="the event-wise selection flag to start this event selection "
220 "from.",
221 meta={'role':'region'})
222 self.addOption('selectionCuts', "", type=str,
223 noneAction='error',
224 info="a single string listing one selection cut per line. "
225 "See [available keywords](https://topcptoolkit.docs.cern.ch/latest/settings/eventselection/#available-keywords).")
226 self.addOption('debugMode', False, type=bool,
227 info="whether to create an output branch for every single line "
228 "of the selection cuts. Setting it to `False` only saves the"
229 " final decision.")
230 self.addOption('useDressedProperties', True, type=bool,
231 info="whether to use dressed truth electron and truth muon "
232 "kinematics rather than simple 4-vector kinematics.")
233 self.step = 0
235 self.cutflow = []
237
238 def instanceName (self) :
239 """Return the instance name for this block"""
240 return self.selectionName
241
243 """Map each keyword to its handler. Dispatch is an exact lookup on the
244 first token, which removes the ordering fragility of token-membership."""
245 d = {
246 "JET_N_BTAG": self.add_NBJET_selector,
247 "JET_N_GHOST": self.add_NJETGHOST_selector,
248 "LJET_N_GHOST": self.add_NLJETGHOST_selector,
249 "LJETMASS_N": self.add_NLJETMASS_selector,
250 "LJETMASSWINDOW_N": self.add_NLJETMASSWINDOW_selector,
251 "OBJ_N": self.add_NOBJ_selector,
252 "SUM_EL_N_MU_N": self.add_SUMNELNMU_selector,
253 "SUM_EL_N_MU_N_TAU_N": self.add_SUMNLEPTONS_selector,
254 "MET": self.add_MET_selector,
255 "MWT": self.add_MWT_selector,
256 "MET+MWT": self.add_METMWT_selector,
257 "MLL": self.add_MLL_selector,
258 "MLLWINDOW": self.add_MLLWINDOW_selector,
259 "MLL_OSSF": self.add_MLL_OSSF_selector,
260 "OS": partial(self._add_charge, osMode=True, tag="OS"),
261 "SS": partial(self._add_charge, osMode=False, tag="SS"),
262 "SAVE": self.add_SAVE,
263 "IMPORT": self.add_IMPORT,
264 "EVENTFLAG": self.add_EVENTFLAG,
265 "GLOBALTRIGMATCH": self.add_GLOBALTRIGMATCH,
266 "RUN_NUMBER": self.add_RUNNUMBER,
267 "EXPR": self.add_EXPR_selector,
268 "EVENTVAR": self.add_EVENTVAR_selector,
269 }
270 for kw, (attr, tag, _noun) in self._NOBJECT.items():
271 d[kw] = partial(self._add_nobject, attr=attr, tag=tag)
272 return d
273
274 # ------------------------------------------------------------------ #
275 # keyword specification (shared with the GUI and the parser) #
276 # ------------------------------------------------------------------ #
277
278 @classmethod
279 def _keywordSpecs(cls) -> dict:
280 """Machine-readable description of every `selectionCuts` keyword.
281
282 Spec fields:
283 * `info` (str, required): one-line description of the keyword.
284 * `args` (list): argument descriptors, in token order.
285 * `forms` (list of arg-lists): used instead of `args` for keywords
286 offering several fixed shapes that are not "required + optionals".
287 * `freeText` (True): everything after the keyword is a single
288 expression (EXPR only).
289 * `grammar` (dict): the vocabulary of that expression (EXPR only).
290 * `deprecated` (True): the keyword is deprecated (SAVE only).
291
292 Argument descriptor fields:
293 * `name` (str), `type` (str), and optionally `optional` (True),
294 `choices` (list), `pattern` (regex str), `signed` (True).
295 * `type` is one of 'str', 'float', 'int', 'sign', 'region', 'flag',
296 'container', where:
297 - 'sign' is one of `<` `>` `==` `>=` `<=`
298 - 'region' is the `selectionName` of another EventSelection
299 - 'flag' is a literal token, present or absent; the literal
300 is the arg's `name`, matched case-insensitively
301 - 'container' is a container reference, in the format `Name` or
302 `Name.selection`
303 * `signed` (True) marks a float that may be negative; every other
304 float goes through `check_float(requirePositive=True)`.
305
306 Filling rule (implemented by `parseArgs`):
307 1. `flag` arguments are lifted out of the token list first, by
308 case-insensitive match against the arg name.
309 2. Of what remains, required arguments are matched first; optional
310 ones are filled left-to-right with the surplus tokens, skipping an
311 optional whose `pattern` the candidate token does not match.
312 3. With `forms`, the first form whose token count and patterns fit
313 wins.
314 """
315 if cls._KEYWORD_SPECS is not None:
316 return cls._KEYWORD_SPECS
317
318 specs = {}
319 for kw, (_attr, _tag, noun) in cls._NOBJECT.items():
320 specs[kw] = {
321 'info': f'Count {noun} above a pT threshold',
322 'args': [
323 {'name': 'sel', 'type': 'str', 'optional': True},
324 {'name': 'ptmin', 'type': 'float'},
325 {'name': 'sign', 'type': 'sign'},
326 {'name': 'count', 'type': 'int'},
327 ],
328 }
329 specs.update({
330 'JET_N_BTAG': {
331 'info': 'Count b-tagged jets above the default b-tagging working '
332 'point, or above a custom one given as `tagger:WP`',
333 'args': [
334 {'name': 'sel', 'type': 'str', 'optional': True, 'pattern': '^[^:]+$'},
335 {'name': 'btag', 'type': 'str', 'optional': True, 'pattern': '^[^:]+:[^:]+$'},
336 {'name': 'sign', 'type': 'sign'},
337 {'name': 'count', 'type': 'int'},
338 ],
339 },
340 'JET_N_GHOST': {
341 'info': 'Count jets ghost-associated to a given particle, e.g. `B`, '
342 'or `B!C` to also veto a second ghost association',
343 'args': [
344 {'name': 'ghost', 'type': 'str', 'pattern': '^[A-Za-z]+(![A-Za-z]+)?$'},
345 {'name': 'ptmin', 'type': 'float', 'optional': True},
346 {'name': 'sign', 'type': 'sign'},
347 {'name': 'count', 'type': 'int'},
348 ],
349 },
350 'LJET_N_GHOST': {
351 'info': 'Count large-R jets ghost-associated to a given particle, e.g. '
352 '`B`, or `B!C` to also veto a second ghost association',
353 'args': [
354 {'name': 'ghost', 'type': 'str', 'pattern': '^[A-Za-z]+(![A-Za-z]+)?$'},
355 {'name': 'ptmin', 'type': 'float', 'optional': True},
356 {'name': 'sign', 'type': 'sign'},
357 {'name': 'count', 'type': 'int'},
358 ],
359 },
360 'LJETMASS_N': {
361 'info': 'Count large-R jets above a mass threshold',
362 'args': [
363 {'name': 'sel', 'type': 'str', 'optional': True},
364 {'name': 'minMass', 'type': 'float'},
365 {'name': 'sign', 'type': 'sign'},
366 {'name': 'count', 'type': 'int'},
367 ],
368 },
369 'LJETMASSWINDOW_N': {
370 'info': 'Count large-R jets inside (or, with `veto`, outside) a mass window',
371 'args': [
372 {'name': 'sel', 'type': 'str', 'optional': True},
373 {'name': 'lowMass', 'type': 'float'},
374 {'name': 'highMass', 'type': 'float'},
375 {'name': 'sign', 'type': 'sign'},
376 {'name': 'count', 'type': 'int'},
377 {'name': 'veto', 'type': 'flag', 'optional': True},
378 ],
379 },
380 'OBJ_N': {
381 'info': 'Count objects of an arbitrary container above a pT threshold',
382 'args': [
383 {'name': 'container', 'type': 'container'},
384 {'name': 'ptmin', 'type': 'float'},
385 {'name': 'sign', 'type': 'sign'},
386 {'name': 'count', 'type': 'int'},
387 ],
388 },
389 'SUM_EL_N_MU_N': {
390 'info': 'Count electrons and muons together, above a common or '
391 'per-flavour pT threshold',
392 'forms': [
393 [
394 {'name': 'ptmin', 'type': 'float'},
395 {'name': 'sign', 'type': 'sign'},
396 {'name': 'count', 'type': 'int'},
397 ],
398 [
399 {'name': 'ptEl', 'type': 'float'},
400 {'name': 'ptMu', 'type': 'float'},
401 {'name': 'sign', 'type': 'sign'},
402 {'name': 'count', 'type': 'int'},
403 ],
404 [
405 {'name': 'selEl', 'type': 'str'},
406 {'name': 'selMu', 'type': 'str'},
407 {'name': 'ptEl', 'type': 'float'},
408 {'name': 'ptMu', 'type': 'float'},
409 {'name': 'sign', 'type': 'sign'},
410 {'name': 'count', 'type': 'int'},
411 ],
412 ],
413 },
414 'SUM_EL_N_MU_N_TAU_N': {
415 'info': 'Count electrons, muons and tau-jets together, above a common '
416 'or per-flavour pT threshold',
417 'forms': [
418 [
419 {'name': 'ptmin', 'type': 'float'},
420 {'name': 'sign', 'type': 'sign'},
421 {'name': 'count', 'type': 'int'},
422 ],
423 [
424 {'name': 'ptEl', 'type': 'float'},
425 {'name': 'ptMu', 'type': 'float'},
426 {'name': 'ptTau', 'type': 'float'},
427 {'name': 'sign', 'type': 'sign'},
428 {'name': 'count', 'type': 'int'},
429 ],
430 [
431 {'name': 'selEl', 'type': 'str'},
432 {'name': 'selMu', 'type': 'str'},
433 {'name': 'selTau', 'type': 'str'},
434 {'name': 'ptEl', 'type': 'float'},
435 {'name': 'ptMu', 'type': 'float'},
436 {'name': 'ptTau', 'type': 'float'},
437 {'name': 'sign', 'type': 'sign'},
438 {'name': 'count', 'type': 'int'},
439 ],
440 ],
441 },
442 'MET': {
443 'info': 'Cut on the missing transverse energy',
444 'args': [
445 {'name': 'sign', 'type': 'sign'},
446 {'name': 'refMET', 'type': 'float'},
447 ],
448 },
449 'MWT': {
450 'info': 'Cut on the transverse mass of the leading lepton and MET',
451 'args': [
452 {'name': 'sign', 'type': 'sign'},
453 {'name': 'refMWT', 'type': 'float'},
454 ],
455 },
456 'MET+MWT': {
457 'info': 'Cut on the sum of the missing transverse energy and the '
458 'transverse mass',
459 'args': [
460 {'name': 'sign', 'type': 'sign'},
461 {'name': 'refMETMWT', 'type': 'float'},
462 ],
463 },
464 'MLL': {
465 'info': 'Cut on the light dilepton invariant mass (optionally including taus with `include_taus`)',
466 'args': [
467 {'name': 'sign', 'type': 'sign'},
468 {'name': 'refMLL', 'type': 'float'},
469 {'name': 'include_taus', 'type': 'flag', 'optional': True},
470 ],
471 },
472 'MLLWINDOW': {
473 'info': 'Require the light dilepton invariant mass inside (or, with `veto`, '
474 'outside) a mass window (optionally including taus with `include_taus`)',
475 'args': [
476 {'name': 'lowMLL', 'type': 'float'},
477 {'name': 'highMLL', 'type': 'float'},
478 {'name': 'veto', 'type': 'flag', 'optional': True},
479 {'name': 'include_taus', 'type': 'flag', 'optional': True},
480 ],
481 },
482 'MLL_OSSF': {
483 'info': 'Require the opposite-sign same-flavour light dilepton invariant mass '
484 'inside (or, with `veto`, outside) a mass window (optionally including taus with `include_taus`)',
485 'args': [
486 {'name': 'lowMll', 'type': 'float'},
487 {'name': 'highMll', 'type': 'float'},
488 {'name': 'veto', 'type': 'flag', 'optional': True},
489 {'name': 'include_taus', 'type': 'flag', 'optional': True},
490 ],
491 },
492 'OS': {
493 'info': 'Require an opposite-sign lepton pair; without any flag, all '
494 'available lepton flavours are considered',
495 'args': [
496 {'name': 'el', 'type': 'flag', 'optional': True},
497 {'name': 'mu', 'type': 'flag', 'optional': True},
498 {'name': 'tau', 'type': 'flag', 'optional': True},
499 ],
500 },
501 'SS': {
502 'info': 'Require a same-sign lepton pair; without any flag, all '
503 'available lepton flavours are considered',
504 'args': [
505 {'name': 'el', 'type': 'flag', 'optional': True},
506 {'name': 'mu', 'type': 'flag', 'optional': True},
507 {'name': 'tau', 'type': 'flag', 'optional': True},
508 ],
509 },
510 'IMPORT': {
511 'info': 'Import all the cuts of a previously defined event selection',
512 'args': [
513 {'name': 'region', 'type': 'region'},
514 ],
515 },
516 'EVENTFLAG': {
517 'info': 'Require an existing event-wise decoration to be true',
518 'args': [
519 {'name': 'decoration', 'type': 'str'},
520 ],
521 },
522 'GLOBALTRIGMATCH': {
523 'info': 'Require the global trigger matching decision, optionally for '
524 'a given trigger-configuration postfix',
525 'args': [
526 {'name': 'postfix', 'type': 'str', 'optional': True},
527 ],
528 },
529 'RUN_NUMBER': {
530 'info': 'Cut on the (random) run number',
531 'args': [
532 {'name': 'sign', 'type': 'sign'},
533 {'name': 'runNumber', 'type': 'int'},
534 ],
535 },
536 'EVENTVAR': {
537 'info': 'Cut on an existing EventInfo scalar variable, e.g. a DNN or '
538 'BDT discriminant',
539 'args': [
540 {'name': 'type', 'type': 'str', 'choices': sorted(cls._EVENTVAR_TYPES)},
541 {'name': 'name', 'type': 'str'},
542 {'name': 'sign', 'type': 'sign'},
543 {'name': 'value', 'type': 'float', 'signed': True},
544 ],
545 },
546 'EXPR': {
547 'info': 'Cut on a generic object-kinematic expression, e.g. '
548 '`dR(el[0],jet[0]) > 0.4`',
549 'freeText': True,
550 'grammar': {
551 'collections': sorted(cls._EXPR_COLL),
552 'variables': sorted(cls._EXPR_VARS),
553 },
554 },
555 'SAVE': {
556 'info': 'Deprecated and ignored: the event filter is now emitted '
557 'automatically at the end of every event selection',
558 'deprecated': True,
559 'args': [],
560 },
561 })
562 cls._KEYWORD_SPECS = specs
563 return cls._KEYWORD_SPECS
564
565 @classmethod
566 def keywordSpecs(cls) -> dict:
567 """Return an independent copy of the keyword specification table."""
568 return deepcopy(cls._keywordSpecs())
569
570 @classmethod
571 def _argCounts(cls, spec):
572 """Return the set of valid total token counts (leading keyword included)
573 allowed by a keyword specification, or None when the keyword takes free
574 text and no count check applies."""
575 if spec.get('freeText'):
576 return None
577 forms = spec.get('forms') or [spec.get('args', [])]
578 counts = set()
579 for form in forms:
580 min_args = sum(1 for arg in form if not arg.get('optional'))
581 max_args = len(form)
582 counts.update(n + 1 for n in range(min_args, max_args + 1))
583 return counts
584
585 def makeAlgs(self, config):
586 existing = config.getContainerMeta('EventInfo', 'eventSelectionNames', defaultValue=[])
587 config.setContainerMeta('EventInfo', 'eventSelectionNames',
588 existing + [f'pass_{self.selectionName}_%SYS%'], allowOverwrite=True)
589
590 # need to re-initialize here to deal with multiple passes
591 self.step = 0
592 # initialize the pre-selection
593 self.currentDecoration = self.preselection
594 # re-initialize the cutflow
595 self.cutflow = []
596 # read the selection cuts
597 if self.selectionCuts is None:
598 raise ValueError ("[EventSelectionConfig] You must provide the 'selectionCuts' option to 'EventSelectionConfig': "
599 "a single string where each line represents a different selection cut to apply in order.")
600 for line in self.selectionCuts.split("\n"):
601 self.interpret(line, config)
602 # the event filter is always created automatically at the end of the
603 # block; an explicit SAVE line only triggers a deprecation warning
604 self._emit_save(config)
605 config.addEventCutFlow(self.selectionName, self.getCutflow())
606
607 def interpret(self, text, cfg):
608 text = text.strip()
609 if not text or text.startswith("#"):
610 return
611 self.step += 1
612 keyword = text.split()[0]
613 handler = self._dispatch.get(keyword)
614 if handler is None:
615 raise ValueError (f"[EventSelectionConfig] The following selection cut is not recognised! --> {text}")
616 handler(text, cfg)
617
618 # ------------------------------------------------------------------ #
619 # validation helpers #
620 # ------------------------------------------------------------------ #
621
622 def raise_misconfig(self, text, keyword):
623 raise ValueError (f"[EventSelectionConfig] Misconfiguration! Check {keyword} in: {text}")
624
625 def raise_missinginput(self, collection):
626 raise ValueError (f"[EventSelectionConfig] Misconfiguration! Missing input collection for {collection}")
627
628 def _check_args(self, items, keyword):
629 """Validate the leading keyword and the number of arguments, the latter
630 derived from the keyword specification table."""
631 if items[0] != keyword:
632 self.raise_misconfig(' '.join(items), keyword)
633 validCounts = self._argCounts(self._keywordSpecs()[keyword])
634 if validCounts is not None and len(items) not in validCounts:
635 self.raise_misconfig(' '.join(items), "number of arguments")
636
637 def _extract_flags(self, form, tokens):
638 """Remove flag tokens from `tokens` and return their boolean values."""
639 tokens = list(tokens)
640 values = {}
641 for arg in form:
642 if arg.get('type') != 'flag':
643 continue
644 values[arg['name']] = False
645 for i, token in enumerate(tokens):
646 if token.lower() == arg['name'].lower():
647 del tokens[i]
648 values[arg['name']] = True
649 break
650 return tokens, values
651
652 def _match_form(self, form, tokens):
653 """Match one keyword argument form, returning values or None."""
654 tokens, values = self._extract_flags(form, tokens)
655 positional = [arg for arg in form if arg.get('type') != 'flag']
656 nRequired = sum(1 for arg in positional if not arg.get('optional'))
657 if len(tokens) < nRequired or len(tokens) > len(positional):
658 return None
659
660 budget = len(tokens) - nRequired
661 cursor = 0
662 for arg in positional:
663 if not arg.get('optional'):
664 values[arg['name']] = tokens[cursor]
665 cursor += 1
666 continue
667 pattern = arg.get('pattern')
668 if budget > 0 and (pattern is None or re.fullmatch(pattern, tokens[cursor])):
669 values[arg['name']] = tokens[cursor]
670 cursor += 1
671 budget -= 1
672 else:
673 values[arg['name']] = ''
674 return values if cursor == len(tokens) else None
675
676 def parseArgs(self, keyword, items):
677 """Turn the already-split token list `items` (leading keyword included)
678 into a dict of argument name -> value, driven by `keywordSpecs`, which
679 is authoritative for the argument grammar."""
680 self._check_args(items, keyword)
681 spec = self._keywordSpecs()[keyword]
682 if spec.get('freeText'):
683 return {'text': ' '.join(items[1:])}
684 for form in (spec.get('forms') or [spec.get('args', [])]):
685 values = self._match_form(form, items[1:])
686 if values is not None:
687 return values
688 self.raise_misconfig(' '.join(items), "number of arguments")
689
690 def check_float(self, test, requirePositive=True):
691 try:
692 value = float(test)
693 if not requirePositive or value >= 0:
694 return value
695 else:
696 raise ValueError (f"[EventSelectionConfig] Misconfiguration! Float {test} is not positive!")
697 except ValueError:
698 raise ValueError (f"[EventSelectionConfig] Misconfiguration! {test} should be a float, not {type(test)}!")
699
700 def check_int(self, test, requirePositive=True):
701 try:
702 numeric = float(test)
703 value = int(numeric)
704 except (TypeError, ValueError):
705 raise ValueError (f"[EventSelectionConfig] Misconfiguration! {test} should be an int, not {type(test)}")
706 if value != numeric:
707 raise ValueError (f"[EventSelectionConfig] Misconfiguration! {test} should be an int, not a float!")
708 if requirePositive and value < 0:
709 raise ValueError (f"[EventSelectionConfig] Misconfiguration! Int {test} is not positive!")
710 return value
711
712 def check_string(self, test):
713 if not isinstance(test, str):
714 raise ValueError (f"[EventSelectionConfig] Misconfiguration! {test} should be a string, not a number!")
715 else:
716 return test
717
718 def check_sign(self, test):
719 mapping = {
720 "<" : "LT",
721 ">" : "GT",
722 "==": "EQ",
723 ">=": "GE",
724 "<=": "LE"
725 }
726 try:
727 return mapping[test]
728 except KeyError:
729 raise KeyError (f"[EventSelectionConfig] Misconfiguration! {test} should be one of {list(mapping.keys())}")
730
731 def check_btagging(self, test):
732 test = test.split(":")
733 if len(test) != 2:
734 raise ValueError (f"[EventSelectionConfig] Misconfiguration! {test} should be provided as 'btagger:btagWP'")
735 else:
736 return test
737
738 def check_ghosts(self, test):
739 test = self.check_string(test)
740 values = test.split("!")
741 ghost_map = {
742 "B": "GhostBHadronsFinalCount",
743 "C": "GhostCHadronsFinalCount",
744 "T": "GhostTQuarksFinalCount",
745 "W": "GhostWBosonsCount",
746 "Z": "GhostZBosonsCount",
747 "H": "GhostHBosonsCount",
748 "TAU": "GhostTausFinalCount"
749 }
750 return [ghost_map.get(value.upper(), value) for value in values]
751
752 # ------------------------------------------------------------------ #
753 # decoration / selection bookkeeping #
754 # ------------------------------------------------------------------ #
755
756 def getCutflow(self):
757 return self.cutflow
758
759 def setDecorationName(self, algorithm, config, decoration):
760 self.cutflow.append( decoration )
761 if algorithm is not None:
762 algorithm.decorationName = f'{decoration},as_char'
763 self.currentDecoration = decoration
764 if self.debugMode:
765 config.addOutputVar('EventInfo', decoration, decoration.split("_%SYS%")[0])
766 else:
767 if self.currentDecoration:
768 self.currentDecoration += '&&' + decoration
769 else:
770 self.currentDecoration = decoration
771 config.addSelection('EventInfo', '', decoration)
772
773 def checkDecorationName(self, decoration):
774 if decoration == '':
775 return decoration
776 decoration = decoration.split("&&")
777 decoration = [sub + ',as_char' if ',as_char' not in sub else sub for sub in decoration]
778 return '&&'.join(decoration)
779
780 def extendObjectSelection(self, config, container, oldSelection, newSelection):
781 if oldSelection:
782 return oldSelection + "&&" + config.getFullSelection(container, newSelection)
783 else:
784 return config.getFullSelection(container, newSelection)
785
786 # ------------------------------------------------------------------ #
787 # shared selector helpers #
788 # ------------------------------------------------------------------ #
789
790 def _require_inputs(self, *names, message=None):
791 """Require at least one of the named input options to be configured."""
792 if not any(getattr(self, name) for name in names):
793 self.raise_missinginput(message or " or ".join(names))
794
795 def _finish_selector(self, alg, config, name):
796 """Apply the common event-preselection and output decoration."""
797 alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
798 self.setDecorationName(alg, config, f'{name}_%SYS%')
799
800 def _configure_leptons(self, alg, config, leptons):
801 """Configure reco/truth lepton handles from explicit metadata."""
802 for spec, reco, truth in leptons:
803 if spec:
804 self._route_lepton(alg, config, spec, reco, truth)
805 self._maybe_dressed(alg, *(spec for spec, _reco, _truth in leptons))
806
807 def _maybe_dressed(self, alg, *specs):
808 """Enable dressed kinematics when any of the given electron/muon
809 containers is a truth container. Dressed kinematics only exist for
810 truth electrons and muons, so only those specs should be passed here."""
811 if any(spec and ("Particle" in spec or "Truth" in spec) for spec in specs):
812 alg.useDressedProperties = self.useDressedProperties
813
814 def _route_lepton(self, alg, config, spec, reco, truth):
815 """Assign (name, selection) to the reco or truth handles of `alg`
816 depending on whether `spec` points to a truth container.
817 `reco`/`truth` are (nameAttr, selectionAttr) pairs."""
818 name, sel = config.readNameAndSelection(spec)
819 nameAttr, selAttr = truth if ("Particle" in spec or "Truth" in spec) else reco
820 setattr(alg, nameAttr, name)
821 setattr(alg, selAttr, sel)
822
823 # ------------------------------------------------------------------ #
824 # selector builders #
825 # ------------------------------------------------------------------ #
826
827 def _add_nobject(self, text, config, *, attr, tag):
828 """Generic builder for the N-object pT selectors (EL_N, MU_N, JET_N,
829 PH_N, TAU_N, LJET_N): identical except for the source container, which
830 is always required since the cut acts on it."""
831 items = text.split()
832 spec = getattr(self, attr)
833 self._require_inputs(attr)
834 # the keyword is not known to this generic builder: read it back
835 args = self.parseArgs(items[0], items)
836 thisalg = f'{self.selectionName}_{tag}_{self.step}'
837 alg = config.createAlgorithm('CP::NObjectPtSelectorAlg', thisalg)
838 alg.particles, alg.objectSelection = config.readNameAndSelection(spec)
839 if attr in ("electrons", "muons"):
840 self._maybe_dressed(alg, spec)
841 if args['sel']:
842 alg.objectSelection = self.extendObjectSelection(
843 config, spec.split(".")[0], alg.objectSelection,
844 self.check_string(args['sel']))
845 alg.minPt = self.check_float(args['ptmin'])
846 alg.sign = self.check_sign(args['sign'])
847 alg.count = self.check_int(args['count'])
848 self._finish_selector(alg, config, thisalg)
849
850 def add_IMPORT(self, text, config):
851 # this is used to import a previous selection
852 args = self.parseArgs("IMPORT", text.split())
853 region = self.check_string(args['region'])
854 if not self.currentDecoration:
855 self.currentDecoration = f'pass_{region}_%SYS%,as_char'
856 else:
857 self.currentDecoration = f'{self.currentDecoration},as_char&&pass_{region}_%SYS%'
858 # for the cutflow, we need to retrieve all the cuts corresponding to this IMPORT
859 imported_cuts = [cut for cut in config.getSelectionCutFlow('EventInfo', '') if cut.startswith(region)]
860 self.cutflow += imported_cuts
861
862 def add_NBJET_selector(self, text, config):
863 args = self.parseArgs("JET_N_BTAG", text.split())
864 self._require_inputs("jets")
865 thisalg = f'{self.selectionName}_NBJET_{self.step}'
866 alg = config.createAlgorithm('CP::NObjectPtSelectorAlg', thisalg)
867 particles, selection = config.readNameAndSelection(self.jets)
868 alg.particles = particles
869 alg.objectSelection = f'{selection}&&{self.btagDecoration},as_char' if selection else f'{self.btagDecoration},as_char'
870 if args['btag']:
871 btagger, btagWP = self.check_btagging(args['btag'])
872 customBtag = f'ftag_select_{btagger}_{btagWP}'
873 alg.objectSelection = f'{selection}&&{customBtag},as_char' if selection else f'{customBtag},as_char'
874 if args['sel']:
875 extraSel = self.check_string(args['sel'])
876 alg.objectSelection = self.extendObjectSelection(config, self.jets.split(".")[0], alg.objectSelection, extraSel)
877 alg.sign = self.check_sign(args['sign'])
878 alg.count = self.check_int(args['count'])
879 self._finish_selector(alg, config, thisalg)
880
881 def add_SUMNELNMU_selector(self, text, config):
882 args = self.parseArgs("SUM_EL_N_MU_N", text.split())
883 self._require_inputs("electrons", "muons")
884 thisalg = f'{self.selectionName}_SUMNELNMU_{self.step}'
885 alg = config.createAlgorithm('CP::SumNLeptonPtSelectorAlg', thisalg)
886 alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
887 alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
888 self._maybe_dressed(alg, self.electrons, self.muons)
889 if args.get('ptmin') is not None:
890 # a single threshold, applied to every flavour
891 alg.minPtEl = self.check_float(args['ptmin'])
892 alg.minPtMu = self.check_float(args['ptmin'])
893 else:
894 if args.get('selEl'):
895 extraSelEl = self.check_string(args['selEl'])
896 alg.electronSelection = self.extendObjectSelection(config, self.electrons.split(".")[0], alg.electronSelection, extraSelEl)
897 if args.get('selMu'):
898 extraSelMu = self.check_string(args['selMu'])
899 alg.muonSelection = self.extendObjectSelection(config, self.muons.split(".")[0], alg.muonSelection, extraSelMu)
900 alg.minPtEl = self.check_float(args['ptEl'])
901 alg.minPtMu = self.check_float(args['ptMu'])
902 alg.sign = self.check_sign(args['sign'])
903 alg.count = self.check_int(args['count'])
904 self._finish_selector(alg, config, thisalg)
905
906 def add_SUMNLEPTONS_selector(self, text, config):
907 args = self.parseArgs("SUM_EL_N_MU_N_TAU_N", text.split())
908 self._require_inputs("electrons", "muons", "taus", message="electrons, muons or taus")
909 thisalg = f'{self.selectionName}_SUMNLEPTONS_{self.step}'
910 alg = config.createAlgorithm('CP::SumNLeptonPtSelectorAlg', thisalg)
911 alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
912 alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
913 alg.taus, alg.tauSelection = config.readNameAndSelection(self.taus)
914 self._maybe_dressed(alg, self.electrons, self.muons)
915 if args.get('ptmin') is not None:
916 # a single threshold, applied to every flavour
917 alg.minPtEl = self.check_float(args['ptmin'])
918 alg.minPtMu = self.check_float(args['ptmin'])
919 alg.minPtTau = self.check_float(args['ptmin'])
920 else:
921 if args.get('selEl'):
922 extraSelEl = self.check_string(args['selEl'])
923 alg.electronSelection = self.extendObjectSelection(config, self.electrons.split(".")[0], alg.electronSelection, extraSelEl)
924 if args.get('selMu'):
925 extraSelMu = self.check_string(args['selMu'])
926 alg.muonSelection = self.extendObjectSelection(config, self.muons.split(".")[0], alg.muonSelection, extraSelMu)
927 if args.get('selTau'):
928 extraSelTau = self.check_string(args['selTau'])
929 alg.tauSelection = self.extendObjectSelection(config, self.taus.split(".")[0], alg.tauSelection, extraSelTau)
930 alg.minPtEl = self.check_float(args['ptEl'])
931 alg.minPtMu = self.check_float(args['ptMu'])
932 alg.minPtTau = self.check_float(args['ptTau'])
933 alg.sign = self.check_sign(args['sign'])
934 alg.count = self.check_int(args['count'])
935 self._finish_selector(alg, config, thisalg)
936
937 def add_NLJETMASS_selector(self, text, config):
938 args = self.parseArgs("LJETMASS_N", text.split())
939 thisalg = f'{self.selectionName}_NLJETMASS_{self.step}'
940 alg = config.createAlgorithm('CP::NObjectMassSelectorAlg', thisalg)
941 alg.particles, alg.objectSelection = config.readNameAndSelection(self.largeRjets)
942 if args['sel']:
943 alg.objectSelection = self.extendObjectSelection(
944 config, self.largeRjets.split(".")[0], alg.objectSelection,
945 self.check_string(args['sel']))
946 alg.minMass = self.check_float(args['minMass'])
947 alg.sign = self.check_sign(args['sign'])
948 alg.count = self.check_int(args['count'])
949 self._finish_selector(alg, config, thisalg)
950
951 def add_NLJETMASSWINDOW_selector(self, text, config):
952 args = self.parseArgs("LJETMASSWINDOW_N", text.split())
953 thisalg = f'{self.selectionName}_NLJETMASSWINDOW_{self.step}'
954 alg = config.createAlgorithm('CP::NLargeRJetMassWindowSelectorAlg', thisalg)
955 alg.ljets, alg.ljetSelection = config.readNameAndSelection(self.largeRjets)
956 if args['sel']:
957 extraSel = self.check_string(args['sel'])
958 alg.ljetSelection = self.extendObjectSelection(config, self.largeRjets.split(".")[0], alg.ljetSelection, extraSel)
959 alg.lowMass = self.check_float(args['lowMass'])
960 alg.highMass = self.check_float(args['highMass'])
961 alg.sign = self.check_sign(args['sign'])
962 alg.count = self.check_int(args['count'])
963 alg.vetoMode = args['veto']
964 self._finish_selector(alg, config, thisalg)
965
966 def add_NJETGHOST_selector(self, text, config):
967 args = self.parseArgs("JET_N_GHOST", text.split())
968 thisalg = f'{self.selectionName}_NJETGHOST_{self.step}'
969 alg = config.createAlgorithm('CP::JetNGhostSelectorAlg', thisalg)
970 alg.jets, alg.jetSelection = config.readNameAndSelection(self.jets)
971 ghosts = self.check_ghosts(args['ghost'])
972 alg.ghost = ghosts[0]
973 if len(ghosts) > 1 :
974 alg.veto = ghosts[1]
975 if args['ptmin']:
976 alg.minPt = self.check_float(args['ptmin'])
977 alg.sign = self.check_sign(args['sign'])
978 alg.count = self.check_int(args['count'])
979 self._finish_selector(alg, config, thisalg)
980
981 def add_NLJETGHOST_selector(self, text, config):
982 args = self.parseArgs("LJET_N_GHOST", text.split())
983 thisalg = f'{self.selectionName}_NLJETGHOST_{self.step}'
984 alg = config.createAlgorithm('CP::JetNGhostSelectorAlg', thisalg)
985 alg.jets, alg.jetSelection = config.readNameAndSelection(self.largeRjets)
986 ghosts = self.check_ghosts(args['ghost'])
987 alg.ghost = ghosts[0]
988 if len(ghosts) > 1 :
989 alg.veto = ghosts[1]
990 if args['ptmin']:
991 alg.minPt = self.check_float(args['ptmin'])
992 alg.sign = self.check_sign(args['sign'])
993 alg.count = self.check_int(args['count'])
994 self._finish_selector(alg, config, thisalg)
995
996 def add_NOBJ_selector(self, text, config):
997 args = self.parseArgs("OBJ_N", text.split())
998 thisalg = f'{self.selectionName}_NOBJ_{self.step}'
999 alg = config.createAlgorithm('CP::NObjectPtSelectorAlg', thisalg)
1000 alg.particles, alg.objectSelection = config.readNameAndSelection(self.check_string(args['container']))
1001 alg.minPt = self.check_float(args['ptmin'])
1002 alg.sign = self.check_sign(args['sign'])
1003 alg.count = self.check_int(args['count'])
1004 self._finish_selector(alg, config, thisalg)
1005
1006 def add_MET_selector(self, text, config):
1007 args = self.parseArgs("MET", text.split())
1008 self._require_inputs("met", message="MET")
1009 thisalg = f'{self.selectionName}_MET_{self.step}'
1010 alg = config.createAlgorithm('CP::MissingETSelectorAlg', thisalg)
1011 alg.met = config.readName(self.met)
1012 alg.metTerm = self.metTerm
1013 alg.sign = self.check_sign(args['sign'])
1014 alg.refMET = self.check_float(args['refMET'])
1015 self._finish_selector(alg, config, thisalg)
1016
1017 def add_MWT_selector(self, text, config):
1018 args = self.parseArgs("MWT", text.split())
1019 self._require_inputs("electrons", "muons")
1020 thisalg = f'{self.selectionName}_MWT_{self.step}'
1021 alg = config.createAlgorithm('CP::TransverseMassSelectorAlg', thisalg)
1022 alg.met = config.readName(self.met)
1023 alg.metTerm = self.metTerm
1024 alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
1025 alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
1026 self._maybe_dressed(alg, self.electrons, self.muons)
1027 alg.sign = self.check_sign(args['sign'])
1028 alg.refMWT = self.check_float(args['refMWT'])
1029 self._finish_selector(alg, config, thisalg)
1030
1031 def add_METMWT_selector(self, text, config):
1032 args = self.parseArgs("MET+MWT", text.split())
1033 self._require_inputs("met", message="MET")
1034 self._require_inputs("electrons", "muons")
1035 thisalg = f'{self.selectionName}_METMWT_{self.step}'
1036 alg = config.createAlgorithm('CP::MissingETPlusTransverseMassSelectorAlg', thisalg)
1037 alg.met = config.readName(self.met)
1038 alg.metTerm = self.metTerm
1039 alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
1040 alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
1041 self._maybe_dressed(alg, self.electrons, self.muons)
1042 alg.sign = self.check_sign(args['sign'])
1043 alg.refMETMWT = self.check_float(args['refMETMWT'])
1044 self._finish_selector(alg, config, thisalg)
1045
1046 def add_MLL_selector(self, text, config):
1047 args = self.parseArgs("MLL", text.split())
1048 self._require_inputs("electrons", "muons")
1049 thisalg = f'{self.selectionName}_MLL_{self.step}'
1050 alg = config.createAlgorithm('CP::DileptonInvariantMassSelectorAlg', thisalg)
1051 self._configure_leptons(alg, config, (
1052 (self.electrons, ('electrons', 'electronSelection'),
1053 ('truthElectrons', 'truthElectronSelection')),
1054 (self.muons, ('muons', 'muonSelection'),
1055 ('truthMuons', 'truthMuonSelection')),
1056 ))
1057 if args['include_taus']:
1058 self._require_inputs("taus")
1059 self._configure_leptons(alg, config, (
1060 (self.taus, ('taus', 'tauSelection'),
1061 ('truthTaus', 'truthTauSelection')),
1062 ))
1063 alg.sign = self.check_sign(args['sign'])
1064 alg.refMLL = self.check_float(args['refMLL'])
1065 self._finish_selector(alg, config, thisalg)
1066
1067 def add_MLLWINDOW_selector(self, text, config):
1068 args = self.parseArgs("MLLWINDOW", text.split())
1069 self._require_inputs("electrons", "muons")
1070 thisalg = f'{self.selectionName}_MLLWINDOW_{self.step}'
1071 alg = config.createAlgorithm('CP::DileptonInvariantMassWindowSelectorAlg', thisalg)
1072 self._configure_leptons(alg, config, (
1073 (self.electrons, ('electrons', 'electronSelection'),
1074 ('truthElectrons', 'truthElectronSelection')),
1075 (self.muons, ('muons', 'muonSelection'),
1076 ('truthMuons', 'truthMuonSelection')),
1077 ))
1078 if args['include_taus']:
1079 self._require_inputs("taus")
1080 self._configure_leptons(alg, config, (
1081 (self.taus, ('taus', 'tauSelection'),
1082 ('truthTaus', 'truthTauSelection')),
1083 ))
1084 alg.lowMLL = self.check_float(args['lowMLL'])
1085 alg.highMLL = self.check_float(args['highMLL'])
1086 alg.vetoMode = args['veto']
1087 self._finish_selector(alg, config, thisalg)
1088
1089 def _add_charge(self, text, config, *, osMode, tag):
1090 """Builder shared by OS and SS: same algorithm, opposite charge mode."""
1091 args = self.parseArgs(tag, text.split())
1092 self._require_inputs("electrons", "muons", "taus")
1093 thisalg = f'{self.selectionName}_{tag}_{self.step}'
1094 alg = config.createAlgorithm('CP::ChargeSelectorAlg', thisalg)
1095 allLeptons = not (args['el'] or args['mu'] or args['tau'])
1096 if self.electrons and (allLeptons or args['el']):
1097 self._route_lepton(alg, config, self.electrons,
1098 ('electrons', 'electronSelection'),
1099 ('truthElectrons', 'truthElectronSelection'))
1100 if self.muons and (allLeptons or args['mu']):
1101 self._route_lepton(alg, config, self.muons,
1102 ('muons', 'muonSelection'),
1103 ('truthMuons', 'truthMuonSelection'))
1104 if self.taus and (allLeptons or args['tau']):
1105 self._route_lepton(alg, config, self.taus,
1106 ('taus', 'tauSelection'),
1107 ('truthTaus', 'truthTauSelection'))
1108 alg.OS = osMode
1109 self._finish_selector(alg, config, thisalg)
1110
1111 def add_MLL_OSSF_selector(self, text, config):
1112 args = self.parseArgs("MLL_OSSF", text.split())
1113 self._require_inputs("electrons", "muons")
1114 thisalg = f'{self.selectionName}_MLL_OSSF_{self.step}'
1115 alg = config.createAlgorithm('CP::DileptonOSSFInvariantMassWindowSelectorAlg', thisalg)
1116 self._configure_leptons(alg, config, (
1117 (self.electrons, ('electrons', 'electronSelection'),
1118 ('truthElectrons', 'truthElectronSelection')),
1119 (self.muons, ('muons', 'muonSelection'),
1120 ('truthMuons', 'truthMuonSelection')),
1121 ))
1122 if args['include_taus']:
1123 self._require_inputs("taus")
1124 self._configure_leptons(alg, config, (
1125 (self.taus, ('taus', 'tauSelection'),
1126 ('truthTaus', 'truthTauSelection')),
1127 ))
1128 alg.lowMll = self.check_float(args['lowMll'])
1129 alg.highMll = self.check_float(args['highMll'])
1130 alg.vetoMode = args['veto']
1131 self._finish_selector(alg, config, thisalg)
1132
1133 def add_EVENTFLAG(self, text, config):
1134 args = self.parseArgs("EVENTFLAG", text.split())
1135 existingDecoration = self.check_string(args['decoration'])
1136 self.setDecorationName(None, config, existingDecoration)
1137
1138 def add_GLOBALTRIGMATCH(self, text, config):
1139 args = self.parseArgs("GLOBALTRIGMATCH", text.split())
1140 postfix = self.check_string(args['postfix'])
1141 self.setDecorationName(None, config, f"globalTriggerMatch{postfix}_%SYS%,as_char")
1142
1143 def add_RUNNUMBER(self, text, config):
1144 args = self.parseArgs("RUN_NUMBER", text.split())
1145 thisalg = f'{self.selectionName}_RUN_NUMBER_{self.step}'
1146 alg = config.createAlgorithm('CP::RunNumberSelectorAlg', thisalg)
1147 alg.sign = self.check_sign(args['sign'])
1148 alg.runNumber = self.check_int(args['runNumber'])
1149 alg.useRandomRunNumber = config.dataType() is not DataType.Data
1150 self._finish_selector(alg, config, thisalg)
1151
1152 # ------------------------------------------------------------------ #
1153 # EXPR: generic object-kinematic expression cuts #
1154 # ------------------------------------------------------------------ #
1155
1156 # collection token -> metadata
1157 _EXPR_COLL = {
1158 "jet": _ExprCollection("jets"),
1159 "bjet": _ExprCollection("jets", apply_btag=True),
1160 "el": _ExprCollection("electrons"),
1161 "mu": _ExprCollection("muons"),
1162 "tau": _ExprCollection("taus"),
1163 "ph": _ExprCollection("photons"),
1164 "ljet": _ExprCollection("largeRjets"),
1165 "met": _ExprCollection("met", is_met=True),
1166 }
1167
1168 # variable -> metadata
1169 _EXPR_VARS = {
1170 "dR": _ExprVariable(2, 2, ",", True, False),
1171 "dEta": _ExprVariable(2, 2, ",", True, False),
1172 "dPhi": _ExprVariable(2, 2, ",", False, True),
1173 "m": _ExprVariable(1, None, "+", False, False),
1174 "e": _ExprVariable(1, None, "+", False, False),
1175 "pt": _ExprVariable(1, None, "+", False, True),
1176 "eta": _ExprVariable(1, 1, None, True, False),
1177 "phi": _ExprVariable(1, 1, None, False, True),
1178 }
1179
1180 _EXPR_TOKEN_RE = re.compile(r"""
1181 (?P<NUM>\d+\.\d+(?:[eE][+-]?\d+)?|\d+[eE][+-]?\d+|\d+)
1182 | (?P<GE>>=) | (?P<LE><=) | (?P<EQ>==) | (?P<LT><) | (?P<GT>>)
1183 | (?P<ID>[A-Za-z_][A-Za-z0-9_]*)
1184 | (?P<LP>\‍() | (?P<RP>\‍)) | (?P<LB>\‍[) | (?P<RB>\‍])
1185 | (?P<COMMA>,) | (?P<PLUS>\+) | (?P<MINUS>-)
1186 | (?P<WS>\s+)
1187 """, re.VERBOSE)
1188
1189
1190 def _expr_tokenize(self, text):
1191 return _ExprParser(self._EXPR_TOKEN_RE).tokenize(text)
1192
1193 def _expr_parse(self, tokens):
1194 return _ExprParser(self._EXPR_TOKEN_RE).parse(tokens)
1195
1196 def _expr_validate(self, variable, separator, operands):
1197 if variable not in self._EXPR_VARS:
1199 f"[EventSelectionConfig] EXPR: variable '{variable}' is not available. "
1200 "Please request it from the EventSelectionAlgorithms developers.")
1201 var_spec = self._EXPR_VARS[variable]
1202 minN = var_spec.min_operands
1203 maxN = var_spec.max_operands
1204 sep = var_spec.separator
1205 metOk = var_spec.met_ok
1206 n = len(operands)
1207 if n < minN or (maxN is not None and n > maxN):
1208 expected = f"{minN}" if maxN == minN else (f"{minN}+" if maxN is None else f"{minN}-{maxN}")
1210 f"[EventSelectionConfig] EXPR: '{variable}' takes {expected} operand(s), got {n}")
1211 if n > 1 and separator != sep:
1212 want = {",": "','", "+": "'+'"}.get(sep, str(sep))
1214 f"[EventSelectionConfig] EXPR: '{variable}' operands must be separated by {want}")
1215 for coll, index in operands:
1216 if coll not in self._EXPR_COLL:
1218 f"[EventSelectionConfig] EXPR: collection '{coll}' is not available. "
1219 "Please request it from the EventSelectionAlgorithms developers.")
1220 coll_spec = self._EXPR_COLL[coll]
1221 isMET = coll_spec.is_met
1222 if isMET:
1223 if not metOk:
1225 f"[EventSelectionConfig] EXPR: 'met' is not valid for '{variable}'")
1226 if index is not None:
1228 "[EventSelectionConfig] EXPR: 'met' cannot be indexed")
1229 if separator == "+":
1231 "[EventSelectionConfig] EXPR: 'met' cannot be combined in a sum")
1232 elif index is None:
1234 f"[EventSelectionConfig] EXPR: '{coll}' must be indexed, e.g. {coll}[0]")
1235
1236 def add_EXPR_selector(self, text, config):
1237 body = text[len("EXPR"):].strip()
1238 if not body:
1239 self.raise_misconfig(text, "EXPR expression")
1240 variable, separator, operands, sign, refValue = self._expr_parse(self._expr_tokenize(body))
1241 self._expr_validate(variable, separator, operands)
1242
1243 thisalg = f'{self.selectionName}_EXPR_{self.step}'
1244 alg = config.createAlgorithm('CP::ObjectKinematicSelectorAlg', thisalg)
1245 alg.variable = variable
1246 alg.sign = sign
1247 alg.refValue = refValue
1248
1249 operandKinds, collections, selections, indices = [], [], [], []
1250 usesMET = False
1251 for coll, index in operands:
1252 coll_spec = self._EXPR_COLL[coll]
1253 opt = coll_spec.option
1254 applyBtag = coll_spec.apply_btag
1255 isMET = coll_spec.is_met
1256 container = getattr(self, opt)
1257 if not container:
1258 self.raise_missinginput(opt)
1259 if isMET:
1260 operandKinds.append("MET")
1261 usesMET = True
1262 continue
1263 operandKinds.append("PARTICLE")
1264 name, selection = config.readNameAndSelection(container)
1265 if applyBtag:
1266 if not self.btagDecoration:
1267 self.raise_missinginput("btagDecoration")
1268 selection = (f'{selection}&&{self.btagDecoration},as_char'
1269 if selection else f'{self.btagDecoration},as_char')
1270 collections.append(name)
1271 selections.append(selection)
1272 indices.append(index)
1273
1274 alg.operandKinds = operandKinds
1275 alg.collections = collections
1276 alg.selections = selections
1277 alg.indices = indices
1278 if usesMET:
1279 if not self.met:
1280 self.raise_missinginput("met")
1281 alg.met = config.readName(self.met)
1282 alg.metTerm = self.metTerm
1283 self._finish_selector(alg, config, thisalg)
1284
1285 # EventInfo scalar (e.g. a DNN/BDT discriminant) -> threshold cut.
1286 # The configurable stored type maps to the matching typed handle on the alg;
1287 # the Python side populates exactly one of them.
1288 _EVENTVAR_TYPES = {"float": "floatVariable", "int": "intVariable", "double": "doubleVariable"}
1289
1290 def add_EVENTVAR_selector(self, text, config):
1291 # EVENTVAR <type> <varname> <sign> <threshold>
1292 args = self.parseArgs("EVENTVAR", text.split())
1293 valueType = self.check_string(args['type'])
1294 if valueType not in self._EVENTVAR_TYPES:
1295 self.raise_misconfig(text, f"value type (one of {sorted(self._EVENTVAR_TYPES)})")
1296 varname = self.check_string(args['name'])
1297 thisalg = f'{self.selectionName}_EVENTVAR_{self.step}'
1298 alg = config.createAlgorithm('CP::EventScalarSelectorAlg', thisalg)
1299 # the variable name is given bare; the systematics suffix is appended here
1300 setattr(alg, self._EVENTVAR_TYPES[valueType], f'{varname}_%SYS%')
1301 alg.sign = self.check_sign(args['sign'])
1302 alg.refValue = self.check_float(args['value'], requirePositive=False) # discriminants may be negative
1303 self._finish_selector(alg, config, thisalg)
1304
1305 def add_SAVE(self, text, config):
1306 # SAVE is deprecated: the event filter is now emitted automatically at
1307 # the end of the block (see makeAlgs). The keyword is accepted only to
1308 # warn existing configs; it performs no operation itself.
1309 self.parseArgs("SAVE", text.split())
1310 warnings.warn(
1311 "[EventSelectionConfig] The 'SAVE' keyword is deprecated: the event "
1312 "filter is now created automatically at the end of each EventSelection "
1313 f"block. Please remove the 'SAVE' line from selection '{self.selectionName}'.",
1314 category=ConfigDeprecationWarning, stacklevel=2)
1315
1316 def _emit_save(self, config):
1317 """Create the SaveFilterAlg that turns the accumulated event selection
1318 into a named, persisted selection (and ntuple branch). Called once per
1319 block, automatically at the end of makeAlgs."""
1320 thisalg = f'{self.selectionName}_SAVE'
1321 alg = config.createAlgorithm('CP::SaveFilterAlg', thisalg)
1322 alg.FilterDescription = f'events passing < {self.selectionName} >'
1323 alg.eventDecisionOutputDecoration = f'ignore_{self.selectionName}_%SYS%'
1324 alg.selection = self.checkDecorationName(self.currentDecoration)
1325 alg.noFilter = True
1326 alg.selectionName = f'pass_{self.selectionName}_%SYS%,as_char' # this one is used as a selection
1327 alg.decorationName = f'ntuplepass_{self.selectionName}_%SYS%' # this one is saved to file
1328 config.addOutputVar('EventInfo', f'ntuplepass_{self.selectionName}_%SYS%', f'pass_{self.selectionName}')
1329
1330
1331@groupBlocks
1333 seq.append(EventSelectionConfig())
1334 seq.append(EventCutFlowBlock())
1335 seq.append(EventSelectionMergerConfig())
std::vector< std::string > tokenize(std::string_view the_str, std::string_view delimiters)
Splits the string into smaller substrings.
setDecorationName(self, algorithm, config, decoration)
extendObjectSelection(self, config, container, oldSelection, newSelection)
_route_lepton(self, alg, config, spec, reco, truth)
_expr_validate(self, variable, separator, operands)
STL class.
std::map< std::string, std::string, std::less<> > parse(const std::string &list)
T * get(TKey *tobj)
get a TObject* from a TKey* (why can't a TObject be a TKey?)
Definition hcg.cxx:132
std::vector< std::string > split(const std::string &s, const std::string &t=":")
Definition hcg.cxx:179
bool match(std::string s1, std::string s2)
match the individual directories of two strings
Definition hcg.cxx:359
void handler(int sig)
signal handler
Definition rmain.cxx:100