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