Calibrators#

The PocketCoffea framework provides a flexible and powerful calibration system to handle object corrections and systematic variations in CMS analyses. The calibration system is designed around the concept of Calibrators - modular components that apply corrections to physics objects (jets, electrons, muons, MET, etc.) and manage their systematic variations.

Overview#

The calibration system consists of three main components:

  1. Calibrator: Abstract base class that defines individual calibration steps

  2. CalibratorsManager: Orchestrates the application of multiple calibrators in sequence

  3. Base Workflow Integration: Automatic handling of systematic variations in the analysis workflow

Key Features#

  • Sequential Processing: Calibrators are applied in a user-defined sequence, allowing for complex interdependencies

  • Automatic Variation Handling: Each calibrator can define its own systematic variations that are automatically propagated through the analysis and made available in the configuration file.

  • Original Collection Preservation: The system maintains references to original collections for calibrators that need uncorrected inputs

  • Flexible Configuration: Calibrators can be configured through parameters and enabled/disabled per data-taking period. Also the systematic variations can be different by period or event type (Sample).

  • Type Safety: Built-in checks ensure calibrators only modify collections they declare to handle

Calibrator Base Class#

All calibrators inherit from the abstract Calibrator class and must implement specific methods:

Class Attributes#

1class YourCalibrator(Calibrator):
2    name: str = "your_calibrator_name"  # Unique identifier
3    has_variations: bool = True  # Whether this calibrator provides variations
4    isMC_only: bool = False  # Whether to run only on MC
5    calibrated_collections: List[str] = [
6        "Collection.field"
7    ]  # Collections this calibrator modifies

Required Methods#

Constructor __init__(self, params, metadata, **kwargs)#

Called to initialize the Calibrator and store necessary metadata for easy later usage.

initialize(events)#

Called once per chunk to prepare calibration data:

1def initialize(self, events):
2    # Prepare calibration factors, load correction files
3    # Set up variations list: self._variations = ["variation1Up", "variation1Down", ...]
4    pass

This method should setup the self._variations variable to define dynamically the list of variations made available for the current chunk of events. Both the Up and Down variations should be defined (meaning that the framework does not assume any variation automatically).

calibrate(events, events_original_collections, variation, already_applied_calibrators)#

Called for each systematic variation to apply corrections:

 1def calibrate(
 2    self,
 3    events,
 4    events_original_collections,
 5    variation,
 6    already_applied_calibrators=None,
 7):
 8    # Apply corrections based on the requested variation
 9    # Return dictionary: {"Collection.field": corrected_values}
10    return {"Jet.pt": corrected_jet_pts}

Built-in Calibrators#

PocketCoffea provides several ready-to-use calibrators:

JetsCalibrator#

  • Name: "jet_calibration"

  • Purpose: Applies Jet Energy Corrections (JEC) and Jet Energy Resolution (JER)

  • Collections: ["Jet", "FatJet"]

  • Variations: JEC and JER uncertainties (e.g., "jet_jecUp", "jet_jerDown")

METCalibrator#

  • Name: "met_rescaling"

  • Purpose: Propagates jet corrections to Missing Energy (MET)

  • Collections: ["MET.pt", "MET.phi"] (configurable)

  • Dependencies: Must run after JetsCalibrator

ElectronsScaleCalibrator#

  • Name: "electron_scale_and_smearing"

  • Purpose: Applies electron energy scale and resolution corrections

  • Collections: ["Electron.pt", "Electron.pt_original"]

  • Variations: "ele_scaleUp/Down", "ele_smearUp/Down" (MC only)

Configuration#

Basic Setup#

In your analysis configuration file:

 1from pocket_coffea.lib.calibrators.common import default_calibrators_sequence
 2
 3cfg = Configurator(
 4    # ... other configuration ...
 5    # Use default calibrator sequence
 6    calibrators=default_calibrators_sequence,
 7    # Configure shape variations
 8    variations={
 9        "shape": {
10            "common": {
11                "inclusive": ["jet_calibration"],  # Run jet variations for all samples
12            },
13            "bysample": {
14                "MC_Sample": {
15                    "inclusive": [
16                        "electron_scale_and_smearing"
17                    ],  # Run electron variations for specific samples
18                }
19            },
20        }
21    },
22)

Custom Calibrator Sequence#

You can define your own calibrator sequence:

1from pocket_coffea.lib.calibrators.common import JetsCalibrator, METCalibrator
2from your_module import CustomCalibrator
3
4custom_sequence = [JetsCalibrator, METCalibrator, CustomCalibrator]
5
6cfg = Configurator(
7    calibrators=custom_sequence,
8    # ... rest of configuration
9)

Parameters Configuration#

Calibrators read their configuration from the parameters system. Example for jet calibration:

# params/jets_calibration.yaml
jets_calibration:
  collection:
    2022:
      AK4PFchs: "Jet"
      AK8PFPuppi: "FatJet"

  jet_types:
    AK4PFchs:
        2016_PreVFP:
        json_path: ${cvmfs:Run2-2016preVFP-UL-NanoAODv9,JME,jet_jerc.json.gz}
        jec_mc: Summer19UL16APV_V7_MC
        jec_data:
          B: Summer19UL16APV_RunBCD_V7_DATA
          C: Summer19UL16APV_RunBCD_V7_DATA
          D: Summer19UL16APV_RunBCD_V7_DATA
          E: Summer19UL16APV_RunEF_V7_DATA
          F: Summer19UL16APV_RunEF_V7_DATA
        jer: Summer20UL16APV_JRV3_MC
        level: L1L2L3Res
    ... 


  apply_jec_MC:
    2022:
      AK4PFchs: true
      AK8PFPuppi: true

  apply_jec_Data:
    2022:
      AK4PFchs: true
      AK8PFPuppi: false

  variations:
    2022:
      AK4PFchs: ["jec", "jer"]
      AK8PFPuppi: ["jec"]

Creating Custom Calibrators#

Simple Example#

Here’s a template for a custom calibrator:

 1from pocket_coffea.lib.calibrators.calibrator import Calibrator
 2import awkward as ak
 3
 4
 5class MyCustomCalibrator(Calibrator):
 6    name = "my_custom_calibrator"
 7    has_variations = True
 8    isMC_only = False
 9    calibrated_collections = ["MyObject.pt", "MyObject.mass"]
10
11    def __init__(self, params, metadata, **kwargs):
12        super().__init__(params, metadata, **kwargs)
13        # Access configuration
14        self.my_config = self.params.my_calibrator_config
15
16    def initialize(self, events):
17        # Prepare correction factors
18        self.scale_factor = self.calculate_scale_factor(events)
19
20        # Define available variations
21        if self.isMC:
22            self._variations = ["myUncertaintyUp", "myUncertaintyDown"]
23        else:
24            self._variations = []
25
26    def calibrate(
27        self, events, orig_colls, variation, already_applied_calibrators=None
28    ):
29        # Get the objects to calibrate
30        objects = events["MyObject"]
31
32        # Apply nominal correction
33        corrected_pt = objects.pt * self.scale_factor
34        corrected_mass = objects.mass * self.scale_factor
35
36        # Apply systematic variations
37        if variation == "myUncertaintyUp":
38            corrected_pt = corrected_pt * 1.02
39        elif variation == "myUncertaintyDown":
40            corrected_pt = corrected_pt * 0.98
41
42        return {"MyObject.pt": corrected_pt, "MyObject.mass": corrected_mass}

Advanced Example with Dependencies#

For calibrators that depend on other calibrators’ output and/or on the original uncalibrated information.

 1class AdvancedCalibrator(Calibrator):
 2    name = "advanced_calibrator"
 3    has_variations = True
 4    isMC_only = True
 5    calibrated_collections = ["DerivedQuantity"]
 6
 7    def calibrate(
 8        self, events, orig_colls, variation, already_applied_calibrators=None
 9    ):
10        # Check dependencies
11        if "jet_calibration" not in already_applied_calibrators:
12            raise ValueError(
13                "This calibrator requires jet_calibration to be applied first"
14            )
15
16        # Use original jets if needed for some calculation
17        if "Jet" in orig_colls:
18            original_jets = orig_colls["Jet"]
19
20        # Use calibrated jets from events
21        # Reading from "events" in practice is taking all the objects calibrated up to this point in the sequence.
22        calibrated_jets = events["Jet"]
23
24        # Compute derived quantity
25        derived = self.compute_derived_quantity(
26            original_jets, calibrated_jets, variation
27        )
28
29        return {"DerivedQuantity": derived}

Systematic Variations#

Variation Naming Convention#

Systematic variations should follow the pattern: "{source}_{direction}" where:

  • source: describes the uncertainty source (e.g., “jec”, “jer”, “ele_scale”)

  • direction: either “Up” or “Down”

Examples:

  • "jet_jecUp", "jet_jecDown"

  • "ele_scaleUp", "ele_scaleDown"

Configuration in Analysis#

Variations are configured in the variations.shape section:

 1variations = {
 2    "shape": {
 3        "common": {
 4            "inclusive": [
 5                "jet_calibration",  # All JEC/JER variations
 6                "electron_scale_and_smearing",  # All electron variations
 7            ],
 8        },
 9        "bysample": {
10            "TTbar": {
11                "inclusive": ["custom_calibrator"],  # Sample-specific variations
12            }
13        },
14    }
15}

Automatic Propagation#

The framework automatically:

  1. Collects all variations from configured calibrators

  2. Loops over each variation during processing

  3. Fills separate histograms for each variation

  4. Resets events to original state between variations

Integration with Workflow#

The calibration system is seamlessly integrated into the base workflow:

Initialization#

1def initialize_calibrators(self):
2    self.calibrators_manager = CalibratorsManager(
3        self.cfg.calibrators,
4        self.events,
5        self.params,
6        self._metadata,
7        jme_factory=self.jmefactory,  # Additional arguments passed to calibrators
8    )

Variation Loop#

1def loop_over_variations(self):
2    for variation, events_calibrated in self.calibrators_manager.calibration_loop(
3        self.events,
4        variations_for_calibrators=self.cfg.available_shape_variations[self._sample],
5    ):
6        self.events = events_calibrated
7        yield variation

Best Practices#

Performance#

  • Heavy computations should be done in initialize() once per chunk

  • Light corrections can be applied dynamically in calibrate()

  • Cache expensive operations when possible

Dependencies#

  • Declare dependencies explicitly by checking already_applied_calibrators

  • Use original collections from orig_colls when needed

  • Order calibrators carefully in your sequence

Error Handling#

  • Validate inputs in both initialize() and calibrate()

  • Check collection existence before accessing

  • Provide meaningful error messages

Testing#

  • Test with both MC and Data if applicable

  • Verify all declared collections are actually modified

  • Check variation names follow conventions

  • Validate with different parameter configurations

Common Issues#

  1. Collection not found: Check calibrated_collections declaration

  2. Variation not applied: Verify variation name and configuration

  3. Performance issues: Move heavy computation to initialize()

  4. Dependency errors: Check calibrator order and requirements