Hypothesis Algorithms

Last update: 28 Apr 2020 [History] [Edit]

Overview

A large change in the software, as compared to Runs 1 and 2, is how trigger hypothesis algorithms are constructed. Greater flexibility is possible, at the expense of increased complexity.

The task of performing the selection is split now into two kinds of components: the hypothesis algorithm (HypoAlg), and a set of private tools called hypothesis tools (HypoTools).

The following uses typedef and helper functions from the TrigCompositeUtils namespace. The namespace is omitted below for readability.

The HypoTool

Every chain in the menu implements its selection using a series of HypoTools. There is no specific framework interface for HypoTools. HypoTools are all named identically to the chain whose selection they apply, and the configuration will typically contain multiple HypoTool instances per chain, implementing the chain’s selection at different steps. Each HypoTool instance is owned by a HypoAlg in each step (e.g. a fast calorimetry step, fast-tracking step, precision calorimetry step and precision tracking step). For example, the chain HLT_e20_loose_L1EM15 would use multiple HypoTools (one per step) having the same tool instance name (HLT_e20_loose_L1EM15). Because each HypoTool is a private tool, its Athena name consists of the owning HypoAlg instance name and the tool instance name e.g. L2CaloHypoAlg.HLT_e20_loose_L1EM15, L2ElectronHypo.HLT_e20_loose_L1EM15, etc.

The HypoTool’s name gets converted into a HLT::Identifier that should be an attribute of every HypoTool class:

HLT::Identifier m_decisionId;

It is then set in the constructor initializer list:

m_decisionId( HLT::Identifier::fromToolName( name ) )

The part of the name after . is used in decision encoding so for each HypoTool instance in the above example, m_decisionId.name() == "HLT_e20_loose_L1EM15". The HLT::Identifier has an integer representation of type DecisionID accessible with m_decisionId.numeric() == 3988941118. It is obtained by hashing m_decisionId.name() and is unique in every functioning configuration (i.e. a menu containing two HLT chains with a hash collision is an invalid menu). For size and efficiency, it is the numeric() ID that is used by the HypoTool to indicate that an object has passed its selection. For a concrete example of a HypoTool, see the TrigL2ElectronHypoTool.

For multi-leg chains, an additional string is added to identify the leg. I.e. for HLT_e10_e20_L12EM10 the framework would configure one HypoTool with name leg000_HLT_e10_e20_L12EM10, configured as HLT_e10_L12EM10, and another HypoTool with name leg001_HLT_e10_e20_L12EM10, configured as HLT_e20_L12EM10. These modifications to the names cause a corresponding change to the HypoTool’s m_decisionId.numeric() identifier.

In the subsequent framework-controlled ComboHypo, the unique DecisionID coming from the leg000_HLT_e10_e20_L12EM10 HypoTool and the leg001_HLT_e10_e20_L12EM10 HypoTool are used to check the multiplicity requirement of the multi-leg chain.

The HypoAlg

All HypoAlgs are expected to inherit from the HypoBase class. This base class standardizes the input and output data handle names for decision collections. In addition, the base class enforces the HypoAlgs to be multi-threading ready; that is the execute method is const, which adds constraints to the implementation.

The HypoTools, used by the HypoAlg have to adhere to some user-defined interface. It can be an abstract tool interface, with a set of specific tools each providing an implementation which matches the interface. Or, if only a single implementation of HypoTool is sufficient, the implementation can itself define the interface. See an example TrigL2ElectronHypoAlgMT.

Making decisions objects in an event

Each HypoAlg must acquire the set of HypoTools that it is responsible for executing. This is done via a private tool handle array which is populated by the menu generation code:

ToolHandleArray< HypoToolInterface > m_hypoTools {this, "HypoTools", {},
  "Tools that perform actual selection"};

In the HypoAlg::initialize method, the tools are retrieved. The key that corresponds to the collection of reconstructed objects that the HypoAlg will be filtering on should also be initialized. If the HypoAlg is running on the output of Event Views (see below), the key must be renounced from the data dependency resolution.

ATH_CHECK( m_hypoTools.retrieve() );
ATH_CHECK( m_objAHandleKey.initialize() );
renounce( m_objAHandleKey ); // Only if using Event Views

The logic in the HypoAlg::execute( const EventContext& context ) const method is responsible for acquiring the decisions of the previous step:

StatusCode HypoAlg::execute( const EventContext& context ) const {
  auto previousDecisionsHandle = SG::makeHandle( decisionInput(), context );
  // Creates the mutable output DecisionContainer and registers it to StoreGate.
  SG::WriteHandle<DecisionContainer> outputHandle 
    = createAndStore(decisionOutput(), context ); 
  DecisionContainer* decisions = outputHandle.ptr();

The HypoAlg obtains a collection of Decision objects from the previous step (with the initial set being derived from the L1 system), each storing a DecisionIDContainer of passing chains. It additionally obtains reconstructed objects from the current step.

The job of the HypoAlg is to create a new Decision per object reconstructed in the current step, associate this with Decision object(s) from the previous step, and subject each object to all HypoTool instances.

The HypoAlg is executed once per event, whereas the HypoTools needs to act on multiple objects. Hence it is customary for the HypoAlg to structure the data required by the HypoTool to perform their selection efficiently.

Here is an example custom structure to be passed to a HypoTool, it contains:

  • the set of passed DecisionIDs for this object from the previous step,
  • EDM object(s) on which the HypoTool operates to obtain its decision,
  • a mutable decision object onto which the HypoTool can append its own DecisionID, should it pass the object.
// This can be a simple struct as it is only used to
// communicate efficiently between the HypoAlg and HypoTool
struct ExampleHypoInfo {
    // The set of positive decisions made at the previous step 
    // (this is a std::set<DecisionID>)
    const DecisionIDContainer previousDecisionIDs;
    // object that is needed by HypoTool to decide
    const ObjA* electron; 
    // another object that is necessary
    const ObjB* cluster; 
    // and whatever else is necessary for tools
    // ... 
    // Place to store DecisionIDs from this step
    Decision* decision;
  };

The construction of a std::vector<ExampleHypoInfo> by the HypoAlg to be passed to its HypoTools differs depending on if the reconstruction step occurred in the full event context, or was restricted to one or more Event Views.

If the previous reconstruction step happened in Event Views

If the previous reconstruction step was performed in views, the reconstructed objects are extracted via the previousDecision. In this case, the handle for the input collection (m_objAHandleKey) should be renounced from the scheduler during initialization, as mentioned above.

For each reconstructed object, objA, a Decision object is created. A “feature” link must be created from the Decision object to objA. At least one Decision from within the previousDecision collection must be set as the parent of the Decision. This is done either by passing the previousDecision to the newDecisionIn call, or explicitly invoking linkToPrevious.

std::vector<ExampleHypoInfo> hypoToolInput;
for (const Decision* previousDecision: *previousDecisionsHandle) {

  // Extract the View from previousDecision
  // From this, obtain the collection of objA reconstructed within
  const auto viewELInfo = findLink< ViewContainer >( previousDecision, viewString() );
  ATH_CHECK( viewELInfo.isValid() );
  auto objACollectionHandle 
    = ViewHelper::makeHandle( *viewELInfo.link, m_objAHandleKey, context );
  ATH_CHECK( objACollectionHandle.isValid() );

  // Iterate over the object(s) reconstructed in the View, 
  // creating an output Decision object for each
  size_t objACounter = 0;
  for (const ObjectA* objA : *objACollectionHandle) {
    // Create a new output Decision object, d, backed by the 'decisions' container.
    // Links previousDecision as the parent of d.
    Decision* d = newDecisionIn( decisions, previousDecision );

    // Note: it may be necessary to re-map different decision(s) 
    // from the previousDecision container as the parent of d. 
    // If this is the case, then do the following where 
    // INDEX is the index within the previousDecision collection.
    // Decision* d = newDecisionIn( decisions );
    // linkToPrevious(d, decisionInput().key(), INDEX);

    // Obtain an ElementLink to objA and set mandatory feature link
    const ElementLink<ObjAContainer> objAEL 
      = ViewHelper::makeLink<ObjAContainer>( *viewELInfo.link, 
                                             objACollectionHandle, 
                                             objACounter );
    d->setObjectLink<ObjAContainer>( featureString(), objAEL );

    // Obtain anything else the HypoTool will need to make its decision.
    const ObjB* objb = ...;

    // Extract the set of previousDecisionIDs from the previousDecision.
    // If you needed to re-map the previous decision(s), then call decisionIDs once
    // for each previous decision
    DecisionIDContainer previousDecisionIDs;
    decisionIDs(previousDecision, previousDecisionIDs);

    // Collect all the required information for the tool together in a handy struct 
    hypoToolInput.push_back( ExampleHypoInfo{previousDecisionIDs, objA, objB, d} );

    ++objACounter;
  }
}

findLink performs a recursive search through the parents of a Decision object, and returns the first link with the specified name.

If the previous reconstruction step happened in the global event context

If the reconstruction was performed in the global event context (not in Views) the reconstructed objects have to be acquired using the input handles and matched to the previous decisions.

std::vector<ExampleHypoInfo> hypoToolInput;
const DecisionContainer* previousDecisions = *previousDecisionsHandle;
auto objACollectionHandle = SG::makeHandle( m_objAHandleKey, context );
for (const ObjectA* objA : *objACollectionHandle) {

  // Associate this object with one or more Decision objects from previousDecision
  // Here we assume that we expect to identify a single parent.
  const Decision* previousDecision = nullptr;
  for (const Decision* testDecision : *previousDecisionsHandle) {
    // associatedWithPreviousStep is some user-implemented matching logic
    // E.g. this could match to a RoIDescriptor 
    // retrieved from testDecision using findLink
    if (associatedWithPreviousStep(testDecision, objA)) {
      previousDecision = testDecision;
      break;
    }
  }
  if (!previousDecision) {
    ATH_MSG_ERROR("Unable to associate a previous decision.");
    return StatusCode::FAILURE;
  }

  // Create a new output Decision object, d, backed by the 'decisions' container.
  // Links previousDecision as the parent of d.
  Decision* d = newDecisionIn( decisions, previousDecision );

  // Obtain an ElementLink to objA and set mandatory feature link
  const ElementLink<ObjAContainer> objAEL 
    = ElementLink<ObjAContainer>( *objACollectionHandle, objA->index() );
  d->setObjectLink<ObjAContainer>( featureString(), objAEL );

  // Obtain anything else the HypoTool will need to make its decision.
  const ObjB* objb = ...;

  // Extract the set of previousDecisionIDs from the previousDecision.
  // If you needed to re-map the previous decision(s), then call decisionIDs once
  // for each previous decision
  DecisionIDContainer previousDecisionIDs;
  decisionIDs(previousDecision, previousDecisionIDs);

  // Collect all the required information for the tool together in a handy struct 
  hypoToolInput.push_back( ExampleHypoInfo{previousDecisionIDs, objA, objB, d} );
}

Running the HypoTools

Once the std::vector<ExampleHypoInfo> is prepared it has to be passed to all owned HypoTools then call the hypoBaseOutputProcessing function of the base class to validate that your Hypo has followed all of the rules and print out debug information.

//example code in the HypoAlg
 for ( auto & tool: m_hypoTools ) {
    ATH_CHECK( tool->decide( hypoToolInput ) );    
  } 
  
  ATH_CHECK( hypoBaseOutputProcessing(outputHandle) );  

The tool will typically iterate over the std::vector<ExampleHypoInfo> and, for every entry for which the previous decision was positive, evaluate the selection code and store any positive decisions.

std::vector<ExampleHypoInfo*> positive( input.size() );
for ( ExampleHypoInfo& hypoInfo: input ) {
  // Only consider objects which passed the 
  // corresponding HypoTool in the previous step.
  // This logic MUST be applied by your tool.
  if ( hypoInfo.previousDecisionsIDs.count( m_decisionId.numeric() ).count() > 0 ) {
    if (passSelection(hypoInfo)) { // User-defined
      addDecisionID( m_decisionId.numeric(), infoPtr->decision );
    }
  }
}

An example of such arrangement can be found in: Fast Electron in files: TrigL2ElectronHypoTool.h/cxx and TrigL2ElectronHypoAlgMT.h/cxx

Monitoring

For how to add monitoring in the HypoTool and HypoAlg see the respective section about online monitoring. Keep in mind that in the typical configuration there will be many HypoTools and thus adding any monitoring in them will result in multiple histograms that is potentially an issue for online monitoring information gathering.

As a rule of thumb in the HypoTool a simple variable like CutCounter should be monitored whereas the quantities characterising physics objects can be monitored in the HypoAlg or even in reconstruction algorithm itself. Note, however, that by doing that the quantity can be biased by menu configuration, so a right balance is needed.