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 dilepton invariant mass',
466 'args': [
467 {'name': 'sign', 'type': 'sign'},
468 {'name': 'refMLL', 'type': 'float'},
469 ],
470 },
471 'MLLWINDOW': {
472 'info': 'Require the dilepton invariant mass inside (or, with `veto`, '
473 'outside) a mass window',
474 'args': [
475 {'name': 'lowMLL', 'type': 'float'},
476 {'name': 'highMLL', 'type': 'float'},
477 {'name': 'veto', 'type': 'flag', 'optional': True},
478 ],
479 },
480 'MLL_OSSF': {
481 'info': 'Require the opposite-sign same-flavour dilepton invariant mass '
482 'inside (or, with `veto`, outside) a mass window',
483 'args': [
484 {'name': 'lowMll', 'type': 'float'},
485 {'name': 'highMll', 'type': 'float'},
486 {'name': 'veto', 'type': 'flag', 'optional': True},
487 ],
488 },
489 'OS': {
490 'info': 'Require an opposite-sign lepton pair; without any flag, all '
491 'available lepton flavours are considered',
492 'args': [
493 {'name': 'el', 'type': 'flag', 'optional': True},
494 {'name': 'mu', 'type': 'flag', 'optional': True},
495 {'name': 'tau', 'type': 'flag', 'optional': True},
496 ],
497 },
498 'SS': {
499 'info': 'Require a same-sign lepton pair; without any flag, all '
500 'available lepton flavours are considered',
501 'args': [
502 {'name': 'el', 'type': 'flag', 'optional': True},
503 {'name': 'mu', 'type': 'flag', 'optional': True},
504 {'name': 'tau', 'type': 'flag', 'optional': True},
505 ],
506 },
507 'IMPORT': {
508 'info': 'Import all the cuts of a previously defined event selection',
509 'args': [
510 {'name': 'region', 'type': 'region'},
511 ],
512 },
513 'EVENTFLAG': {
514 'info': 'Require an existing event-wise decoration to be true',
515 'args': [
516 {'name': 'decoration', 'type': 'str'},
517 ],
518 },
519 'GLOBALTRIGMATCH': {
520 'info': 'Require the global trigger matching decision, optionally for '
521 'a given trigger-configuration postfix',
522 'args': [
523 {'name': 'postfix', 'type': 'str', 'optional': True},
524 ],
525 },
526 'RUN_NUMBER': {
527 'info': 'Cut on the (random) run number',
528 'args': [
529 {'name': 'sign', 'type': 'sign'},
530 {'name': 'runNumber', 'type': 'int'},
531 ],
532 },
533 'EVENTVAR': {
534 'info': 'Cut on an existing EventInfo scalar variable, e.g. a DNN or '
535 'BDT discriminant',
536 'args': [
537 {'name': 'type', 'type': 'str', 'choices': sorted(cls._EVENTVAR_TYPES)},
538 {'name': 'name', 'type': 'str'},
539 {'name': 'sign', 'type': 'sign'},
540 {'name': 'value', 'type': 'float', 'signed': True},
541 ],
542 },
543 'EXPR': {
544 'info': 'Cut on a generic object-kinematic expression, e.g. '
545 '`dR(el[0],jet[0]) > 0.4`',
546 'freeText': True,
547 'grammar': {
548 'collections': sorted(cls._EXPR_COLL),
549 'variables': sorted(cls._EXPR_VARS),
550 },
551 },
552 'SAVE': {
553 'info': 'Deprecated and ignored: the event filter is now emitted '
554 'automatically at the end of every event selection',
555 'deprecated': True,
556 'args': [],
557 },
558 })
559 cls._KEYWORD_SPECS = specs
560 return cls._KEYWORD_SPECS
561
562 @classmethod
563 def keywordSpecs(cls) -> dict:
564 """Return an independent copy of the keyword specification table."""
565 return deepcopy(cls._keywordSpecs())
566
567 @classmethod
568 def _argCounts(cls, spec):
569 """Return the set of valid total token counts (leading keyword included)
570 allowed by a keyword specification, or None when the keyword takes free
571 text and no count check applies."""
572 if spec.get('freeText'):
573 return None
574 forms = spec.get('forms') or [spec.get('args', [])]
575 counts = set()
576 for form in forms:
577 min_args = sum(1 for arg in form if not arg.get('optional'))
578 max_args = len(form)
579 counts.update(n + 1 for n in range(min_args, max_args + 1))
580 return counts
581
582 def makeAlgs(self, config):
583 existing = config.getContainerMeta('EventInfo', 'eventSelectionNames', defaultValue=[])
584 config.setContainerMeta('EventInfo', 'eventSelectionNames',
585 existing + [f'pass_{self.selectionName}_%SYS%'], allowOverwrite=True)
586
587 # need to re-initialize here to deal with multiple passes
588 self.step = 0
589 # initialize the pre-selection
590 self.currentDecoration = self.preselection
591 # re-initialize the cutflow
592 self.cutflow = []
593 # read the selection cuts
594 if self.selectionCuts is None:
595 raise ValueError ("[EventSelectionConfig] You must provide the 'selectionCuts' option to 'EventSelectionConfig': "
596 "a single string where each line represents a different selection cut to apply in order.")
597 for line in self.selectionCuts.split("\n"):
598 self.interpret(line, config)
599 # the event filter is always created automatically at the end of the
600 # block; an explicit SAVE line only triggers a deprecation warning
601 self._emit_save(config)
602 config.addEventCutFlow(self.selectionName, self.getCutflow())
603
604 def interpret(self, text, cfg):
605 text = text.strip()
606 if not text or text.startswith("#"):
607 return
608 self.step += 1
609 keyword = text.split()[0]
610 handler = self._dispatch.get(keyword)
611 if handler is None:
612 raise ValueError (f"[EventSelectionConfig] The following selection cut is not recognised! --> {text}")
613 handler(text, cfg)
614
615 # ------------------------------------------------------------------ #
616 # validation helpers #
617 # ------------------------------------------------------------------ #
618
619 def raise_misconfig(self, text, keyword):
620 raise ValueError (f"[EventSelectionConfig] Misconfiguration! Check {keyword} in: {text}")
621
622 def raise_missinginput(self, collection):
623 raise ValueError (f"[EventSelectionConfig] Misconfiguration! Missing input collection for {collection}")
624
625 def _check_args(self, items, keyword):
626 """Validate the leading keyword and the number of arguments, the latter
627 derived from the keyword specification table."""
628 if items[0] != keyword:
629 self.raise_misconfig(' '.join(items), keyword)
630 validCounts = self._argCounts(self._keywordSpecs()[keyword])
631 if validCounts is not None and len(items) not in validCounts:
632 self.raise_misconfig(' '.join(items), "number of arguments")
633
634 def _extract_flags(self, form, tokens):
635 """Remove flag tokens from `tokens` and return their boolean values."""
636 tokens = list(tokens)
637 values = {}
638 for arg in form:
639 if arg.get('type') != 'flag':
640 continue
641 values[arg['name']] = False
642 for i, token in enumerate(tokens):
643 if token.lower() == arg['name'].lower():
644 del tokens[i]
645 values[arg['name']] = True
646 break
647 return tokens, values
648
649 def _match_form(self, form, tokens):
650 """Match one keyword argument form, returning values or None."""
651 tokens, values = self._extract_flags(form, tokens)
652 positional = [arg for arg in form if arg.get('type') != 'flag']
653 nRequired = sum(1 for arg in positional if not arg.get('optional'))
654 if len(tokens) < nRequired or len(tokens) > len(positional):
655 return None
656
657 budget = len(tokens) - nRequired
658 cursor = 0
659 for arg in positional:
660 if not arg.get('optional'):
661 values[arg['name']] = tokens[cursor]
662 cursor += 1
663 continue
664 pattern = arg.get('pattern')
665 if budget > 0 and (pattern is None or re.fullmatch(pattern, tokens[cursor])):
666 values[arg['name']] = tokens[cursor]
667 cursor += 1
668 budget -= 1
669 else:
670 values[arg['name']] = ''
671 return values if cursor == len(tokens) else None
672
673 def parseArgs(self, keyword, items):
674 """Turn the already-split token list `items` (leading keyword included)
675 into a dict of argument name -> value, driven by `keywordSpecs`, which
676 is authoritative for the argument grammar."""
677 self._check_args(items, keyword)
678 spec = self._keywordSpecs()[keyword]
679 if spec.get('freeText'):
680 return {'text': ' '.join(items[1:])}
681 for form in (spec.get('forms') or [spec.get('args', [])]):
682 values = self._match_form(form, items[1:])
683 if values is not None:
684 return values
685 self.raise_misconfig(' '.join(items), "number of arguments")
686
687 def check_float(self, test, requirePositive=True):
688 try:
689 value = float(test)
690 if not requirePositive or value >= 0:
691 return value
692 else:
693 raise ValueError (f"[EventSelectionConfig] Misconfiguration! Float {test} is not positive!")
694 except ValueError:
695 raise ValueError (f"[EventSelectionConfig] Misconfiguration! {test} should be a float, not {type(test)}!")
696
697 def check_int(self, test, requirePositive=True):
698 try:
699 numeric = float(test)
700 value = int(numeric)
701 except (TypeError, ValueError):
702 raise ValueError (f"[EventSelectionConfig] Misconfiguration! {test} should be an int, not {type(test)}")
703 if value != numeric:
704 raise ValueError (f"[EventSelectionConfig] Misconfiguration! {test} should be an int, not a float!")
705 if requirePositive and value < 0:
706 raise ValueError (f"[EventSelectionConfig] Misconfiguration! Int {test} is not positive!")
707 return value
708
709 def check_string(self, test):
710 if not isinstance(test, str):
711 raise ValueError (f"[EventSelectionConfig] Misconfiguration! {test} should be a string, not a number!")
712 else:
713 return test
714
715 def check_sign(self, test):
716 mapping = {
717 "<" : "LT",
718 ">" : "GT",
719 "==": "EQ",
720 ">=": "GE",
721 "<=": "LE"
722 }
723 try:
724 return mapping[test]
725 except KeyError:
726 raise KeyError (f"[EventSelectionConfig] Misconfiguration! {test} should be one of {list(mapping.keys())}")
727
728 def check_btagging(self, test):
729 test = test.split(":")
730 if len(test) != 2:
731 raise ValueError (f"[EventSelectionConfig] Misconfiguration! {test} should be provided as 'btagger:btagWP'")
732 else:
733 return test
734
735 def check_ghosts(self, test):
736 test = self.check_string(test)
737 values = test.split("!")
738 ghost_map = {
739 "B": "GhostBHadronsFinalCount",
740 "C": "GhostCHadronsFinalCount",
741 "T": "GhostTQuarksFinalCount",
742 "W": "GhostWBosonsCount",
743 "Z": "GhostZBosonsCount",
744 "H": "GhostHBosonsCount",
745 "TAU": "GhostTausFinalCount"
746 }
747 return [ghost_map.get(value.upper(), value) for value in values]
748
749 # ------------------------------------------------------------------ #
750 # decoration / selection bookkeeping #
751 # ------------------------------------------------------------------ #
752
753 def getCutflow(self):
754 return self.cutflow
755
756 def setDecorationName(self, algorithm, config, decoration):
757 self.cutflow.append( decoration )
758 if algorithm is not None:
759 algorithm.decorationName = f'{decoration},as_char'
760 self.currentDecoration = decoration
761 if self.debugMode:
762 config.addOutputVar('EventInfo', decoration, decoration.split("_%SYS%")[0])
763 else:
764 if self.currentDecoration:
765 self.currentDecoration += '&&' + decoration
766 else:
767 self.currentDecoration = decoration
768 config.addSelection('EventInfo', '', decoration)
769
770 def checkDecorationName(self, decoration):
771 if decoration == '':
772 return decoration
773 decoration = decoration.split("&&")
774 decoration = [sub + ',as_char' if ',as_char' not in sub else sub for sub in decoration]
775 return '&&'.join(decoration)
776
777 def extendObjectSelection(self, config, container, oldSelection, newSelection):
778 if oldSelection:
779 return oldSelection + "&&" + config.getFullSelection(container, newSelection)
780 else:
781 return config.getFullSelection(container, newSelection)
782
783 # ------------------------------------------------------------------ #
784 # shared selector helpers #
785 # ------------------------------------------------------------------ #
786
787 def _require_inputs(self, *names, message=None):
788 """Require at least one of the named input options to be configured."""
789 if not any(getattr(self, name) for name in names):
790 self.raise_missinginput(message or " or ".join(names))
791
792 def _finish_selector(self, alg, config, name):
793 """Apply the common event-preselection and output decoration."""
794 alg.eventPreselection = self.checkDecorationName(self.currentDecoration)
795 self.setDecorationName(alg, config, f'{name}_%SYS%')
796
797 def _configure_leptons(self, alg, config, leptons):
798 """Configure reco/truth lepton handles from explicit metadata."""
799 for spec, reco, truth in leptons:
800 if spec:
801 self._route_lepton(alg, config, spec, reco, truth)
802 self._maybe_dressed(alg, *(spec for spec, _reco, _truth in leptons))
803
804 def _maybe_dressed(self, alg, *specs):
805 """Enable dressed kinematics when any of the given electron/muon
806 containers is a truth container. Dressed kinematics only exist for
807 truth electrons and muons, so only those specs should be passed here."""
808 if any(spec and ("Particle" in spec or "Truth" in spec) for spec in specs):
809 alg.useDressedProperties = self.useDressedProperties
810
811 def _route_lepton(self, alg, config, spec, reco, truth):
812 """Assign (name, selection) to the reco or truth handles of `alg`
813 depending on whether `spec` points to a truth container.
814 `reco`/`truth` are (nameAttr, selectionAttr) pairs."""
815 name, sel = config.readNameAndSelection(spec)
816 nameAttr, selAttr = truth if ("Particle" in spec or "Truth" in spec) else reco
817 setattr(alg, nameAttr, name)
818 setattr(alg, selAttr, sel)
819
820 # ------------------------------------------------------------------ #
821 # selector builders #
822 # ------------------------------------------------------------------ #
823
824 def _add_nobject(self, text, config, *, attr, tag):
825 """Generic builder for the N-object pT selectors (EL_N, MU_N, JET_N,
826 PH_N, TAU_N, LJET_N): identical except for the source container, which
827 is always required since the cut acts on it."""
828 items = text.split()
829 spec = getattr(self, attr)
830 self._require_inputs(attr)
831 # the keyword is not known to this generic builder: read it back
832 args = self.parseArgs(items[0], items)
833 thisalg = f'{self.selectionName}_{tag}_{self.step}'
834 alg = config.createAlgorithm('CP::NObjectPtSelectorAlg', thisalg)
835 alg.particles, alg.objectSelection = config.readNameAndSelection(spec)
836 if attr in ("electrons", "muons"):
837 self._maybe_dressed(alg, spec)
838 if args['sel']:
839 alg.objectSelection = self.extendObjectSelection(
840 config, spec.split(".")[0], alg.objectSelection,
841 self.check_string(args['sel']))
842 alg.minPt = self.check_float(args['ptmin'])
843 alg.sign = self.check_sign(args['sign'])
844 alg.count = self.check_int(args['count'])
845 self._finish_selector(alg, config, thisalg)
846
847 def add_IMPORT(self, text, config):
848 # this is used to import a previous selection
849 args = self.parseArgs("IMPORT", text.split())
850 region = self.check_string(args['region'])
851 if not self.currentDecoration:
852 self.currentDecoration = f'pass_{region}_%SYS%,as_char'
853 else:
854 self.currentDecoration = f'{self.currentDecoration},as_char&&pass_{region}_%SYS%'
855 # for the cutflow, we need to retrieve all the cuts corresponding to this IMPORT
856 imported_cuts = [cut for cut in config.getSelectionCutFlow('EventInfo', '') if cut.startswith(region)]
857 self.cutflow += imported_cuts
858
859 def add_NBJET_selector(self, text, config):
860 args = self.parseArgs("JET_N_BTAG", text.split())
861 self._require_inputs("jets")
862 thisalg = f'{self.selectionName}_NBJET_{self.step}'
863 alg = config.createAlgorithm('CP::NObjectPtSelectorAlg', thisalg)
864 particles, selection = config.readNameAndSelection(self.jets)
865 alg.particles = particles
866 alg.objectSelection = f'{selection}&&{self.btagDecoration},as_char' if selection else f'{self.btagDecoration},as_char'
867 if args['btag']:
868 btagger, btagWP = self.check_btagging(args['btag'])
869 customBtag = f'ftag_select_{btagger}_{btagWP}'
870 alg.objectSelection = f'{selection}&&{customBtag},as_char' if selection else f'{customBtag},as_char'
871 if args['sel']:
872 extraSel = self.check_string(args['sel'])
873 alg.objectSelection = self.extendObjectSelection(config, self.jets.split(".")[0], alg.objectSelection, extraSel)
874 alg.sign = self.check_sign(args['sign'])
875 alg.count = self.check_int(args['count'])
876 self._finish_selector(alg, config, thisalg)
877
878 def add_SUMNELNMU_selector(self, text, config):
879 args = self.parseArgs("SUM_EL_N_MU_N", text.split())
880 self._require_inputs("electrons", "muons")
881 thisalg = f'{self.selectionName}_SUMNELNMU_{self.step}'
882 alg = config.createAlgorithm('CP::SumNLeptonPtSelectorAlg', thisalg)
883 alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
884 alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
885 self._maybe_dressed(alg, self.electrons, self.muons)
886 if args.get('ptmin') is not None:
887 # a single threshold, applied to every flavour
888 alg.minPtEl = self.check_float(args['ptmin'])
889 alg.minPtMu = self.check_float(args['ptmin'])
890 else:
891 if args.get('selEl'):
892 extraSelEl = self.check_string(args['selEl'])
893 alg.electronSelection = self.extendObjectSelection(config, self.electrons.split(".")[0], alg.electronSelection, extraSelEl)
894 if args.get('selMu'):
895 extraSelMu = self.check_string(args['selMu'])
896 alg.muonSelection = self.extendObjectSelection(config, self.muons.split(".")[0], alg.muonSelection, extraSelMu)
897 alg.minPtEl = self.check_float(args['ptEl'])
898 alg.minPtMu = self.check_float(args['ptMu'])
899 alg.sign = self.check_sign(args['sign'])
900 alg.count = self.check_int(args['count'])
901 self._finish_selector(alg, config, thisalg)
902
903 def add_SUMNLEPTONS_selector(self, text, config):
904 args = self.parseArgs("SUM_EL_N_MU_N_TAU_N", text.split())
905 self._require_inputs("electrons", "muons", "taus", message="electrons, muons or taus")
906 thisalg = f'{self.selectionName}_SUMNLEPTONS_{self.step}'
907 alg = config.createAlgorithm('CP::SumNLeptonPtSelectorAlg', thisalg)
908 alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
909 alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
910 alg.taus, alg.tauSelection = config.readNameAndSelection(self.taus)
911 self._maybe_dressed(alg, self.electrons, self.muons)
912 if args.get('ptmin') is not None:
913 # a single threshold, applied to every flavour
914 alg.minPtEl = self.check_float(args['ptmin'])
915 alg.minPtMu = self.check_float(args['ptmin'])
916 alg.minPtTau = self.check_float(args['ptmin'])
917 else:
918 if args.get('selEl'):
919 extraSelEl = self.check_string(args['selEl'])
920 alg.electronSelection = self.extendObjectSelection(config, self.electrons.split(".")[0], alg.electronSelection, extraSelEl)
921 if args.get('selMu'):
922 extraSelMu = self.check_string(args['selMu'])
923 alg.muonSelection = self.extendObjectSelection(config, self.muons.split(".")[0], alg.muonSelection, extraSelMu)
924 if args.get('selTau'):
925 extraSelTau = self.check_string(args['selTau'])
926 alg.tauSelection = self.extendObjectSelection(config, self.taus.split(".")[0], alg.tauSelection, extraSelTau)
927 alg.minPtEl = self.check_float(args['ptEl'])
928 alg.minPtMu = self.check_float(args['ptMu'])
929 alg.minPtTau = self.check_float(args['ptTau'])
930 alg.sign = self.check_sign(args['sign'])
931 alg.count = self.check_int(args['count'])
932 self._finish_selector(alg, config, thisalg)
933
934 def add_NLJETMASS_selector(self, text, config):
935 args = self.parseArgs("LJETMASS_N", text.split())
936 thisalg = f'{self.selectionName}_NLJETMASS_{self.step}'
937 alg = config.createAlgorithm('CP::NObjectMassSelectorAlg', thisalg)
938 alg.particles, alg.objectSelection = config.readNameAndSelection(self.largeRjets)
939 if args['sel']:
940 alg.objectSelection = self.extendObjectSelection(
941 config, self.largeRjets.split(".")[0], alg.objectSelection,
942 self.check_string(args['sel']))
943 alg.minMass = self.check_float(args['minMass'])
944 alg.sign = self.check_sign(args['sign'])
945 alg.count = self.check_int(args['count'])
946 self._finish_selector(alg, config, thisalg)
947
948 def add_NLJETMASSWINDOW_selector(self, text, config):
949 args = self.parseArgs("LJETMASSWINDOW_N", text.split())
950 thisalg = f'{self.selectionName}_NLJETMASSWINDOW_{self.step}'
951 alg = config.createAlgorithm('CP::NLargeRJetMassWindowSelectorAlg', thisalg)
952 alg.ljets, alg.ljetSelection = config.readNameAndSelection(self.largeRjets)
953 if args['sel']:
954 extraSel = self.check_string(args['sel'])
955 alg.ljetSelection = self.extendObjectSelection(config, self.largeRjets.split(".")[0], alg.ljetSelection, extraSel)
956 alg.lowMass = self.check_float(args['lowMass'])
957 alg.highMass = self.check_float(args['highMass'])
958 alg.sign = self.check_sign(args['sign'])
959 alg.count = self.check_int(args['count'])
960 alg.vetoMode = args['veto']
961 self._finish_selector(alg, config, thisalg)
962
963 def add_NJETGHOST_selector(self, text, config):
964 args = self.parseArgs("JET_N_GHOST", text.split())
965 thisalg = f'{self.selectionName}_NJETGHOST_{self.step}'
966 alg = config.createAlgorithm('CP::JetNGhostSelectorAlg', thisalg)
967 alg.jets, alg.jetSelection = config.readNameAndSelection(self.jets)
968 ghosts = self.check_ghosts(args['ghost'])
969 alg.ghost = ghosts[0]
970 if len(ghosts) > 1 :
971 alg.veto = ghosts[1]
972 if args['ptmin']:
973 alg.minPt = self.check_float(args['ptmin'])
974 alg.sign = self.check_sign(args['sign'])
975 alg.count = self.check_int(args['count'])
976 self._finish_selector(alg, config, thisalg)
977
978 def add_NLJETGHOST_selector(self, text, config):
979 args = self.parseArgs("LJET_N_GHOST", text.split())
980 thisalg = f'{self.selectionName}_NLJETGHOST_{self.step}'
981 alg = config.createAlgorithm('CP::JetNGhostSelectorAlg', thisalg)
982 alg.jets, alg.jetSelection = config.readNameAndSelection(self.largeRjets)
983 ghosts = self.check_ghosts(args['ghost'])
984 alg.ghost = ghosts[0]
985 if len(ghosts) > 1 :
986 alg.veto = ghosts[1]
987 if args['ptmin']:
988 alg.minPt = self.check_float(args['ptmin'])
989 alg.sign = self.check_sign(args['sign'])
990 alg.count = self.check_int(args['count'])
991 self._finish_selector(alg, config, thisalg)
992
993 def add_NOBJ_selector(self, text, config):
994 args = self.parseArgs("OBJ_N", text.split())
995 thisalg = f'{self.selectionName}_NOBJ_{self.step}'
996 alg = config.createAlgorithm('CP::NObjectPtSelectorAlg', thisalg)
997 alg.particles, alg.objectSelection = config.readNameAndSelection(self.check_string(args['container']))
998 alg.minPt = self.check_float(args['ptmin'])
999 alg.sign = self.check_sign(args['sign'])
1000 alg.count = self.check_int(args['count'])
1001 self._finish_selector(alg, config, thisalg)
1002
1003 def add_MET_selector(self, text, config):
1004 args = self.parseArgs("MET", text.split())
1005 self._require_inputs("met", message="MET")
1006 thisalg = f'{self.selectionName}_MET_{self.step}'
1007 alg = config.createAlgorithm('CP::MissingETSelectorAlg', thisalg)
1008 alg.met = config.readName(self.met)
1009 alg.metTerm = self.metTerm
1010 alg.sign = self.check_sign(args['sign'])
1011 alg.refMET = self.check_float(args['refMET'])
1012 self._finish_selector(alg, config, thisalg)
1013
1014 def add_MWT_selector(self, text, config):
1015 args = self.parseArgs("MWT", text.split())
1016 self._require_inputs("electrons", "muons")
1017 thisalg = f'{self.selectionName}_MWT_{self.step}'
1018 alg = config.createAlgorithm('CP::TransverseMassSelectorAlg', thisalg)
1019 alg.met = config.readName(self.met)
1020 alg.metTerm = self.metTerm
1021 alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
1022 alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
1023 self._maybe_dressed(alg, self.electrons, self.muons)
1024 alg.sign = self.check_sign(args['sign'])
1025 alg.refMWT = self.check_float(args['refMWT'])
1026 self._finish_selector(alg, config, thisalg)
1027
1028 def add_METMWT_selector(self, text, config):
1029 args = self.parseArgs("MET+MWT", text.split())
1030 self._require_inputs("met", message="MET")
1031 self._require_inputs("electrons", "muons")
1032 thisalg = f'{self.selectionName}_METMWT_{self.step}'
1033 alg = config.createAlgorithm('CP::MissingETPlusTransverseMassSelectorAlg', thisalg)
1034 alg.met = config.readName(self.met)
1035 alg.metTerm = self.metTerm
1036 alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
1037 alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
1038 self._maybe_dressed(alg, self.electrons, self.muons)
1039 alg.sign = self.check_sign(args['sign'])
1040 alg.refMETMWT = self.check_float(args['refMETMWT'])
1041 self._finish_selector(alg, config, thisalg)
1042
1043 def add_MLL_selector(self, text, config):
1044 args = self.parseArgs("MLL", text.split())
1045 self._require_inputs("electrons", "muons")
1046 thisalg = f'{self.selectionName}_MLL_{self.step}'
1047 alg = config.createAlgorithm('CP::DileptonInvariantMassSelectorAlg', thisalg)
1048 self._configure_leptons(alg, config, (
1049 (self.electrons, ('electrons', 'electronSelection'),
1050 ('truthElectrons', 'truthElectronSelection')),
1051 (self.muons, ('muons', 'muonSelection'),
1052 ('truthMuons', 'truthMuonSelection')),
1053 ))
1054 alg.sign = self.check_sign(args['sign'])
1055 alg.refMLL = self.check_float(args['refMLL'])
1056 self._finish_selector(alg, config, thisalg)
1057
1058 def add_MLLWINDOW_selector(self, text, config):
1059 args = self.parseArgs("MLLWINDOW", text.split())
1060 self._require_inputs("electrons", "muons")
1061 thisalg = f'{self.selectionName}_MLLWINDOW_{self.step}'
1062 alg = config.createAlgorithm('CP::DileptonInvariantMassWindowSelectorAlg', thisalg)
1063 self._configure_leptons(alg, config, (
1064 (self.electrons, ('electrons', 'electronSelection'),
1065 ('truthElectrons', 'truthElectronSelection')),
1066 (self.muons, ('muons', 'muonSelection'),
1067 ('truthMuons', 'truthMuonSelection')),
1068 ))
1069 alg.lowMLL = self.check_float(args['lowMLL'])
1070 alg.highMLL = self.check_float(args['highMLL'])
1071 alg.vetoMode = args['veto']
1072 self._finish_selector(alg, config, thisalg)
1073
1074 def _add_charge(self, text, config, *, osMode, tag):
1075 """Builder shared by OS and SS: same algorithm, opposite charge mode."""
1076 args = self.parseArgs(tag, text.split())
1077 self._require_inputs("electrons", "muons", "taus")
1078 thisalg = f'{self.selectionName}_{tag}_{self.step}'
1079 alg = config.createAlgorithm('CP::ChargeSelectorAlg', thisalg)
1080 allLeptons = not (args['el'] or args['mu'] or args['tau'])
1081 if self.electrons and (allLeptons or args['el']):
1082 self._route_lepton(alg, config, self.electrons,
1083 ('electrons', 'electronSelection'),
1084 ('truthElectrons', 'truthElectronSelection'))
1085 if self.muons and (allLeptons or args['mu']):
1086 self._route_lepton(alg, config, self.muons,
1087 ('muons', 'muonSelection'),
1088 ('truthMuons', 'truthMuonSelection'))
1089 if self.taus and (allLeptons or args['tau']):
1090 self._route_lepton(alg, config, self.taus,
1091 ('taus', 'tauSelection'),
1092 ('truthTaus', 'truthTauSelection'))
1093 alg.OS = osMode
1094 self._finish_selector(alg, config, thisalg)
1095
1096 def add_MLL_OSSF_selector(self, text, config):
1097 args = self.parseArgs("MLL_OSSF", text.split())
1098 self._require_inputs("electrons", "muons")
1099 thisalg = f'{self.selectionName}_MLL_OSSF_{self.step}'
1100 alg = config.createAlgorithm('CP::DileptonOSSFInvariantMassWindowSelectorAlg', thisalg)
1101 self._configure_leptons(alg, config, (
1102 (self.electrons, ('electrons', 'electronSelection'),
1103 ('truthElectrons', 'truthElectronSelection')),
1104 (self.muons, ('muons', 'muonSelection'),
1105 ('truthMuons', 'truthMuonSelection')),
1106 ))
1107 alg.lowMll = self.check_float(args['lowMll'])
1108 alg.highMll = self.check_float(args['highMll'])
1109 alg.vetoMode = args['veto']
1110 self._finish_selector(alg, config, thisalg)
1111
1112 def add_EVENTFLAG(self, text, config):
1113 args = self.parseArgs("EVENTFLAG", text.split())
1114 existingDecoration = self.check_string(args['decoration'])
1115 self.setDecorationName(None, config, existingDecoration)
1116
1117 def add_GLOBALTRIGMATCH(self, text, config):
1118 args = self.parseArgs("GLOBALTRIGMATCH", text.split())
1119 postfix = self.check_string(args['postfix'])
1120 self.setDecorationName(None, config, f"globalTriggerMatch{postfix}_%SYS%,as_char")
1121
1122 def add_RUNNUMBER(self, text, config):
1123 args = self.parseArgs("RUN_NUMBER", text.split())
1124 thisalg = f'{self.selectionName}_RUN_NUMBER_{self.step}'
1125 alg = config.createAlgorithm('CP::RunNumberSelectorAlg', thisalg)
1126 alg.sign = self.check_sign(args['sign'])
1127 alg.runNumber = self.check_int(args['runNumber'])
1128 alg.useRandomRunNumber = config.dataType() is not DataType.Data
1129 self._finish_selector(alg, config, thisalg)
1130
1131 # ------------------------------------------------------------------ #
1132 # EXPR: generic object-kinematic expression cuts #
1133 # ------------------------------------------------------------------ #
1134
1135 # collection token -> metadata
1136 _EXPR_COLL = {
1137 "jet": _ExprCollection("jets"),
1138 "bjet": _ExprCollection("jets", apply_btag=True),
1139 "el": _ExprCollection("electrons"),
1140 "mu": _ExprCollection("muons"),
1141 "tau": _ExprCollection("taus"),
1142 "ph": _ExprCollection("photons"),
1143 "ljet": _ExprCollection("largeRjets"),
1144 "met": _ExprCollection("met", is_met=True),
1145 }
1146
1147 # variable -> metadata
1148 _EXPR_VARS = {
1149 "dR": _ExprVariable(2, 2, ",", True, False),
1150 "dEta": _ExprVariable(2, 2, ",", True, False),
1151 "dPhi": _ExprVariable(2, 2, ",", False, True),
1152 "m": _ExprVariable(1, None, "+", False, False),
1153 "e": _ExprVariable(1, None, "+", False, False),
1154 "pt": _ExprVariable(1, None, "+", False, True),
1155 "eta": _ExprVariable(1, 1, None, True, False),
1156 "phi": _ExprVariable(1, 1, None, False, True),
1157 }
1158
1159 _EXPR_TOKEN_RE = re.compile(r"""
1160 (?P<NUM>\d+\.\d+(?:[eE][+-]?\d+)?|\d+[eE][+-]?\d+|\d+)
1161 | (?P<GE>>=) | (?P<LE><=) | (?P<EQ>==) | (?P<LT><) | (?P<GT>>)
1162 | (?P<ID>[A-Za-z_][A-Za-z0-9_]*)
1163 | (?P<LP>\‍() | (?P<RP>\‍)) | (?P<LB>\‍[) | (?P<RB>\‍])
1164 | (?P<COMMA>,) | (?P<PLUS>\+) | (?P<MINUS>-)
1165 | (?P<WS>\s+)
1166 """, re.VERBOSE)
1167
1168
1169 def _expr_tokenize(self, text):
1170 return _ExprParser(self._EXPR_TOKEN_RE).tokenize(text)
1171
1172 def _expr_parse(self, tokens):
1173 return _ExprParser(self._EXPR_TOKEN_RE).parse(tokens)
1174
1175 def _expr_validate(self, variable, separator, operands):
1176 if variable not in self._EXPR_VARS:
1178 f"[EventSelectionConfig] EXPR: variable '{variable}' is not available. "
1179 "Please request it from the EventSelectionAlgorithms developers.")
1180 var_spec = self._EXPR_VARS[variable]
1181 minN = var_spec.min_operands
1182 maxN = var_spec.max_operands
1183 sep = var_spec.separator
1184 metOk = var_spec.met_ok
1185 n = len(operands)
1186 if n < minN or (maxN is not None and n > maxN):
1187 expected = f"{minN}" if maxN == minN else (f"{minN}+" if maxN is None else f"{minN}-{maxN}")
1189 f"[EventSelectionConfig] EXPR: '{variable}' takes {expected} operand(s), got {n}")
1190 if n > 1 and separator != sep:
1191 want = {",": "','", "+": "'+'"}.get(sep, str(sep))
1193 f"[EventSelectionConfig] EXPR: '{variable}' operands must be separated by {want}")
1194 for coll, index in operands:
1195 if coll not in self._EXPR_COLL:
1197 f"[EventSelectionConfig] EXPR: collection '{coll}' is not available. "
1198 "Please request it from the EventSelectionAlgorithms developers.")
1199 coll_spec = self._EXPR_COLL[coll]
1200 isMET = coll_spec.is_met
1201 if isMET:
1202 if not metOk:
1204 f"[EventSelectionConfig] EXPR: 'met' is not valid for '{variable}'")
1205 if index is not None:
1207 "[EventSelectionConfig] EXPR: 'met' cannot be indexed")
1208 if separator == "+":
1210 "[EventSelectionConfig] EXPR: 'met' cannot be combined in a sum")
1211 elif index is None:
1213 f"[EventSelectionConfig] EXPR: '{coll}' must be indexed, e.g. {coll}[0]")
1214
1215 def add_EXPR_selector(self, text, config):
1216 body = text[len("EXPR"):].strip()
1217 if not body:
1218 self.raise_misconfig(text, "EXPR expression")
1219 variable, separator, operands, sign, refValue = self._expr_parse(self._expr_tokenize(body))
1220 self._expr_validate(variable, separator, operands)
1221
1222 thisalg = f'{self.selectionName}_EXPR_{self.step}'
1223 alg = config.createAlgorithm('CP::ObjectKinematicSelectorAlg', thisalg)
1224 alg.variable = variable
1225 alg.sign = sign
1226 alg.refValue = refValue
1227
1228 operandKinds, collections, selections, indices = [], [], [], []
1229 usesMET = False
1230 for coll, index in operands:
1231 coll_spec = self._EXPR_COLL[coll]
1232 opt = coll_spec.option
1233 applyBtag = coll_spec.apply_btag
1234 isMET = coll_spec.is_met
1235 container = getattr(self, opt)
1236 if not container:
1237 self.raise_missinginput(opt)
1238 if isMET:
1239 operandKinds.append("MET")
1240 usesMET = True
1241 continue
1242 operandKinds.append("PARTICLE")
1243 name, selection = config.readNameAndSelection(container)
1244 if applyBtag:
1245 if not self.btagDecoration:
1246 self.raise_missinginput("btagDecoration")
1247 selection = (f'{selection}&&{self.btagDecoration},as_char'
1248 if selection else f'{self.btagDecoration},as_char')
1249 collections.append(name)
1250 selections.append(selection)
1251 indices.append(index)
1252
1253 alg.operandKinds = operandKinds
1254 alg.collections = collections
1255 alg.selections = selections
1256 alg.indices = indices
1257 if usesMET:
1258 if not self.met:
1259 self.raise_missinginput("met")
1260 alg.met = config.readName(self.met)
1261 alg.metTerm = self.metTerm
1262 self._finish_selector(alg, config, thisalg)
1263
1264 # EventInfo scalar (e.g. a DNN/BDT discriminant) -> threshold cut.
1265 # The configurable stored type maps to the matching typed handle on the alg;
1266 # the Python side populates exactly one of them.
1267 _EVENTVAR_TYPES = {"float": "floatVariable", "int": "intVariable", "double": "doubleVariable"}
1268
1269 def add_EVENTVAR_selector(self, text, config):
1270 # EVENTVAR <type> <varname> <sign> <threshold>
1271 args = self.parseArgs("EVENTVAR", text.split())
1272 valueType = self.check_string(args['type'])
1273 if valueType not in self._EVENTVAR_TYPES:
1274 self.raise_misconfig(text, f"value type (one of {sorted(self._EVENTVAR_TYPES)})")
1275 varname = self.check_string(args['name'])
1276 thisalg = f'{self.selectionName}_EVENTVAR_{self.step}'
1277 alg = config.createAlgorithm('CP::EventScalarSelectorAlg', thisalg)
1278 # the variable name is given bare; the systematics suffix is appended here
1279 setattr(alg, self._EVENTVAR_TYPES[valueType], f'{varname}_%SYS%')
1280 alg.sign = self.check_sign(args['sign'])
1281 alg.refValue = self.check_float(args['value'], requirePositive=False) # discriminants may be negative
1282 self._finish_selector(alg, config, thisalg)
1283
1284 def add_SAVE(self, text, config):
1285 # SAVE is deprecated: the event filter is now emitted automatically at
1286 # the end of the block (see makeAlgs). The keyword is accepted only to
1287 # warn existing configs; it performs no operation itself.
1288 self.parseArgs("SAVE", text.split())
1289 warnings.warn(
1290 "[EventSelectionConfig] The 'SAVE' keyword is deprecated: the event "
1291 "filter is now created automatically at the end of each EventSelection "
1292 f"block. Please remove the 'SAVE' line from selection '{self.selectionName}'.",
1293 category=ConfigDeprecationWarning, stacklevel=2)
1294
1295 def _emit_save(self, config):
1296 """Create the SaveFilterAlg that turns the accumulated event selection
1297 into a named, persisted selection (and ntuple branch). Called once per
1298 block, automatically at the end of makeAlgs."""
1299 thisalg = f'{self.selectionName}_SAVE'
1300 alg = config.createAlgorithm('CP::SaveFilterAlg', thisalg)
1301 alg.FilterDescription = f'events passing < {self.selectionName} >'
1302 alg.eventDecisionOutputDecoration = f'ignore_{self.selectionName}_%SYS%'
1303 alg.selection = self.checkDecorationName(self.currentDecoration)
1304 alg.noFilter = True
1305 alg.selectionName = f'pass_{self.selectionName}_%SYS%,as_char' # this one is used as a selection
1306 alg.decorationName = f'ntuplepass_{self.selectionName}_%SYS%' # this one is saved to file
1307 config.addOutputVar('EventInfo', f'ntuplepass_{self.selectionName}_%SYS%', f'pass_{self.selectionName}')
1308
1309
1310@groupBlocks
1312 seq.append(EventSelectionConfig())
1313 seq.append(EventCutFlowBlock())
1314 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:99