Copying and Modifying xAOD objects (optional/advanced)

Last update: 16 Aug 2024 [History] [Edit]

In many situations just reading xAOD objects from a file is not good enough, but you may want to modify the objects as well. You could imagine you might want to do some of the following:

  • deep copy: create a new container with the same variables as an existing container, but with only a subset of objects/events passing some selection criteria (example: create a new jet collection that only contains jets with a pT greater than 50 GeV)

  • shallow copy: create a light-weight container that only “updates” some variables from an original container to new values (example: apply some energy correction and override existing reconstructed energy)

  • adding new variables: add variables/attributes to objects (example: adding a variable that identifies the jet as a b-jet)

Each of these are described in more detail below.

Many of the CP tools take advantage of one of these features to either apply corrections to existing containers or copied containers, or to decorate objects with new information.

Deep copy

Deep copying will create new objects/containers that have all the attributes (aka variables) of the original container. This is useful when you are only interested in objects that pass certain criteria.

Let’s create a new jet container of jets passing a pT threshold. First, you need to create the new jet container in your execute() function. For this, remember what you heard about the xAOD EDM in the tutorial. An xAOD container is always described by two objects. An “interface container” that you interact with, and an auxiliary store object that holds all the data. Now that we want to create a new container, you need to create both of these objects.

The naming convention for the auxiliary stores is that if an interface container is called xAOD::BlahContainer, then its auxiliary store type is called xAOD::BlahAuxContainer, where Blah is the object of interest. So for jets will look like:

#include <xAODJet/JetContainer.h>
#include <xAODJet/JetAuxContainer.h>

However instead of using the JetAuxContainer we will use the generic AuxContainerBase this is because if we were using a derivation as input some of the original (so-called “auxiliary”) variables may have been slimmed away (removed to make the container smaller), so if we were to do a deep-copy of the full JetAuxContainer then we would make our container larger than necessary (by creating a bunch of variables that were not even in the original input DxAOD). Instead let’s add these lines instead (to MyxAODAnalysis.cxx):

#include <xAODJet/JetContainer.h>
#include <xAODCore/AuxContainerBase.h>
...
execute() {
   ...
   // Retrieve input jet container
   const xAOD::JetContainer *jets = nullptr;
   ANA_CHECK (evtStore()->retrieve (jets, "AntiKt4EMPFlowJets"));

   // Create the new container and its auxiliary store.
   auto goodJets = std::make_unique<xAOD::JetContainer>();
   auto goodJetsAux = std::make_unique<xAOD::AuxContainerBase>();
   goodJets->setStore (goodJetsAux.get()); //< Connect the two
   ...
   for (auto jet : *jets) {
      if (jet->pt() < 50e03) continue; // skip jets below 50 GeV
      // Copy this jet to the output container:
      xAOD::Jet* goodJet = new xAOD::Jet();
      goodJets->push_back (goodJet); // jet acquires the goodJets auxstore
      *goodJet = *jet; // copies auxdata from one auxstore to the other
   }
   ...
   // Record the objects into the event store
   ANA_CHECK (evtStore()->record (goodJets.release(), "GoodJets"));
   ANA_CHECK (evtStore()->record (goodJetsAux.release(), "GoodJetsAux."));
   ...
}

Take note that you can call the interface container whatever you want (within reason, it should be an alphanumeric name), but the accompanying auxiliary store object must always have the same name, postfixed by Aux.. So, for any Blah interface container you would record a BlahAux. auxiliary container.

Also notice that we create the output objects on the heap. We use std::make_unique for that, which ensures that the object gets deleted once we are done with it. Once we pass the objects over to evtStore()->record() the event store takes ownership, meaning we need to release it.

To make this compile make sure you have updated your package dependencies in MyAnalysis/CMakeLists.txt to include:

LINK_LIBRARIES [...] xAODJet xAODCore

where [...] is any other package dependencies you may already have included.

Typically the object will then either be used by a subsequent algorithm, or by writing them to a file. However for the sake of this tutorial, let’s just read it back in the same algorithm and print out the pT to make sure it got copied:

Shallow copy

Another way of making copies of containers is called the shallow copy. If you want to apply some sort of modification/calibration to all objects in an input container, but you don’t want to perform an object selection at the same time, then the best idea is to make a “shallow copy” of the input container. This is done with the help of xAOD::shallowCopyContainer. Note that you cannot add/remove objects to/from a shallow copy container (but you can decorate it with new variables associated to each object - see decorations in the next section). However it’s absolutely possible to make deep copies of some selected shallow copies later on in your code, and put these deep copies into a container of selected objects.

This creates a copy which only overrides default values with new ones that you set, while keeping all other unaffected quantities unchanged from the original. The shallowCopyContainer will return a pair of xAOD objects (one for the interface and one for the auxiliary store).

Let’s create a shallow copy of the "AntiKt4EMPFlowJets" and shift the pT of all jets up by 5%. In our source code (in MyxAODAnalysis.cxx) lets add:

#include <xAODCore/ShallowCopy.h>
...
execute() {
  ...
  const xAOD::JetContainer* jets = 0;
  ANA_CHECK (evtStore()->retrieve (jets, "AntiKt4EMPFlowJets" ));

  //--------------
  // shallow copy 
  //--------------
  // "jets" jet container already defined above
  auto shallowCopy = xAOD::shallowCopyContainer (*jets);
  std::unique_ptr<xAOD::JetContainer> shallowJets (shallowCopy.first);
  std::unique_ptr<xAOD::ShallowAuxContainer> shallowAux (shallowCopy.second);

  // iterate over the shallow copy
  for (auto jetSC : *shallowJets) {
    // apply a shift in pt, up by 5%
    double newPt =  jetSC->pt() * (1 + 0.05);

    // from: https://gitlab.cern.ch/atlas/athena/tree/21.2/Event/xAOD/xAODJet/xAODJet/versions/Jet_v1.h
    xAOD::JetFourMom_t newp4 (newPt, jetSC->eta(), jetSC->phi(), jetSC->m());
    jetSC->setJetP4 (newp4); // we've overwritten the 4-momentum
  } // end iterator over jet shallow copy
  ...
}
...

By default when you create a shallowCopyContainer you take ownership of the pairs (meaning you need to take care to delete the both). You can give ownership to evtStore(), which will then handle deletion for you.

New variables

You might want to modify an object by adding a new attribute (or variable) to it’s container. There are two ways to do this, depending on the “const” state of the container you want to modify:

  • for const objects (for example adding variables to containers from the input xAOD) you will “decorate” the container using the auxdecor function

  • for nonconst objects (for example possibly like a shallow or deep copied container) you will add auxiliary data to this container using the auxdata function

Now we will show you a simple example of each below.

auxdecor

You can loop over the (const) jet container that you retrieved and decorate the container by adding a variable called mySignal that just checks if the jet pT is greater than 40 GeV. Modify the loop over the jets so it looks like:

  ...
  // loop over the jets in the container
  for (auto jet : *jets) {
    // just to print out something
    ANA_MSG_INFO ("execute(): jet pt = " << (jet->pt() * 0.001) << " GeV");
    if (jet->pt() > 40000) {
      jet->auxdecor<int>("mySignal") = 1; // 1 = yes, it's a signal jet!
    } else {
      jet->auxdecor<int>("mySignal") = 0; // 0 = nope, not a signal jet
    }
  } // end for loop over jets
  ...

auxdata

Here we will add a variable to the (nonconst) jet container we did the shallow copy of above. In the loop over the shallow copied jets, after we have shifted the pT up by 5% you can add the following line:

  ...
  // iterate over the shallow copy
  for (auto jetSC : *shallowJets) {
    // adding a (silly) variable: checking if shallow copied pt is greater
    // than 40 GeV, after 5% shift up (classify as signal or not)
    if (jetSC->pt() > 40000) {
      jetSC->auxdata<int>("mySignal") = 1; // 1 = yes, it's a signal jet!
    } else {
      jetSC->auxdata<int>("mySignal") = 0; // 0 = nope, not a signal jet
    }
  } // end iterator over jet shallow copy
  ...

Here we have added an integer variable called mySignal to the shallow copied jets.

ConstDataVector (advanced/optional)

The final container type available in the ATLAS EDM is ConstDataVector. It is a transient only type (so you can’t write it to an output file) that allows one to create a selected list of objects, and make them appear as a “normal” container of that object type.

For instance to create a list of selected muons from a muon container, you can do the following:

#include <AthContainers/ConstDataVector.h>
...
const xAOD::MuonContainer *allMuons = nullptr;
ANA_CHECK (evtStore()->retrieve (allMuons, "Muons"));
auto selectedMuons = std::make_unique<ConstDataVector<xAOD::MuonContainer>> (SG::VIEW_ELEMENTS);
for (const xAOD::Muon *mu : *allMuons) {
  if (mu->pt() > 30000) {
    selectedMuons->push_back (mu);
  }
}
ANA_CHECK (evtStore()->record( selectedMuons.release(), "SelectedMuons"));

Notice that:

  • You can’t just use xAOD::MuonContainer for this, as that type doesn’t allow constant pointers to be added to it;
  • You need to explicitly use SG::VIEW_ELEMENTS to tell the object that it doesn’t own its elements. For analysis purposes you should only ever need to construct ConstDataVector containers in this mode.
  • Since the container is really just a list of pointers in this setup, it is very efficient for sorting operations for instance. So if you need to sort any container as a function of let’s say pT, you practically should always use a ConstDataVector.

A container of type ConstDataVector can be retrieved as a “normal” container in downstream code. Allowing another class/function to write:

const xAOD::MuonContainer *selectedMuons = nullptr;
ANA_CHECK (evtStore()->retrieve (selectionMuon, "SelectedMuons"));

⭐️ Bonus Exercise

  • Make a new collection of muons with pT between 10 and 20 GeV.
  • Store the absolute value of the muon’s eta using auxdecor.