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