diff --git a/Event/FourMomUtils/FourMomUtils/xAODP4Helpers.h b/Event/FourMomUtils/FourMomUtils/xAODP4Helpers.h
index 5364bc0a316312a47a77acbbf0b8bb85ffe465ce..199929b513dc8088fa2a2acd905209cfb20b81da 100644
--- a/Event/FourMomUtils/FourMomUtils/xAODP4Helpers.h
+++ b/Event/FourMomUtils/FourMomUtils/xAODP4Helpers.h
@@ -106,18 +106,24 @@ namespace xAOD
 
 
 
+    /// @f$ \Delta{R}^2 @f$ from bare rapidity,phi
+    inline
+    double deltaR2( double rapidity1, double phi1, double rapidity2, double phi2 )
+    {
+      const double dPhi = xAOD::P4Helpers::deltaPhi( phi1, phi2 );
+      const double dRapidity = rapidity1-rapidity2;
+      return dRapidity*dRapidity + dPhi*dPhi;
+    }
+
     /// @f$ \Delta{R}^2 @f$ from 1 @c xAOD::IParticle
     inline
     double deltaR2( const xAOD::IParticle& p4, double rapidity, double phi, bool useRapidity=true )
     {
-      const double dPhi = xAOD::P4Helpers::deltaPhi( p4, phi );
       if (useRapidity) {
-        const double dRapidity = p4.rapidity() - rapidity;
-        return dRapidity*dRapidity + dPhi*dPhi;
+        return xAOD::P4Helpers::deltaR2(p4.rapidity(),p4.phi(),rapidity,phi);
       }
       else {
-        const double dEta = p4.eta() - rapidity;
-        return dEta*dEta + dPhi*dPhi;
+        return xAOD::P4Helpers::deltaR2(p4.eta(),p4.phi(),rapidity,phi);
       }
     }
 
@@ -125,24 +131,24 @@ namespace xAOD
     inline
     double deltaR2( const xAOD::IParticle& pA, const xAOD::IParticle& pB, bool useRapidity=true )
     {
-      const double dPhi = xAOD::P4Helpers::deltaPhi( pA, pB );
-      const double dPhiSq = dPhi*dPhi;
       if (useRapidity) {
-        const double dRapidity = xAOD::P4Helpers::deltaRapidity( pA, pB );
-        return dRapidity*dRapidity + dPhiSq;
+        return xAOD::P4Helpers::deltaR2(pA.rapidity(),pA.phi(),pB.rapidity(),pB.phi());
       }
       else {
-        const double dEta = xAOD::P4Helpers::deltaEta( pA, pB );
-        return dEta*dEta + dPhiSq;
+        return xAOD::P4Helpers::deltaR2(pA.eta(),pA.phi(),pB.eta(),pB.phi());
       }
     }
 
-    /** delta R from two xAOD::IParticle pointers */
+    /** delta R^2 from two xAOD::IParticle pointers */
     inline
     double deltaR2( const xAOD::IParticle * const pA, const xAOD::IParticle * const pB, bool useRapidity=true )
     { return xAOD::P4Helpers::deltaR2( *pA, *pB, useRapidity ); }
 
 
+    /// @f$ \Delta{R} @f$ from bare bare rapidity,phi
+    inline
+    double deltaR( double rapidity1, double phi1, double rapidity2, double phi2 )
+    { return std::sqrt( xAOD::P4Helpers::deltaR2( rapidity1, phi1, rapidity2, phi2 ) ); }
 
     /// @f$ \Delta{R} @f$ from 1 @c xAOD::IParticle
     inline
diff --git a/Trigger/TrigHypothesis/TrigHypoCommonTools/CMakeLists.txt b/Trigger/TrigHypothesis/TrigHypoCommonTools/CMakeLists.txt
index c36e7f111374f22bbaaaef2012bad4000379db4b..4a6be0796776cd00fe1cfc4ca5c9e212bb3f88a7 100644
--- a/Trigger/TrigHypothesis/TrigHypoCommonTools/CMakeLists.txt
+++ b/Trigger/TrigHypothesis/TrigHypoCommonTools/CMakeLists.txt
@@ -3,11 +3,15 @@
 # Declare the package name:
 atlas_subdir( TrigHypoCommonTools )
 
+# External reqs
+find_package( ROOT COMPONENTS Core GenVector )
+
 # Component(s) in the package:
 atlas_add_component( TrigHypoCommonTools
                      src/*.cxx
                      src/components/*.cxx
-                     LINK_LIBRARIES AsgTools AthLinks AthenaBaseComps AthenaMonitoringKernelLib DecisionHandlingLib GaudiKernel HLTSeedingLib TrigCompositeUtilsLib TrigT1Result xAODBase )
+                     INCLUDE_DIRS ${ROOT_INCLUDE_DIRS}
+                     LINK_LIBRARIES ${ROOT_LIBRARIES} AsgTools AthLinks AthenaBaseComps AthenaMonitoringKernelLib DecisionHandlingLib FourMomUtils GaudiKernel HLTSeedingLib TrigCompositeUtilsLib TrigT1Result xAODBase )
 
 # Install files from the package:
 atlas_install_python_modules( python/*.py POST_BUILD_CMD ${ATLAS_FLAKE8} --extend-extensions=ATL900,ATL901 )
diff --git a/Trigger/TrigHypothesis/TrigHypoCommonTools/src/TrigComboHypoTool.cxx b/Trigger/TrigHypothesis/TrigHypoCommonTools/src/TrigComboHypoTool.cxx
index f456e6dd75e6bdda906e40e6d862ec0af52f46b1..761c0e828f7b821b5d9efd8659493abad21e3db6 100644
--- a/Trigger/TrigHypothesis/TrigHypoCommonTools/src/TrigComboHypoTool.cxx
+++ b/Trigger/TrigHypothesis/TrigHypoCommonTools/src/TrigComboHypoTool.cxx
@@ -3,26 +3,59 @@
 */
 
 #include "TrigComboHypoTool.h"
+#include "GaudiKernel/SystemOfUnits.h"
+#include <Math/Vector4D.h>    // for LorentzVector
+#include <Math/Vector4Dfwd.h> // PtEtaPhiM typedef
+#include <Math/Vector2D.h>    // for DisplacementVector
+#include <Math/Vector2Dfwd.h> // Polar2DVectorF typedef
+
 #include "xAODTrigMissingET/TrigMissingETContainer.h"
+#include "FourMomUtils/xAODP4Helpers.h"
 
 #include <vector>
+#include <algorithm>
 #include <cmath>
 
+constexpr float invGeV = 1. / Gaudi::Units::GeV;
+
 using namespace TrigCompositeUtils;
 
+// Translate strings into enum values
+const std::map<std::string, TrigComboHypoTool::ComboHypoVars> VarMap = {
+  {"dR",   TrigComboHypoTool::ComboHypoVars::DR},
+  {"invm", TrigComboHypoTool::ComboHypoVars::INVM},
+  {"dphi", TrigComboHypoTool::ComboHypoVars::DPHI},
+  {"mT",   TrigComboHypoTool::ComboHypoVars::MT}
+};
+
 TrigComboHypoTool::TrigComboHypoTool(const std::string& type, 
 				     const std::string& name, 
 				     const IInterface*  parent)
   : ComboHypoToolBase(type, name, parent)
 {}
 
-void   TrigComboHypoTool::fillVarMap(){
-  m_varMap["dR"]   = comboHypoVars::DR;
-  m_varMap["invm"] = comboHypoVars::INVM;
-  m_varMap["dphi"] = comboHypoVars::DPHI;
-  m_varMap["mT"]   = comboHypoVars::MT;
+
+bool TrigComboHypoTool::VarInfo::validate(std::string& errmsg) const {
+  if (legA==0){
+    errmsg = "legA ID not set!";
+    return false;
+  }
+  if (legB==0){
+    errmsg="legB ID not set!";
+    return false;
+  }
+  if ((!useMin) && (!useMax)){
+    errmsg="Trying to configure the Tool without setting at least one of UseMin or UseMax!";
+    return false;
+  }
+  if (legA==legB && (legA_is_MET || legB_is_MET)) {
+    errmsg = "Cannot specify the same MET leg for both sides!";
+    return false;
+  }
+  return true;
 }
 
+
 StatusCode TrigComboHypoTool::initialize()
 {
   ATH_MSG_DEBUG("Variable   = " << m_varTag_vec );
@@ -36,235 +69,268 @@ StatusCode TrigComboHypoTool::initialize()
   ATH_CHECK( m_monTool_vec.retrieve() );
 
   if (m_legA_vec.size() != m_legB_vec.size()) {
-    ATH_MSG_ERROR("Trying to configure the Tool with legA and legB with different size!");
+    ATH_MSG_ERROR("Trying to configure the Tool with legA and legB vectors of different size!");
     return StatusCode::FAILURE;
   }
   if (m_useMin_vec.size() != m_useMax_vec.size()) {
-    ATH_MSG_ERROR("Trying to configure the Tool with UseMin and UseMax with different size!");
+    ATH_MSG_ERROR("Trying to configure the Tool with UseMin and UseMax vectors of different size!");
     return StatusCode::FAILURE;
   }
   if (m_legA_vec.size() != m_useMax_vec.size()) {
-    ATH_MSG_ERROR("Trying to configure the Tool with legA/B and UseMax/Min with different size!");
+    ATH_MSG_ERROR("Trying to configure the Tool with legA/B and UseMax/Min vectors of different size!");
     return StatusCode::FAILURE;
   }
   if (m_varTag_vec.size() != m_useMax_vec.size()) {
-    ATH_MSG_ERROR("Trying to configure the Tool with varTag and UseMax/Min(LegA/B) with different size!");
+    ATH_MSG_ERROR("Trying to configure the Tool with varTag and UseMax/Min(LegA/B) vectors of different size!");
     return StatusCode::FAILURE;
   }
-  fillVarMap();
 
-  for (size_t i=0; i<m_legA_vec.size(); ++i){
-    if (m_legA_vec[i]==""){
-      ATH_MSG_ERROR("Array of legA IDs not set!");
-      return StatusCode::FAILURE;
-    }
-    if (m_legB_vec[i]==""){
-      ATH_MSG_ERROR("Array of legB IDs not set!");
-      return StatusCode::FAILURE;
+  for (size_t i=0; i<m_varTag_vec.size(); ++i){
+    VarInfo info;
+    info.index = i;
+    if(!m_monTool_vec.empty()) {
+      info.monToolName = m_monTool_vec[i].name();
     }
-    if ((!m_useMin_vec[i]) && (!m_useMax_vec[i])){
-      ATH_MSG_ERROR("Trying to configure the Tool without setting UseMin and UseMax!");
+    if (VarMap.find(m_varTag_vec[i]) == VarMap.end()){
+      ATH_MSG_ERROR("The variable is not present in the ComboHypoVars list");
       return StatusCode::FAILURE;
     }
-    if (m_varMap.find(m_varTag_vec[i]) == m_varMap.end()){
-      ATH_MSG_ERROR("The variable is not present in the comboHypoVars list");
+    info.varTag = (m_varTag_vec[i]);
+    info.var = VarMap.at(m_varTag_vec[i]);
+    //
+    info.useMin = m_useMin_vec[i];
+    if(info.useMin) {info.varMin=m_varMin_vec[i];}
+    info.useMax = m_useMax_vec[i];
+    if(info.useMax) {info.varMax=m_varMax_vec[i];}
+    //
+    info.legA = m_legA_vec[i];
+    info.legA_is_MET = m_isLegA_MET_vec[i];
+    info.legB = m_legB_vec[i];
+    info.legB_is_MET = m_isLegB_MET_vec[i];
+    info.legsAreEqual = info.legA==info.legB;
+
+    std::string validmsg{""};
+    if(!info.validate(validmsg)) {
+      ATH_MSG_ERROR(validmsg);
       return StatusCode::FAILURE;
     }
-    m_var_vec.push_back(m_varMap[m_varTag_vec[i]]);
+
+    m_varInfo_vec.push_back(info);
   }
   ATH_MSG_DEBUG("Initialization completed successfully");
   
   return StatusCode::SUCCESS;
 }
 
-bool TrigComboHypoTool::executeAlg(const std::vector<Combo::LegDecision>& combination) const {
+
+bool TrigComboHypoTool::executeAlg(const Combination& combination) const {
   //loop over all the hypos
-  bool                 lastDecision(true);
-  std::vector<float>   values;
-  values.reserve(m_varTag_vec.size());
+  bool lastDecision(true);
+  std::vector<float> values;
+  values.reserve(m_varInfo_vec.size());
 
-  for (size_t i=0; i<m_varTag_vec.size() && lastDecision; ++i){
-    lastDecision = executeAlgStep(combination, i, values);
+  for (auto varInfo = m_varInfo_vec.cbegin(); varInfo!=m_varInfo_vec.cend() && lastDecision; ++varInfo){
+    lastDecision = executeAlgStep(combination, *varInfo, values);
   }
-  
-  //now fill the monitoring histograms with the accepted values
-  for (size_t i=0; i<m_varTag_vec.size() && lastDecision; ++i){
-    std::string   monToolName=m_varTag_vec[i];
-    if (m_varTag_vec.size()>1){
-      monToolName = monToolName + m_legA_vec[i] + m_legB_vec[i];
+
+  // Monitoring of variables for only accepted events
+  if(lastDecision && !m_monTool_vec.empty()) {
+    for (auto varInfo = m_varInfo_vec.cbegin(); varInfo!=m_varInfo_vec.cend(); ++varInfo){
+      float value = values[varInfo->index];
+      auto varOfAccepted  = Monitored::Scalar(varInfo->monToolName+"OfAccepted", value );
+      auto monitorIt      = Monitored::Group (m_monTool_vec[varInfo->index], varOfAccepted);
+      ATH_MSG_DEBUG( varInfo->varTag << " = " << value << " is in range " << varInfo->rangeStr() << ".");
     }
-    auto varOfAccepted  = Monitored::Scalar(monToolName+"OfAccepted"   , -1.0 );
-    auto monitorIt      = Monitored::Group (m_monTool_vec[i], varOfAccepted);
-    varOfAccepted = values[i];
-    ATH_MSG_DEBUG( m_varTag_vec[i] <<"= "<< varOfAccepted << " is  within [" <<m_varMin_vec[i]<< "," << m_varMax_vec[i] << "] This selection passed! ");
   }
-  
   return lastDecision;
 }
 
-bool TrigComboHypoTool::executeAlgStep(const std::vector<Combo::LegDecision>& combination, size_t index, std::vector<float> &vals) const {
-  ATH_MSG_DEBUG("On combination executeAlg");
-  std::string   monToolName=m_varTag_vec[index];
-  if (m_varTag_vec.size()>1){
-    monToolName = monToolName + m_legA_vec[index] + m_legB_vec[index];
-  }
-  auto varOfProcessed = Monitored::Scalar(monToolName+"OfProcessed"  , -1.0 );
-  auto monitorIt      = Monitored::Group (m_monTool_vec[index], varOfProcessed);
 
-  //check that we found the two legs
-  int nCombs(combination.size());
-  if (nCombs < 2){
-    ATH_MSG_ERROR("Number of Decision Objects passed is less than 2! Sum over decision objects on all legs = " << combination.size() );
+bool TrigComboHypoTool::executeAlgStep(const Combination& combination, const VarInfo& varInfo, std::vector<float> &vals) const {
+  ATH_MSG_DEBUG("Executing selection " << varInfo.index << " of " << m_varInfo_vec.size() << ": " << varInfo.rangeStr());
+
+  std::pair<KineInfo,KineInfo> kinepair;
+  if(!fillPairKinematics(kinepair, combination, varInfo)) {
+    ATH_MSG_ERROR("Failed to extract kinematics of feature pair!");
     return false;
   }
-  int           legA_index(-1), legB_index(-1);
-
-  ATH_MSG_DEBUG("Decision objects available = "<< combination);
-  for (int i=0; i<nCombs; ++i){
-    auto combId = HLT::Identifier(combination[i].first);
-    if (!TrigCompositeUtils::isLegId(combId))
-      continue;
-    std::string   legName = combId.name().substr(0,6);
-    if (legName == m_legA_vec[index]){
-      if (legA_index != -1) {
-        ATH_MSG_WARNING("More than one Decision Object supplied on " << legName 
-          << "! E.g. from a 2muX leg or similar. Do not know which one to use, will take the last one!");
-      }
-      legA_index = i;
-    }else  if (legName == m_legB_vec[index]){
-      if (legB_index != -1) {
-        ATH_MSG_WARNING("More than one Decision Object supplied on " << legName 
-          << "! E.g. from a 2muX leg or similar. Do not know which one to use, will take the last one!");
-      }
-      legB_index = i;
-    }
-    ATH_MSG_DEBUG("\t Leg: "<< legName <<", full name:"<<combId.name());
+
+  if(msgLvl(MSG::DEBUG)) {
+    float eta_check, phi_check, pt_check;
+    std::tie(eta_check,phi_check,pt_check) = kinepair.first;
+    msg() << MSG::DEBUG << "Test filled legA kinematics: pt " << pt_check*invGeV << ", eta " << eta_check << ", phi " << phi_check << endmsg;
+
+    std::tie(eta_check,phi_check,pt_check) = kinepair.second;
+    msg() << MSG::DEBUG << "Test filled legB kinematics: pt " << pt_check*invGeV << ", eta " << eta_check << ", phi " << phi_check << endmsg;
   }
 
-  if ( legA_index<0){
-    ATH_MSG_ERROR("legA = "<< m_legA_vec[index] << " NOT FOUND!");
-    return false;
+  // apply the cut
+  float value = compute(kinepair,varInfo.var);
+  if(!m_monTool_vec.empty()) {
+    auto varOfProcessed = Monitored::Scalar(varInfo.monToolName+"OfProcessed"  , value );
+    auto monitorIt      = Monitored::Group (m_monTool_vec[varInfo.index], varOfProcessed);
   }
-  if ( legB_index<0){
-    ATH_MSG_ERROR("legB = "<< m_legB_vec[index] << " NOT FOUND!");
+  vals.push_back(value);
+  bool pass = varInfo.test(value);
+
+  ATH_MSG_DEBUG("Found a combination with " << value);
+  if(!pass) {
+    ATH_MSG_DEBUG("Combination failed var cut: " << varInfo.varTag << " = " << value << " not in range " << varInfo.rangeStr());
+  }
+  return pass;
+}
+
+
+/// Test function to compare decision ID with the legs to be used in var computation
+bool testLegId(const Combo::LegDecision& d, uint32_t targetleg) {
+  auto combId = HLT::Identifier(d.first);
+  if(!TrigCompositeUtils::isLegId(combId)) return false;
+  return combId.numeric() == targetleg;
+}
+
+
+bool TrigComboHypoTool::fillLegDecisions_sameLeg(std::pair<Combo::LegDecision,Combo::LegDecision>& legpair, const Combination& combination, uint32_t leg) const {
+  Combination leg_features;
+  if(m_skipLegCheck) {
+    // If there is only one leg, the decision IDs don't have a leg ID
+    std::copy(combination.begin(),combination.end(),std::back_inserter(leg_features));
+  } else {
+    // Extract the features matching the legs
+    // We take all of them, so as to be able to check if there is any ambiguity
+    auto isMyLeg = [&leg](const Combo::LegDecision& d) { return testLegId(d,leg); };
+    std::copy_if(combination.begin(),combination.end(),std::back_inserter(leg_features),isMyLeg);
+  }
+
+  if (leg_features.size()==2) {
+    legpair.first = leg_features[0];
+    legpair.second = leg_features[1];
+  } else {
+    ATH_MSG_ERROR(leg_features.size() << " Decision Objects supplied on leg " << leg
+                  << ", must be 2 for same-leg topo selection!");
     return false;
   }
 
-  float  eta1, phi1, pt1;
-  float  eta2, phi2, pt2;
-  
-  auto EL= combination[legA_index].second;    
+  return true;
+}
 
-  if (m_isLegA_MET_vec[index]) {
-    auto legA_pLink = TrigCompositeUtils::findLink<xAOD::TrigMissingETContainer>( *EL, featureString() ).link;
-    if (!legA_pLink.isValid()){
-      ATH_MSG_ERROR("link for "<<m_legA_vec[index]<<"is MET");
-      ATH_MSG_ERROR("link for "<<m_legA_vec[index]<<" not valid");
-      return false;
-    }
-    float ex = (*legA_pLink)[0].ex()/1000.;//converting to GeV
-    float ey = (*legA_pLink)[0].ey()/1000.;//converting to GeV
-    eta1 = -9999.;
-    phi1 = ex==0.0 && ey==0.0 ? 0.0: std::atan2(ey,ex);
-    pt1  = std::sqrt(ex*ex+ey*ey);
-  }else {
-    auto legA_pLink = TrigCompositeUtils::findLink<xAOD::IParticleContainer>( *EL, featureString() ).link;
-    if (!legA_pLink.isValid()){
-      ATH_MSG_ERROR("link for "<<m_legA_vec[index]<<"is not MET");
-      ATH_MSG_ERROR("link for "<<m_legA_vec[index]<<" not valid");
-      return false;
-    }
-    eta1 = (*legA_pLink)->p4().Eta();
-    phi1 = (*legA_pLink)->p4().Phi();
-    pt1  = (*legA_pLink)->p4().Pt();
 
+bool TrigComboHypoTool::fillLegDecisions_diffLeg(std::pair<Combo::LegDecision,Combo::LegDecision>& legpair, const Combination& combination, uint32_t legA, uint32_t legB) const {
+  // Extract the features matching the legs
+  // We take all of them, so as to be able to check if there is any ambiguity
+  auto isLegA = [&legA](const Combo::LegDecision& d) { return testLegId(d,legA); };
+  auto isLegB = [&legB](const Combo::LegDecision& d) { return testLegId(d,legB); };
+  Combination legA_features, legB_features;
+
+  std::copy_if(combination.begin(),combination.end(),std::back_inserter(legA_features),isLegA);
+  if(legA_features.size()!=1) {
+    ATH_MSG_ERROR(legA_features.size() << " Decision Objects supplied on leg " << legA
+                  << ", must be 1 for different-leg topo selection!");
+    return false;
+  }
+
+  std::copy_if(combination.begin(),combination.end(),std::back_inserter(legB_features),isLegB);
+  if (legB_features.size()!=1) {
+    ATH_MSG_ERROR(legB_features.size() << " Decision Objects supplied on leg " << legB
+                  << ", must be 1 for different-leg topo selection!");
+    return false;
   }
-  ATH_MSG_DEBUG("link for legA: "<<m_legA_vec[index]<<" is valid");
 
-  EL = combination[legB_index].second;
+  legpair.first = legA_features[0];
+  legpair.second = legB_features[0];
 
-  if (m_isLegB_MET_vec[index]) {
-    auto legB_pLink = TrigCompositeUtils::findLink<xAOD::TrigMissingETContainer>( *EL, featureString() ).link;
-    if (!legB_pLink.isValid()){
-      ATH_MSG_ERROR("link for "<<m_legB_vec[index]<<" not valid");
+  return true;
+}
+
+
+bool TrigComboHypoTool::fillPairKinematics(std::pair<KineInfo,KineInfo>& kinepair, const Combination& combination, const VarInfo& varInfo) const {
+    ATH_MSG_DEBUG("Decision objects available = "<< combination);
+    // Check that there are enough features
+    size_t nFeatures(combination.size());
+    if (nFeatures < 2){
+      ATH_MSG_ERROR("Number of Decision Objects passed is less than 2! Sum over decision objects on all legs = " << combination.size() );
       return false;
     }
-    float ex = (*legB_pLink)[0].ex()/1000.;//converting to GeV
-    float ey = (*legB_pLink)[0].ey()/1000.;//converting to GeV
-    eta2 = -9999.;
-    phi2 = ex==0.0 && ey==0.0 ? 0.0: std::atan2(ey,ex);
-    pt2  = std::sqrt(ex*ex+ey*ey);
-  }else {
-    auto legB_pLink = TrigCompositeUtils::findLink<xAOD::IParticleContainer>( *EL, featureString() ).link;
-    if (!legB_pLink.isValid()){
-      ATH_MSG_ERROR("link for "<<m_legB_vec[index]<<"is not MET");
-      ATH_MSG_ERROR("link for "<<m_legB_vec[index]<<" not valid");
+    std::pair<Combo::LegDecision,Combo::LegDecision> legpair;
+    if (varInfo.legsAreEqual) {fillLegDecisions_sameLeg(legpair,combination,varInfo.legA);}
+    else {fillLegDecisions_diffLeg(legpair,combination,varInfo.legA,varInfo.legB);}
+    ATH_MSG_DEBUG("Fill leg A kinematics");
+    if(!fillKineInfo(kinepair.first,legpair.first,varInfo.legA_is_MET)) {
+      ATH_MSG_ERROR("Failed to extract requisite kinematic info from leg " << varInfo.legA << "!");
       return false;
     }
-    eta2 = (*legB_pLink)->p4().Eta();
-    phi2 = (*legB_pLink)->p4().Phi();
-    pt2  = (*legB_pLink)->p4().Pt();
-  }
-  ATH_MSG_DEBUG("link for legB: "<<m_legB_vec[index]<<" is valid");
-
-  // apply the cut
-  bool  pass(true);
-  float value(-9999.);
-
-  switch(m_var_vec[index]){
-  case comboHypoVars::DR:
-    {
-      float dEta = eta2 - eta1;
-      float dPhi = -remainder( -phi1 + phi2, 2*M_PI );
-      value      = std::sqrt(dEta*dEta + dPhi*dPhi);
-      break;
-    }
-  case comboHypoVars::INVM:
-    {
-      value = std::sqrt(2.*pt1*pt2*(std::cosh(eta1 - eta2) - std::cos(phi1 - phi2) ) )*1e-3; // Convert to GeV
-      break;
-    }
-  case comboHypoVars::DPHI:
-    {
-      value = std::fabs(remainder( -phi1 + phi2, 2*M_PI ));
-      break;
-    }
-  case comboHypoVars::MT:
-    {
-      float ex = pt1*std::cos(phi1) + pt2*std::cos(phi2);
-      float ey = pt1*std::sin(phi1) + pt2*std::sin(phi2);
-      value    = std::sqrt(ex*ex + ey*ey)/1000.;//converting to GeV   
-      break;
+    ATH_MSG_DEBUG("Fill leg B kinematics");
+    if(!fillKineInfo(kinepair.second,legpair.second,varInfo.legB_is_MET)) {
+      ATH_MSG_ERROR("Failed to extract requisite kinematic info from leg " << varInfo.legB << "!");
+      return false;
     }
-  default:
-    ATH_MSG_ERROR("m_varTag =  "<<m_varTag_vec[index]<<" not present in the list of comboHypoVars");
-    return false;
+    return true;
   }
-  varOfProcessed = value;
-  vals.push_back(value);
 
-  ATH_MSG_DEBUG("Found a combination with " << varOfProcessed);
 
-  if (m_useMin_vec[index] && m_useMax_vec[index]){
-    if (varOfProcessed < m_varMin_vec[index] || varOfProcessed > m_varMax_vec[index]){ 
-      ATH_MSG_DEBUG("Combination failed var cut: "<< m_varTag_vec[index] <<"= "<< varOfProcessed << " not in [" << m_varMin_vec[index] << "," <<  m_varMax_vec[index] << "]");
-      pass=false;
-    }
-  }else if (m_useMin_vec[index]){
-    if (varOfProcessed < m_varMin_vec[index] ){ 
-      ATH_MSG_DEBUG("Combination failed var cut: "<< m_varTag_vec[index] <<"= "<< varOfProcessed << " not > " << m_varMin_vec[index]);
-      pass=false;
+bool TrigComboHypoTool::fillKineInfo(TrigComboHypoTool::KineInfo& kinematics, Combo::LegDecision decision, bool isMET) const {
+  float eta, phi, pt;
+  if (isMET) {
+    auto pLink = TrigCompositeUtils::findLink<xAOD::TrigMissingETContainer>( *decision.second, featureString() ).link;
+    if (!pLink.isValid()){
+      ATH_MSG_ERROR("link for MET not valid");
+      return false;
     }
-  }else if (m_useMax_vec[index]){
-    if (varOfProcessed > m_varMax_vec[index]){ 
-      ATH_MSG_DEBUG("Combination failed var cut: "<< m_varTag_vec[index] <<"= "<< varOfProcessed << " not < " << m_varMax_vec[index]);
-      pass=false;
+    ROOT::Math::XYVectorF metv((*pLink)->ex(),(*pLink)->ey());
+    eta = FLOATDEFAULT;
+    phi = metv.phi();
+    pt  = metv.r();
+  } else {
+    auto pLink = TrigCompositeUtils::findLink<xAOD::IParticleContainer>( *decision.second, featureString() ).link;
+    if (!pLink.isValid()){
+      ATH_MSG_ERROR("link for IParticle not valid");
+      return false;
     }
+    eta = (*pLink)->p4().Eta();
+    phi = (*pLink)->p4().Phi();
+    pt  = (*pLink)->p4().Pt();
   }
-  return pass;
-
+  ATH_MSG_DEBUG("Filled kinematics with pt " << pt*invGeV << ", eta " << eta << ", phi " << phi);
+  kinematics = std::make_tuple(eta,phi,pt);
+  return true;
 }
 
 
+float TrigComboHypoTool::compute(const std::pair<KineInfo,KineInfo>& kinepair, ComboHypoVars var) const {
+  const auto& [legA_kine,legB_kine] = kinepair;
+  const auto& [eta1,phi1,pt1] = legA_kine;
+  const auto& [eta2,phi2,pt2] = legB_kine;
 
+  ATH_MSG_DEBUG("Leg A has pt " << pt1*invGeV << ", eta " << eta1 << ", phi " << phi1);
+  ATH_MSG_DEBUG("Leg B has pt " << pt2*invGeV << ", eta " << eta2 << ", phi " << phi2);
 
+  float value(0);
+  switch(var) {
+    case ComboHypoVars::DR:
+      {
+        value = xAOD::P4Helpers::deltaR(eta1,phi1,eta2,phi2);
+        break;
+      }
+    case ComboHypoVars::DPHI:
+      {
+        value = xAOD::P4Helpers::deltaPhi(phi1,phi2);
+        break;
+      }
+    case ComboHypoVars::INVM:
+      {
+        ROOT::Math::PtEtaPhiMVector p1(pt1,eta1,phi1,0.), p2(pt2,eta2,phi2,0.);
+        value = (p1+p2).M()*invGeV; // Convert to GeV
+        break;
+      }
+    case ComboHypoVars::MT:
+      {
+        ROOT::Math::Polar2DVectorF p1(pt1,phi1), p2(pt2,phi2);
+        value = sqrt((p1+p2).Mag2())*invGeV; // Convert to GeV
+        break;
+      }
+    default:
+      {
+        ATH_MSG_ERROR("Undefined variable requested -- should never happen!");
+      }
+  }
+  return value;
+}
\ No newline at end of file
diff --git a/Trigger/TrigHypothesis/TrigHypoCommonTools/src/TrigComboHypoTool.h b/Trigger/TrigHypothesis/TrigHypoCommonTools/src/TrigComboHypoTool.h
index 3b5bfca80d9c2b353455765a42a29abb20539876..c9100bb114fdccaf375dbf229ee22a514eb36c64 100644
--- a/Trigger/TrigHypothesis/TrigHypoCommonTools/src/TrigComboHypoTool.h
+++ b/Trigger/TrigHypothesis/TrigHypoCommonTools/src/TrigComboHypoTool.h
@@ -7,6 +7,8 @@
 
 #include <string>
 #include <vector>
+#include <tuple>
+#include <limits>
 
 #include "DecisionHandling/ComboHypoToolBase.h"
 
@@ -29,7 +31,9 @@
 
 
 class TrigComboHypoTool:  public ComboHypoToolBase {
+
  public:
+  enum ComboHypoVars { UNDEF=-1, DR=0, DPHI, INVM, MT};
   
   TrigComboHypoTool(const std::string& type,
 		    const std::string& name,
@@ -38,35 +42,83 @@ class TrigComboHypoTool:  public ComboHypoToolBase {
   virtual ~TrigComboHypoTool() {};
   virtual StatusCode initialize() override;
 
-  enum comboHypoVars { DR=0, INVM, DPHI, MT}; 
-
  private:
-  
-  virtual bool executeAlg(const std::vector<Combo::LegDecision>& combination) const override;
-  bool         executeAlgStep(const std::vector<Combo::LegDecision>& combination, size_t index, std::vector<float>& values) const;
-  
+
+  static constexpr float FLOATDEFAULT = std::numeric_limits<float>::lowest();
+
+  /// Organise info per var selection in a struct
+  struct VarInfo {
+    std::string varTag{""};
+    ComboHypoVars var{UNDEF};
+    size_t index{0};
+    std::string monToolName{""};
+
+    bool useMin{false};
+    float varMin{FLOATDEFAULT};
+    bool useMax{false};
+    float varMax{FLOATDEFAULT};
+
+    bool legA_is_MET{false};
+    uint32_t legA{0};
+    bool legB_is_MET{false};
+    uint32_t legB{0};
+    bool legsAreEqual{false};
+
+    /// Check consistency of single var config
+    bool validate(std::string& errmsg) const;
+    /// Generate range string for printing
+    std::string rangeStr() const {
+      return (useMin ? std::to_string(varMin) + " < " : "") + varTag + (useMax ? " < " + std::to_string(varMax): "");
+    }
+    bool test(float value) const {
+      return (useMin ? value > varMin : true) && (useMax ? value < varMax : true);
+    }
+
+  };
+
+  /// Typedef for convenience, will contain eta/phi/pt info
+  typedef std::tuple<float,float,float> KineInfo;
+  typedef std::vector<Combo::LegDecision> Combination;
+
+  /// Top-level function to make chain-level decision
+  /// This applies the AND of all configured var selections
+  virtual bool executeAlg(const Combination& combination) const override;
+
+  /// Implementation of selection on individual variables
+  bool executeAlgStep(const Combination& combination, const VarInfo&, std::vector<float>& values) const;
+  /// Computation of the variables from the specified kinematics
+  float compute(const std::pair<KineInfo,KineInfo>& kinepair, ComboHypoVars var) const;
+
+  /// Helpers to extract kinematics from the specified legs of the chain
+  /// Specialised for two cases -- exactly two objects from the same leg
+  /// or exactly one object each from two legs.
+  bool fillLegDecisions_sameLeg(std::pair<Combo::LegDecision,Combo::LegDecision>& legpair, const Combination& combination, uint32_t leg) const;
+  bool fillLegDecisions_diffLeg(std::pair<Combo::LegDecision,Combo::LegDecision>& legpair, const Combination& combination, uint32_t legA, uint32_t legB) const;
+  bool fillPairKinematics(std::pair<KineInfo,KineInfo>& kinepair, const Combination& combination, const VarInfo& varInfo) const;
+  bool fillKineInfo(KineInfo& kinematics, Combo::LegDecision decision, bool isMET) const;
+
+  /// Gaudi configuration hooks
   // flags
-  Gaudi::Property<std::vector<std::string>> m_varTag_vec     {this, "Variables"  , {   ""}, "Variables to cut on"};
-  Gaudi::Property< std::vector<bool> >      m_useMin_vec     {this, "UseMinVec"  , {false}, "Array with the apply_min_cut setting"};
-  Gaudi::Property< std::vector<bool> >      m_useMax_vec     {this, "UseMaxVec"  , {false}, "Array with the apply_max_cut setting"};
+  Gaudi::Property<std::vector<std::string>> m_varTag_vec     {this, "Variables"  , {""}, "Variables to cut on"};
+  Gaudi::Property<std::vector<bool> >       m_useMin_vec     {this, "UseMinVec"  , {false}, "Array with the apply_min_cut setting"};
+  Gaudi::Property<std::vector<bool> >       m_useMax_vec     {this, "UseMaxVec"  , {false}, "Array with the apply_max_cut setting"};
 
   //legs
-  Gaudi::Property<std::vector<std::string>> m_legA_vec       {this, "LegAVec"      , {   ""}, "Array with the first Leg ID"};
-  Gaudi::Property<std::vector<std::string>> m_legB_vec       {this, "LegBVec"      , {   ""}, "Array with the second Leg ID"};
+  Gaudi::Property<std::vector<uint32_t>>    m_legA_vec       {this, "LegAVec"      , {0}, "Array with the first Leg ID"};
+  Gaudi::Property<std::vector<uint32_t>>    m_legB_vec       {this, "LegBVec"      , {0}, "Array with the second Leg ID"};
   Gaudi::Property<std::vector< bool >>      m_isLegA_MET_vec {this, "IsLegA_METVec", {false}, "Array with the first Leg MET identifier"};
   Gaudi::Property<std::vector< bool >>      m_isLegB_MET_vec {this, "IsLegB_METVec", {false}, "Array with the second Leg MET identifier"};
+  Gaudi::Property<bool>                     m_skipLegCheck   {this, "SkipLegCheck" , {false}, "Ignore leg IDs for chains with only one leg"};
 
   // cuts
-  Gaudi::Property<std::vector<float>>       m_varMin_vec     {this, "LowerCutVec", {-9999.}, "Array with the lower cut for legs pair"};
-  Gaudi::Property<std::vector<float>>       m_varMax_vec     {this, "UpperCutVec", {-9999.}, "Array with the upper cut for legs pair"};
+  Gaudi::Property<std::vector<float>>       m_varMin_vec     {this, "LowerCutVec", {FLOATDEFAULT}, "Array with the lower cut for legs pair"};
+  Gaudi::Property<std::vector<float>>       m_varMax_vec     {this, "UpperCutVec", {FLOATDEFAULT}, "Array with the upper cut for legs pair"};
   
   // monitoring
   ToolHandleArray<GenericMonitoringTool>    m_monTool_vec    {this, "MonTools", {}, "Monitoring tools" };
 
-  std::map<std::string, comboHypoVars>      m_varMap;
-  std::vector<comboHypoVars>                m_var_vec;
-
-  void     fillVarMap();
+  /// Internal variables for more efficient config lookup
+  std::vector<VarInfo>                      m_varInfo_vec;
 
 }; // TRIGCOMBOHYPO_TRIGCOMBOHYPOTOOL_H
 #endif
diff --git a/Trigger/TrigValidation/TrigAnalysisTest/share/ref_RDOtoRDOTrig_v1Dev_build.ref b/Trigger/TrigValidation/TrigAnalysisTest/share/ref_RDOtoRDOTrig_v1Dev_build.ref
index 3ada39a58ca12be91fd905f9d9d899ae67d21be8..71163814dcda7822b3c1f004f2c65dde2d3cbcc3 100644
--- a/Trigger/TrigValidation/TrigAnalysisTest/share/ref_RDOtoRDOTrig_v1Dev_build.ref
+++ b/Trigger/TrigValidation/TrigAnalysisTest/share/ref_RDOtoRDOTrig_v1Dev_build.ref
@@ -74,8 +74,14 @@ HLT_2g10_loose_mu20_L1MU20:
   stepFeatures:
     0: 14
     1: 1
+HLT_2g15_loose_25dphiAA_invmAA80_L1DPHI-M70-2eEM12M:
+  eventCount: 0
 HLT_2g15_loose_dPhi25_m80_L1DPHI-M70-2eEM12M:
   eventCount: 0
+HLT_2g15_tight_25dphiAA_L1DPHI-M70-2eEM12M:
+  eventCount: 0
+HLT_2g15_tight_25dphiAA_invmAA80_L1DPHI-M70-2eEM12M:
+  eventCount: 0
 HLT_2g15_tight_L1DPHI-M70-2eEM12M:
   eventCount: 0
 HLT_2g15_tight_dPhi25_L1DPHI-M70-2eEM12M:
@@ -621,6 +627,18 @@ HLT_2mu6_10invm70_L1MU6:
     1: 6
     2: 8
     3: 8
+HLT_2mu6_10invmAA70_L1MU6:
+  eventCount: 2
+  stepCounts:
+    0: 4
+    1: 3
+    2: 3
+    3: 2
+  stepFeatures:
+    0: 8
+    1: 6
+    2: 8
+    3: 8
 HLT_2mu6_2j50_0eta490_j0_DJMASS900j50_L1MJJ-500-NFF:
   eventCount: 0
 HLT_2mu6_L12MU6:
@@ -1463,10 +1481,28 @@ HLT_e140_lhloose_L1eEM22M:
   eventCount: 0
 HLT_e140_lhloose_noringer_L1EM22VHI:
   eventCount: 0
+HLT_e14_lhtight_e4_etcut_1invmAB3_L1JPSI-1M5-EM12:
+  eventCount: 0
+  stepFeatures:
+    0: 14
 HLT_e14_lhtight_e4_etcut_Jpsiee_L1JPSI-1M5-EM12:
   eventCount: 0
   stepFeatures:
     0: 14
+HLT_e14_lhtight_e4_etcut_probe_1invmAB3_L1JPSI-1M5-EM12:
+  eventCount: 0
+HLT_e14_lhtight_noringer_e4_etcut_1invmAB3_L1JPSI-1M5-EM12:
+  eventCount: 0
+  stepCounts:
+    0: 1
+    1: 1
+    2: 1
+    3: 1
+  stepFeatures:
+    0: 15
+    1: 18
+    2: 12
+    3: 7
 HLT_e14_lhtight_noringer_e4_etcut_Jpsiee_L1JPSI-1M5-EM12:
   eventCount: 0
   stepCounts:
@@ -1479,6 +1515,18 @@ HLT_e14_lhtight_noringer_e4_etcut_Jpsiee_L1JPSI-1M5-EM12:
     1: 18
     2: 12
     3: 7
+HLT_e14_lhtight_noringer_e4_etcut_probe_1invmAB3_L1JPSI-1M5-EM12:
+  eventCount: 0
+  stepCounts:
+    0: 1
+    1: 1
+    2: 1
+    3: 1
+  stepFeatures:
+    0: 1
+    1: 2
+    2: 1
+    3: 1
 HLT_e15_lhvloose_L1EM10VH:
   eventCount: 5
   stepCounts:
@@ -1674,15 +1722,15 @@ HLT_e20_lhvloose_L1EM15VH:
     2: 4
     3: 4
     4: 4
-HLT_e24_lhmedium_2g12_loose_02dRAB_L1EM20VH_3EM10VH:
+HLT_e24_lhmedium_g12_loose_g12_loose_02dRAB_02dRAC_L1EM20VH_3EM10VH:
   eventCount: 0
   stepCounts:
     0: 1
     1: 1
   stepFeatures:
-    0: 7
-    1: 3
-    2: 3
+    0: 13
+    1: 5
+    2: 5
 HLT_e24_lhmedium_g25_medium_02dRAB_L12EM20VH:
   eventCount: 0
   stepCounts:
@@ -1998,6 +2046,20 @@ HLT_e26_lhtight_L1EM22VHI:
     2: 4
     3: 4
     4: 4
+HLT_e26_lhtight_e15_etcut_50invmAB130_L1EM22VHI:
+  eventCount: 3
+  stepCounts:
+    0: 3
+    1: 3
+    2: 3
+    3: 3
+    4: 3
+  stepFeatures:
+    0: 23
+    1: 65
+    2: 19
+    3: 16
+    4: 3
 HLT_e26_lhtight_e15_etcut_L1EM22VHI:
   eventCount: 3
   stepCounts:
@@ -2040,6 +2102,28 @@ HLT_e26_lhtight_e15_etcut_idperf_Zee_L1EM22VHI:
     2: 19
     3: 16
     4: 3
+HLT_e26_lhtight_e15_etcut_probe_50invmAB130_L1EM22VHI:
+  eventCount: 3
+  stepCounts:
+    0: 5
+    1: 4
+    2: 4
+    3: 4
+    4: 4
+    5: 3
+    6: 3
+    7: 3
+    8: 3
+  stepFeatures:
+    0: 5
+    1: 6
+    2: 4
+    3: 4
+    4: 4
+    5: 14
+    6: 61
+    7: 16
+    8: 13
 HLT_e26_lhtight_ivarloose_2j20_0eta290_020jvt_pf_ftf_boffperf_L1EM22VHI:
   eventCount: 3
   stepCounts:
@@ -3058,6 +3142,18 @@ HLT_e5_idperf_tight_L1EM3:
     2: 24
     3: 24
     4: 24
+HLT_e5_lhtight_e14_etcut_1invmAB3_L1JPSI-1M5-EM12:
+  eventCount: 0
+  stepCounts:
+    0: 2
+    1: 2
+    2: 2
+    3: 2
+  stepFeatures:
+    0: 8
+    1: 23
+    2: 4
+    3: 4
 HLT_e5_lhtight_e14_etcut_Jpsiee_L1JPSI-1M5-EM12:
   eventCount: 0
   stepCounts:
@@ -3070,6 +3166,30 @@ HLT_e5_lhtight_e14_etcut_Jpsiee_L1JPSI-1M5-EM12:
     1: 23
     2: 4
     3: 4
+HLT_e5_lhtight_e14_etcut_probe_1invmAB3_L1JPSI-1M5-EM12:
+  eventCount: 0
+  stepCounts:
+    0: 2
+    1: 2
+    2: 2
+    3: 2
+  stepFeatures:
+    0: 4
+    1: 5
+    2: 2
+    3: 2
+HLT_e5_lhtight_e9_etcut_1invmAB3_L1JPSI-1M5-EM7:
+  eventCount: 0
+  stepCounts:
+    0: 3
+    1: 3
+    2: 3
+    3: 2
+  stepFeatures:
+    0: 17
+    1: 48
+    2: 10
+    3: 9
 HLT_e5_lhtight_e9_etcut_Jpsiee_L1JPSI-1M5-EM7:
   eventCount: 0
   stepCounts:
@@ -3082,6 +3202,27 @@ HLT_e5_lhtight_e9_etcut_Jpsiee_L1JPSI-1M5-EM7:
     1: 48
     2: 10
     3: 9
+HLT_e5_lhtight_e9_etcut_probe_1invmAB3_L1JPSI-1M5-EM7:
+  eventCount: 0
+  stepCounts:
+    0: 3
+    1: 3
+    2: 3
+    3: 3
+  stepFeatures:
+    0: 10
+    1: 22
+    2: 3
+    3: 3
+HLT_e5_lhtight_noringer_e14_etcut_1invmAB3_L1JPSI-1M5-EM12:
+  eventCount: 0
+  stepCounts:
+    0: 1
+    1: 1
+  stepFeatures:
+    0: 5
+    1: 10
+    2: 2
 HLT_e5_lhtight_noringer_e14_etcut_Jpsiee_L1JPSI-1M5-EM12:
   eventCount: 0
   stepCounts:
@@ -3091,6 +3232,30 @@ HLT_e5_lhtight_noringer_e14_etcut_Jpsiee_L1JPSI-1M5-EM12:
     0: 5
     1: 10
     2: 2
+HLT_e5_lhtight_noringer_e14_etcut_probe_1invmAB3_L1JPSI-1M5-EM12:
+  eventCount: 0
+  stepCounts:
+    0: 1
+    1: 1
+    2: 1
+    3: 1
+  stepFeatures:
+    0: 1
+    1: 2
+    2: 1
+    3: 1
+HLT_e5_lhtight_noringer_e9_etcut_1invmAB3_L1JPSI-1M5-EM7:
+  eventCount: 0
+  stepCounts:
+    0: 2
+    1: 2
+    2: 2
+    3: 1
+  stepFeatures:
+    0: 10
+    1: 21
+    2: 6
+    3: 5
 HLT_e5_lhtight_noringer_e9_etcut_Jpsiee_L1JPSI-1M5-EM7:
   eventCount: 0
   stepCounts:
@@ -3103,6 +3268,18 @@ HLT_e5_lhtight_noringer_e9_etcut_Jpsiee_L1JPSI-1M5-EM7:
     1: 21
     2: 6
     3: 5
+HLT_e5_lhtight_noringer_e9_etcut_probe_1invmAB3_L1JPSI-1M5-EM7:
+  eventCount: 0
+  stepCounts:
+    0: 2
+    1: 2
+    2: 2
+    3: 2
+  stepFeatures:
+    0: 3
+    1: 9
+    2: 2
+    3: 2
 HLT_e5_lhvloose_j70_0eta320_j50_0eta490_j0_DJMASS1000j50_xe50_tcpufit_L1MJJ-500-NFF:
   eventCount: 0
   stepCounts:
@@ -3272,6 +3449,15 @@ HLT_e7_lhmedium_mu24_L1MU20:
     8: 1
 HLT_e80_lhvloose_L1EM22VHI:
   eventCount: 0
+HLT_e9_lhtight_e4_etcut_1invmAB3_L1JPSI-1M5-EM7:
+  eventCount: 0
+  stepCounts:
+    0: 2
+    1: 2
+  stepFeatures:
+    0: 32
+    1: 88
+    2: 22
 HLT_e9_lhtight_e4_etcut_Jpsiee_L1JPSI-1M5-EM7:
   eventCount: 0
   stepCounts:
@@ -3290,6 +3476,26 @@ HLT_e9_lhtight_e4_etcut_L1JPSI-1M5-EM7:
     0: 32
     1: 88
     2: 22
+HLT_e9_lhtight_e4_etcut_probe_1invmAB3_L1JPSI-1M5-EM7:
+  eventCount: 0
+  stepCounts:
+    0: 2
+    1: 2
+  stepFeatures:
+    0: 2
+    1: 6
+HLT_e9_lhtight_noringer_e4_etcut_1invmAB3_L1JPSI-1M5-EM7:
+  eventCount: 0
+  stepCounts:
+    0: 2
+    1: 2
+    2: 1
+    3: 1
+  stepFeatures:
+    0: 32
+    1: 86
+    2: 29
+    3: 7
 HLT_e9_lhtight_noringer_e4_etcut_Jpsiee_L1JPSI-1M5-EM7:
   eventCount: 0
   stepCounts:
@@ -3302,6 +3508,18 @@ HLT_e9_lhtight_noringer_e4_etcut_Jpsiee_L1JPSI-1M5-EM7:
     1: 86
     2: 29
     3: 7
+HLT_e9_lhtight_noringer_e4_etcut_probe_1invmAB3_L1JPSI-1M5-EM7:
+  eventCount: 0
+  stepCounts:
+    0: 2
+    1: 2
+    2: 1
+    3: 1
+  stepFeatures:
+    0: 2
+    1: 6
+    2: 1
+    3: 1
 HLT_e9_lhvloose_mu20_mu8noL1_L1MU20:
   eventCount: 0
   stepCounts:
diff --git a/Trigger/TrigValidation/TriggerTest/share/ref_data_v1Dev_build.ref b/Trigger/TrigValidation/TriggerTest/share/ref_data_v1Dev_build.ref
index e270fc1fa64a42bfadba5c3d506fea29258b9576..608d39b9b2a7d512f770ff90ec434ab5116f06e3 100644
--- a/Trigger/TrigValidation/TriggerTest/share/ref_data_v1Dev_build.ref
+++ b/Trigger/TrigValidation/TriggerTest/share/ref_data_v1Dev_build.ref
@@ -36,8 +36,14 @@ HLT_2e5_lhvloose_bBeeM6000_L12EM3:
     3: 2
 HLT_2g10_loose_mu20_L1MU20:
   eventCount: 0
+HLT_2g15_loose_25dphiAA_invmAA80_L1DPHI-M70-2eEM12M:
+  eventCount: 0
 HLT_2g15_loose_dPhi25_m80_L1DPHI-M70-2eEM12M:
   eventCount: 0
+HLT_2g15_tight_25dphiAA_L1DPHI-M70-2eEM12M:
+  eventCount: 0
+HLT_2g15_tight_25dphiAA_invmAA80_L1DPHI-M70-2eEM12M:
+  eventCount: 0
 HLT_2g15_tight_L1DPHI-M70-2eEM12M:
   eventCount: 0
 HLT_2g15_tight_dPhi25_L1DPHI-M70-2eEM12M:
@@ -255,6 +261,12 @@ HLT_2mu6_10invm70_L1MU6:
     0: 1
   stepFeatures:
     0: 2
+HLT_2mu6_10invmAA70_L1MU6:
+  eventCount: 0
+  stepCounts:
+    0: 1
+  stepFeatures:
+    0: 2
 HLT_2mu6_2j50_0eta490_j0_DJMASS900j50_L1MJJ-500-NFF:
   eventCount: 0
 HLT_2mu6_L12MU6:
@@ -586,10 +598,18 @@ HLT_e140_lhloose_L1eEM22M:
   eventCount: 0
 HLT_e140_lhloose_noringer_L1EM22VHI:
   eventCount: 0
+HLT_e14_lhtight_e4_etcut_1invmAB3_L1JPSI-1M5-EM12:
+  eventCount: 0
 HLT_e14_lhtight_e4_etcut_Jpsiee_L1JPSI-1M5-EM12:
   eventCount: 0
+HLT_e14_lhtight_e4_etcut_probe_1invmAB3_L1JPSI-1M5-EM12:
+  eventCount: 0
+HLT_e14_lhtight_noringer_e4_etcut_1invmAB3_L1JPSI-1M5-EM12:
+  eventCount: 0
 HLT_e14_lhtight_noringer_e4_etcut_Jpsiee_L1JPSI-1M5-EM12:
   eventCount: 0
+HLT_e14_lhtight_noringer_e4_etcut_probe_1invmAB3_L1JPSI-1M5-EM12:
+  eventCount: 0
 HLT_e15_lhvloose_L1EM10VH:
   eventCount: 0
   stepCounts:
@@ -620,7 +640,7 @@ HLT_e20_lhtight_ivarloose_L1ZAFB-25DPHI-eEM18M:
   eventCount: 0
 HLT_e20_lhvloose_L1EM15VH:
   eventCount: 0
-HLT_e24_lhmedium_2g12_loose_02dRAB_L1EM20VH_3EM10VH:
+HLT_e24_lhmedium_g12_loose_g12_loose_02dRAB_02dRAC_L1EM20VH_3EM10VH:
   eventCount: 0
 HLT_e24_lhmedium_g25_medium_02dRAB_L12EM20VH:
   eventCount: 0
@@ -668,6 +688,10 @@ HLT_e26_lhmedium_nopix_lrttight_L1EM22VHI:
   eventCount: 0
 HLT_e26_lhtight_L1EM22VHI:
   eventCount: 0
+HLT_e26_lhtight_e15_etcut_50invmAB130_L1EM22VHI:
+  eventCount: 0
+  stepFeatures:
+    0: 1
 HLT_e26_lhtight_e15_etcut_L1EM22VHI:
   eventCount: 0
   stepFeatures:
@@ -680,6 +704,8 @@ HLT_e26_lhtight_e15_etcut_idperf_Zee_L1EM22VHI:
   eventCount: 0
   stepFeatures:
     0: 1
+HLT_e26_lhtight_e15_etcut_probe_50invmAB130_L1EM22VHI:
+  eventCount: 0
 HLT_e26_lhtight_ivarloose_2j20_0eta290_020jvt_pf_ftf_boffperf_L1EM22VHI:
   eventCount: 0
 HLT_e26_lhtight_ivarloose_L1EM22VHI:
@@ -900,18 +926,58 @@ HLT_e5_idperf_tight_L1EM3:
     2: 5
     3: 5
     4: 5
+HLT_e5_lhtight_e14_etcut_1invmAB3_L1JPSI-1M5-EM12:
+  eventCount: 0
 HLT_e5_lhtight_e14_etcut_Jpsiee_L1JPSI-1M5-EM12:
   eventCount: 0
+HLT_e5_lhtight_e14_etcut_probe_1invmAB3_L1JPSI-1M5-EM12:
+  eventCount: 0
+HLT_e5_lhtight_e9_etcut_1invmAB3_L1JPSI-1M5-EM7:
+  eventCount: 0
+  stepFeatures:
+    0: 3
 HLT_e5_lhtight_e9_etcut_Jpsiee_L1JPSI-1M5-EM7:
   eventCount: 0
   stepFeatures:
     0: 3
+HLT_e5_lhtight_e9_etcut_probe_1invmAB3_L1JPSI-1M5-EM7:
+  eventCount: 0
+  stepCounts:
+    0: 1
+    1: 1
+    2: 1
+    3: 1
+  stepFeatures:
+    0: 3
+    1: 6
+    2: 1
+    3: 1
+HLT_e5_lhtight_noringer_e14_etcut_1invmAB3_L1JPSI-1M5-EM12:
+  eventCount: 0
 HLT_e5_lhtight_noringer_e14_etcut_Jpsiee_L1JPSI-1M5-EM12:
   eventCount: 0
+HLT_e5_lhtight_noringer_e14_etcut_probe_1invmAB3_L1JPSI-1M5-EM12:
+  eventCount: 0
+HLT_e5_lhtight_noringer_e9_etcut_1invmAB3_L1JPSI-1M5-EM7:
+  eventCount: 0
+  stepFeatures:
+    0: 2
 HLT_e5_lhtight_noringer_e9_etcut_Jpsiee_L1JPSI-1M5-EM7:
   eventCount: 0
   stepFeatures:
     0: 2
+HLT_e5_lhtight_noringer_e9_etcut_probe_1invmAB3_L1JPSI-1M5-EM7:
+  eventCount: 0
+  stepCounts:
+    0: 1
+    1: 1
+    2: 1
+    3: 1
+  stepFeatures:
+    0: 2
+    1: 4
+    2: 1
+    3: 1
 HLT_e5_lhvloose_j70_0eta320_j50_0eta490_j0_DJMASS1000j50_xe50_tcpufit_L1MJJ-500-NFF:
   eventCount: 0
 HLT_e5_lhvloose_nopix_lrtloose_idperf_probe_g25_medium_L1EM20VH:
@@ -940,6 +1006,10 @@ HLT_e7_lhmedium_mu24_L1MU20:
   eventCount: 0
 HLT_e80_lhvloose_L1EM22VHI:
   eventCount: 0
+HLT_e9_lhtight_e4_etcut_1invmAB3_L1JPSI-1M5-EM7:
+  eventCount: 0
+  stepFeatures:
+    0: 5
 HLT_e9_lhtight_e4_etcut_Jpsiee_L1JPSI-1M5-EM7:
   eventCount: 0
   stepFeatures:
@@ -948,10 +1018,18 @@ HLT_e9_lhtight_e4_etcut_L1JPSI-1M5-EM7:
   eventCount: 0
   stepFeatures:
     0: 5
+HLT_e9_lhtight_e4_etcut_probe_1invmAB3_L1JPSI-1M5-EM7:
+  eventCount: 0
+HLT_e9_lhtight_noringer_e4_etcut_1invmAB3_L1JPSI-1M5-EM7:
+  eventCount: 0
+  stepFeatures:
+    0: 5
 HLT_e9_lhtight_noringer_e4_etcut_Jpsiee_L1JPSI-1M5-EM7:
   eventCount: 0
   stepFeatures:
     0: 5
+HLT_e9_lhtight_noringer_e4_etcut_probe_1invmAB3_L1JPSI-1M5-EM7:
+  eventCount: 0
 HLT_e9_lhvloose_mu20_mu8noL1_L1MU20:
   eventCount: 0
 HLT_eb_low_L1RD2_FILLED:
diff --git a/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/ComboHypoHandling.py b/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/ComboHypoHandling.py
index 551146debfe71adf0345c20f2134c100399ceb9a..c2b5c175212474e05e3de40e083e2c925139ac82 100644
--- a/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/ComboHypoHandling.py
+++ b/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/ComboHypoHandling.py
@@ -8,6 +8,8 @@ from AthenaCommon.Logging import logging
 log = logging.getLogger(__name__)
 logging.getLogger().info("Importing %s",__name__)
 import math
+import re
+from TrigConfHLTData.HLTUtils import string2hash
 
 topoLegIndices = "ABCDEF"
 
@@ -47,161 +49,145 @@ def TrigComboHypoToolFromDict(chainDict):
     from TrigHypoCommonTools.TrigHypoCommonToolsConf import TrigComboHypoTool
     from AthenaMonitoringKernel.GenericMonitoringTool import GenericMonitoringTool, defineHistogram
 
-    name     = chainDict['chainName']
-    log.debug("[TrigComboHypoToolFromDict] chain %s, combo hypos to be processed: %s, t", name, chainDict['extraComboHypos'])
+    chainName = chainDict['chainName']
+    log.debug("[TrigComboHypoToolFromDict] chain %s, combo hypos to be processed: %s, t", chainName, chainDict['extraComboHypos'])
     #define the list for housing all the info needed to initialize the TrigComboHypoTool module in the form of a dict
     topoDefs = []
 
-    for topoID in range(len(chainDict['extraComboHypos'])):
-        topoInfo = chainDict['extraComboHypos'][topoID]
-        #here we need to decompress the name to get: variable_name, min, max
-        log.debug("[TrigComboHypoToolFromDict] new combo hypo name: %s, topoInfo = %s", name, topoInfo)
-        singleTopoDef = {}
-
-        isLegMET = []
-        for chId in range(len(chainDict['chainParts'])):
-            if chainDict['chainParts'][chId]['signature'] == 'MET':
-                isLegMET.append(True)
-            else:
-                isLegMET.append(False)            
-            log.debug("[TrigComboHypoToolFromDict] chainParts[%i]: %s", chId, chainDict['chainParts'][chId])
-        
-        import re
-        # get the variable
-        obs_to_use = []
-        for obs in allowed_obs.keys():
-            if obs in topoInfo:
-                obs_to_use.append(obs)
-        if len(obs_to_use)!=1:
-            log.error("[TrigComboHypoToolFromDict] N of vars found in he hypo name = %d in chain name %s", len(obs_to_use), name)
-            raise Exception("[TrigComboHypoToolFromDict] N of vars found in the hypo name is different from 1")
-        singleTopoDef['var'] = obs_to_use[0]
-
-        #get the limits
-        l_min = re.findall(r"\d+"+obs_to_use[0], topoInfo)
-        if len(l_min)==1:
-            l_min[0] = l_min[0].replace(obs_to_use[0],"")
-            if obs_to_use[0] in ['dR','dPhi']:
-                cut_min = float(l_min[0])/10.
-            else:
-                cut_min = float(l_min[0])
-        if len(l_min)>1:
-            log.error("[TrigComboHypoToolFromDict] unable to get min value: N min = %d, l_min = %d", len(l_min), l_min)
-            raise Exception("[TrigComboHypoToolFromDict] cannot set min value")
-
-        if len(l_min)==1:#remove the min value from the string name
-            l_max = re.findall(r"\d+", topoInfo.replace(l_min[0],""))
-        else:#no min value was found
-            l_max = re.findall(r"\d+", topoInfo)
-        if len(l_max)>1:
-            log.error("[TrigComboHypoToolFromDict] unable to get max value: N max = %d, l_max = %d", len(l_max), l_max)
-            raise Exception("[TrigComboHypoToolFromDict] cannot set max value")
-        if len(l_max)==1:
-            if obs_to_use[0] in ['dR','dPhi']:
-                cut_max = float(l_max[0])/10.
-            else:
-                cut_max = float(l_min[0])
-        
-        #get the legs
-        l_names = topoInfo.replace(obs_to_use[0], "")
-        if len(l_min)>0: 
-            l_names = l_names.replace(l_min[0], "") 
-        if len(l_max)>0: 
-            l_names = l_names.replace(l_max[0], "") 
-
-        if len(l_names)!=2:
-            log.error("[TrigComboHypoToolFromDict] N_legs = %d, legs_name = %s", len(l_names), l_names)
-            raise Exception("[TrigComboHypoToolFromDict] Number of legs is different from 2")
-
-        legA = -1
-        legB = -1
-        for i in range(len(topoLegIndices)):
-            if topoLegIndices[i] == l_names[0]:
-                legA = i
-            elif topoLegIndices[i] == l_names[1]:
-                legB = i
-        if legA<0 or legB<0:
-            log.error("[TrigComboHypoToolFromDict] Didn't find leg indexes in %s", l_names)
-            raise Exception("[TrigComboHypoToolFromDict]  Didn't find leg indexes")
-        singleTopoDef['legA'] = "leg{:03d}".format(legA)
-        singleTopoDef['legB'] = "leg{:03d}".format(legB)
-
-        #count the number of MET legs used in the hypo
-        n_MET_legs=0
-        if isLegMET[legA]:
-            n_MET_legs += 1
-        if isLegMET[legB]:
-            n_MET_legs += 1
+    # Define regex for parsing the topoInfo
+    # Pattern is: min cut, var, legA, legB, max cut
+    # Min and max are both optional, check afterwards that at least one is filled
+    # Only the allowed vars and legs will be recognised, anything else fails to match
+    theregex = fr"(\d*)({'|'.join(allowed_obs.keys())})([{topoLegIndices}])([{topoLegIndices}])(\d*)"
+    matcher = re.compile(theregex)
+
+    for iTopo, topoInfo in enumerate(chainDict['extraComboHypos']):
+        log.debug("[TrigComboHypoToolFromDict] new combo hypo for chain: %s, topoInfo = %s", chainName, topoInfo)
+        # Attempt to regex-match the topo specification
+        result = matcher.match(topoInfo)
+        if not result:
+            log.error("[TrigComboHypoToolFromDict] Topo expression %s does not conform to format (min?)(var)(legA)(legB)(max?).",topoInfo)
+            log.error("[TrigComboHypoToolFromDict] Must use leg IDs in %s, vars in {allowed_obs.keys()}",topoLegIndices)
+            raise ValueError(f"[TrigComboHypoToolFromDict] Invalid topo expression {topoInfo} received in 'extraComboHypos'!")
+
+        # Extract the matched info and validate
+        str_min, var, char_legA, char_legB, str_max = result.groups()
+        # Manipulation of the cuts
+        # At least one must be filled
+        use_min = bool(str_min)
+        use_max = bool(str_max)
+        if not (use_min or use_max):
+            log.error("[TrigComboHypoToolFromDict] Topo expression %s does not specify a min or max cut value.",topoInfo)
+            raise ValueError(f"[TrigComboHypoToolFromDict] Invalid topo expression {topoInfo} received in 'extraComboHypos'!")
+        # Convert into float values, dividing for 0.1 precision as needed
+        if var in ['dR','dphi']:
+            cut_min = float(str_min)/10. if use_min else float('nan')
+            cut_max = float(str_max)/10. if use_max else float('nan')
+        else:
+            cut_min = float(str_min) if use_min else float('nan')
+            cut_max = float(str_max) if use_max else float('nan')
+
+        # Convert char leg representation to int
+        i_legA = topoLegIndices.find(char_legA)
+        i_legB = topoLegIndices.find(char_legB)
+
+        # Fill info for each leg, looking up in chainParts
+        # Convert leg name into HLT identifier for matching in the tool
+        legInfo = []
+        for ileg in [i_legA,i_legB]:
+            cpart = chainDict['chainParts'][ileg]
+            legname = f"leg{ileg:03d}_{chainName}"
+            legInfo.append({
+                'index'       : ileg,
+                'legname'     : legname,
+                'HLTId'       : string2hash(legname),
+                'isMET'       : cpart['signature']=='MET',
+                'multiplicity': int(cpart['multiplicity'])
+            })
+
+        # Count how many input legs are MET, for consistency checks
+        n_MET_legs = legInfo[0]['isMET'] + legInfo[1]['isMET']
+
+        # Check multiplicity of the configured legs
+        # For chains like "HLT_2muX_10invm70AA", no leg ID will be attached
+        # in which case set a flag to use all objects in the combination list
+        skipLegCheck = False
+        if i_legA==i_legB:
+            if legInfo[0]['multiplicity'] != 2:
+                log.error("[TrigComboHypoToolFromDict] Error configuring topo for chain %s!",chainName)
+                log.error("[TrigComboHypoToolFromDict] Topo selection %s requires multiplicity 2 on leg %d, found %d!",topoInfo,i_legA,legInfo[0]['multiplicity'])
+                raise Exception("[TrigComboHypoToolFromDict] Invalid multiplicity")
+            if n_MET_legs==2:
+                log.error("[TrigComboHypoToolFromDict] Configured with the same MET leg on both sides -- impossible to satisfy!")
+                raise Exception("[TrigComboHypoToolFromDict] Identical MET legs for topo selection")
+            if len(chainDict['chainParts'])==1:
+                skipLegCheck=True
+        else:
+            for li in legInfo:
+                if li['multiplicity'] != 1:
+                    log.error("[TrigComboHypoToolFromDict] Error configuring topo for chain %s!",chainName)
+                    log.error("[TrigComboHypoToolFromDict] Topo selection %s requires multiplicity 1 on leg %d, found %d!",topoInfo,li['index'],li['multiplicity'])
+                    raise Exception("[TrigComboHypoToolFromDict] Invalid multiplicity")
+
         #now check that the variable we plan to use allows the use of the MET
-        if n_MET_legs not in allowed_obs[obs_to_use[0]]['n_MET_legs']:
-            log.error("[TrigComboHypoToolFromDict] Attempting to use the MET leg in var %s. N_MET_legs = %d", obs_to_use[0], isLegMET.count(True))
+        if n_MET_legs not in allowed_obs[var]['n_MET_legs']:
+            log.error("[TrigComboHypoToolFromDict] Attempting var %s with %d MET legs, %s allowed", var, n_MET_legs, allowed_obs[var]['n_MET_legs'])
             raise Exception("[TrigComboHypoToolFromDict] Attempting to use the MET leg in var")
-        singleTopoDef['isLegA_MET'] = isLegMET[legA]
-        singleTopoDef['isLegB_MET'] = isLegMET[legB]
-
+ 
         if len(chainDict['extraComboHypos'])==1:#to avoid breaking changes in the ref files
-            monToolName = "MonTool_"+name
-            histNameTag = obs_to_use[0]
+            monToolName = "MonTool_"+chainName
+            histNameTag = var
         else:
-            monToolName = "MonTool_"+name+"_"+chainDict['extraComboHypos'][topoID]
-            histNameTag = obs_to_use[0] + "leg{:03d}".format(legA) + "leg{:03d}".format(legB)
+            monToolName = f"MonTool_{chainName}_{chainDict['extraComboHypos'][iTopo]}"
+            histNameTag = f"{var}leg{i_legA:03d}leg{i_legB:03d}"
         monTool = GenericMonitoringTool(monToolName)
-        monTool.Histograms = [defineHistogram(histNameTag+'OfAccepted', type='TH1F', path='EXPERT', 
-                                              title=obs_to_use[0]+" in accepted combinations; {}".format(obs_to_use[0]), 
-                                              xbins=allowed_obs[obs_to_use[0]]['hist_nbins'], 
-                                              xmin=allowed_obs[obs_to_use[0]]['hist_min'], 
-                                              xmax=allowed_obs[obs_to_use[0]]['hist_max']), 
-                              defineHistogram(histNameTag+'OfProcessed', type='TH1F', path='EXPERT', 
-                                              title=obs_to_use[0]+" in processed combinations; {}".format(obs_to_use[0]), 
-                                              xbins=allowed_obs[obs_to_use[0]]['hist_nbins'], 
-                                              xmin=allowed_obs[obs_to_use[0]]['hist_min'], 
-                                              xmax=allowed_obs[obs_to_use[0]]['hist_max'])]
-        log.debug("[TrigComboHypoToolFromDict] tool configured for hypo name: %s, topoInfo = %s", name, topoInfo)
-        #now fill the holders needed to initialize the TrigComboHypoTool
-        if len(l_min)==1:
-            singleTopoDef['useMin']   = True
-            singleTopoDef['lowerCut'] = cut_min
-        else:
-            singleTopoDef['useMin']   = False
-            singleTopoDef['lowerCut'] = 0.
-            
-        if len(l_max)==1:
-            singleTopoDef['useMax']   = True
-            singleTopoDef['upperCut'] = cut_max
-        else:
-            singleTopoDef['useMax']   = False
-            singleTopoDef['upperCut'] = 0.
+        monTool.Histograms = [defineHistogram(histNameTag+'OfAccepted', type='TH1F', path='EXPERT',
+                                              title=var+" in accepted combinations; {}".format(var),
+                                              xbins=allowed_obs[var]['hist_nbins'],
+                                              xmin=allowed_obs[var]['hist_min'],
+                                              xmax=allowed_obs[var]['hist_max']),
+                              defineHistogram(histNameTag+'OfProcessed', type='TH1F', path='EXPERT',
+                                              title=var+" in processed combinations; {}".format(var),
+                                              xbins=allowed_obs[var]['hist_nbins'],
+                                              xmin=allowed_obs[var]['hist_min'],
+                                              xmax=allowed_obs[var]['hist_max'])]
+        log.debug("[TrigComboHypoToolFromDict] tool configured for hypo name: %s, topoInfo = %s", chainName, topoInfo)
 
         if len(chainDict['extraComboHypos'])==1:#to avoid breaking changes in the ref files
-            monTool.HistPath = 'ComboHypo/'+name
+            monTool.HistPath = f'ComboHypo/{chainName}'
         else:
-            monTool.HistPath = 'ComboHypo/'+name+"_detail_"+singleTopoDef['var'] + singleTopoDef['legA'] + singleTopoDef['legB']
-        singleTopoDef['monTool'] = monTool
-
+            monTool.HistPath = f'ComboHypo/{chainName}_detail_{histNameTag}'
+
+        # Set keys of dict to match tool config properties
+        singleTopoDef = {
+            "Variables"    : var,
+            "UseMinVec"    : use_min,
+            "UseMaxVec"    : use_max,
+            "LowerCutVec"  : cut_min,
+            "UpperCutVec"  : cut_max,
+            "LegAVec"      : legInfo[0]["HLTId"],
+            "LegBVec"      : legInfo[1]["HLTId"],
+            "IsLegA_METVec": legInfo[0]["isMET"],
+            "IsLegB_METVec": legInfo[1]["isMET"],
+            "MonTools"     : monTool,
+        }
         topoDefs.append(singleTopoDef)
 
         #some debug info
-        log.debug("[TrigComboHypoToolFromDict] tool configured for hypo name: %s, topoInfo = %s", name, topoInfo)
-        log.debug("[TrigComboHypoToolFromDict] var  = %s", singleTopoDef['var'])
-        log.debug("[TrigComboHypoToolFromDict] legA = %d", singleTopoDef['legA'])
-        log.debug("[TrigComboHypoToolFromDict] legB = %d", singleTopoDef['legB'])
-        if len(l_min)==1:
-            log.debug("[TrigComboHypoToolFromDict] min  = %10.3f", singleTopoDef['lowerCut'])
-        if len(l_max)==1:
-            log.debug("[TrigComboHypoToolFromDict] max  = %10.3f", singleTopoDef['upperCut'])
+        log.debug("[TrigComboHypoToolFromDict] tool configured for hypo name: %s, topoInfo = %s", chainName, topoInfo)
+        log.debug("[TrigComboHypoToolFromDict] var  = %s", singleTopoDef['Variables'])
+        log.debug("[TrigComboHypoToolFromDict] legA = %s", singleTopoDef['LegAVec'])
+        log.debug("[TrigComboHypoToolFromDict] legB = %s", singleTopoDef['LegBVec'])
+        if use_min:
+            log.debug("[TrigComboHypoToolFromDict] min  = %10.3f", singleTopoDef['LowerCutVec'])
+        if use_max:
+            log.debug("[TrigComboHypoToolFromDict] max  = %10.3f", singleTopoDef['UpperCutVec'])
 
         #end of the loop over the hypos
-    tool= TrigComboHypoTool(name)
-    tool.Variables      = [x['var']        for x in topoDefs]
-    tool.LegAVec        = [x['legA']       for x in topoDefs]
-    tool.IsLegA_METVec  = [x['isLegA_MET'] for x in topoDefs]
-    tool.LegBVec        = [x['legB']       for x in topoDefs]
-    tool.IsLegB_METVec  = [x['isLegB_MET'] for x in topoDefs]
-    tool.UseMinVec      = [x['useMin']     for x in topoDefs]
-    tool.LowerCutVec    = [x['lowerCut']   for x in topoDefs]
-    tool.UseMaxVec      = [x['useMax']     for x in topoDefs]
-    tool.UpperCutVec    = [x['upperCut']   for x in topoDefs]
-    tool.MonTools       = [x['monTool']    for x in topoDefs]
+    
+    # convert list of dicts into dict of lists
+    toolProps = {k:[thedef[k] for thedef in topoDefs] for k in topoDefs[0]}
+    tool = TrigComboHypoTool(chainName, SkipLegCheck=skipLegCheck, **toolProps)
 
     return tool
 
@@ -214,6 +200,7 @@ comboConfigurator = {
 }
 
 def addTopoInfo(theChainConfig, mainChainDict, listOfChainDefs, lengthOfChainConfigs):
+    log.debug("[addTopoInfo] Adding topo info to chain %s", theChainConfig)
 
     def findStepIndexInChain(chain, step):
         for istep,chainstep in enumerate(chain.steps):
@@ -222,7 +209,8 @@ def addTopoInfo(theChainConfig, mainChainDict, listOfChainDefs, lengthOfChainCon
         return None
 
     for step,(topoCfg,topoExpr) in theChainConfig.topoMap.items():
-        thestep = -1 if step=="last" else findStepIndexInChain(theChainConfig,step)
+        thestep = theChainConfig.steps[-1] if step=="last" else theChainConfig.steps[findStepIndexInChain(theChainConfig,step)]
+        log.debug("[addTopoInfo] Adding %s to step %s",topoExpr,thestep)
         if thestep is None:
             log.error("Failed to find step %s in Chain! ChainDict follows:", step)
             log.error(mainChainDict)
@@ -237,16 +225,16 @@ def addTopoInfo(theChainConfig, mainChainDict, listOfChainDefs, lengthOfChainCon
 
         # No need to add if it has been added previously
         # Handle better and avoid doing this repeatedly on the same steps?
-        if topoCfg not in theChainConfig.steps[thestep].comboToolConfs:
-            if len(theChainConfig.steps[thestep].comboToolConfs) > 0:
-                log.warning("[addTopoInfo] step %s already has ComboHypo tools %s",theChainConfig.steps[thestep],theChainConfig.steps[thestep].comboToolConfs)
+        if topoCfg not in thestep.comboToolConfs:
+            if len(thestep.comboToolConfs) > 0:
+                log.warning("[addTopoInfo] step %s already has ComboHypo tools %s",thestep,thestep.comboToolConfs)
                 log.warning("[addTopoInfo] these will be added to, make sure this is the behaviour you want.")
 
-            theChainConfig.steps[thestep].addComboHypoTools(topoCfg)
-            theChainConfig.steps[thestep].name = theChainConfig.steps[thestep].name+'_combo_'+topoExpr 
+            thestep.name = thestep.name+'_combo_'+topoExpr 
+            thestep.addComboHypoTools(topoCfg)
     
-            theChainConfig.steps[thestep].makeCombo()
-            log.debug("[addTopoInfo] new combo hypo name: %s",theChainConfig.steps[thestep].combo.name)
+            thestep.makeCombo()
+            log.debug("[addTopoInfo] new combo hypo name: %s",thestep.combo.name)
 
             if bonus_debug:
                 log.debug("[addTopoInfo] new theChainConfig %s", theChainConfig)
diff --git a/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/LS2_v1.py b/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/LS2_v1.py
index 2272c8d226ac76e7b4cc8038349b92c627ce4c3d..bb8051ace40e9e1ecd530e160555359359bb769b 100644
--- a/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/LS2_v1.py
+++ b/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/LS2_v1.py
@@ -86,6 +86,8 @@ def setupMenu():
         ChainProp(name='HLT_mu6_msonly_L1MU6',     groups=SingleMuonGroup, monGroups=['muonMon:shifter','muonMon:val','idMon:t0']),
 
         ChainProp(name='HLT_2mu6_10invm70_L1MU6', groups=SingleMuonGroup),
+        # Using generic hypo
+        ChainProp(name='HLT_2mu6_10invmAA70_L1MU6', groups=SingleMuonGroup),
         ChainProp(name='HLT_mu10_lateMu_L1LATE-MU10_XE50', l1SeedThresholds=['FSNOSEED'], groups=SingleMuonGroup),
 
         # ATR-20049
@@ -200,6 +202,10 @@ def setupMenu():
         #Support photon chains ATR-23425
         ChainProp(name='HLT_2g15_loose_dPhi25_m80_L1DPHI-M70-2eEM12M', l1SeedThresholds=['eEM10L'], groups=SupportPhIGroup+MultiPhotonGroup), # TODO: mismatch between L1topo threshold and L1 seed to be fixed
         ChainProp(name='HLT_2g20_loose_L12EM15VH', groups=SupportLegGroup+MultiPhotonGroup),
+        # Copy with generic TrigComboHypoTool
+        ChainProp(name='HLT_2g15_tight_25dphiAA_invmAA80_L1DPHI-M70-2eEM12M', l1SeedThresholds=['eEM10L'], groups=PrimaryPhIGroup+MultiPhotonGroup),
+        ChainProp(name='HLT_2g15_loose_25dphiAA_invmAA80_L1DPHI-M70-2eEM12M', l1SeedThresholds=['eEM10L'], groups=SupportPhIGroup+MultiPhotonGroup),
+        ChainProp(name='HLT_2g15_tight_25dphiAA_L1DPHI-M70-2eEM12M', l1SeedThresholds=['eEM10L'], groups=PrimaryPhIGroup+MultiPhotonGroup),
         
         #------------ 1e_1g HEG ATR-23158
         ChainProp(name='HLT_e25_mergedtight_g35_medium_Heg_02dRAB_L12EM20VH',l1SeedThresholds=['EM20VH','EM20VH'], groups=PrimaryLegGroup+MultiElectronGroup), 
@@ -287,6 +293,30 @@ def setupMenu():
         ChainProp(name='HLT_2g22_tight_L1EM7_UNPAIRED_ISO', l1SeedThresholds=['EM7'], stream=['Late'], groups=PrimaryLegGroup+MultiPhotonGroup),
         ChainProp(name='HLT_2g50_tight_L1EM7_EMPTY', l1SeedThresholds=['EM7'], stream=['Late'], groups=PrimaryLegGroup+MultiPhotonGroup),
         ChainProp(name='HLT_2g50_tight_L1EM7_UNPAIRED_ISO', l1SeedThresholds=['EM7'], stream=['Late'], groups=PrimaryLegGroup+MultiPhotonGroup),
+
+        # Alternative formulation of T&P chains with generic mass cut combohypotool
+        # With & without 'probe' expression to check count consistency
+        # Zee
+        ChainProp(name='HLT_e26_lhtight_e15_etcut_probe_50invmAB130_L1EM22VHI', l1SeedThresholds=['EM22VHI','EM7'], groups=TagAndProbeLegGroup+MultiElectronGroup), 
+        ChainProp(name='HLT_e26_lhtight_e15_etcut_50invmAB130_L1EM22VHI', l1SeedThresholds=['EM22VHI','EM7'], groups=MultiElectronGroup), 
+        # Jpsiee
+        ChainProp(name='HLT_e9_lhtight_e4_etcut_probe_1invmAB3_L1JPSI-1M5-EM7', l1SeedThresholds=['EM7','EM3'], groups=SupportLegGroup+MultiElectronGroup), 
+        ChainProp(name='HLT_e5_lhtight_e9_etcut_probe_1invmAB3_L1JPSI-1M5-EM7', l1SeedThresholds=['EM3','EM7'], groups=SupportLegGroup+MultiElectronGroup), 
+        ChainProp(name='HLT_e14_lhtight_e4_etcut_probe_1invmAB3_L1JPSI-1M5-EM12', l1SeedThresholds=['EM12','EM3'], groups=SupportLegGroup+MultiElectronGroup), 
+        ChainProp(name='HLT_e5_lhtight_e14_etcut_probe_1invmAB3_L1JPSI-1M5-EM12', l1SeedThresholds=['EM3','EM12'], groups=SupportLegGroup+MultiElectronGroup), 
+        ChainProp(name='HLT_e9_lhtight_noringer_e4_etcut_probe_1invmAB3_L1JPSI-1M5-EM7', l1SeedThresholds=['EM7','EM3'], groups=SupportLegGroup+MultiElectronGroup), 
+        ChainProp(name='HLT_e5_lhtight_noringer_e9_etcut_probe_1invmAB3_L1JPSI-1M5-EM7', l1SeedThresholds=['EM3','EM7'], groups=SupportLegGroup+MultiElectronGroup), 
+        ChainProp(name='HLT_e14_lhtight_noringer_e4_etcut_probe_1invmAB3_L1JPSI-1M5-EM12', l1SeedThresholds=['EM12','EM3'], groups=SupportLegGroup+MultiElectronGroup), 
+        ChainProp(name='HLT_e5_lhtight_noringer_e14_etcut_probe_1invmAB3_L1JPSI-1M5-EM12', l1SeedThresholds=['EM3','EM12'], groups=SupportLegGroup+MultiElectronGroup), 
+        #
+        ChainProp(name='HLT_e9_lhtight_e4_etcut_1invmAB3_L1JPSI-1M5-EM7', l1SeedThresholds=['EM7','EM3'], groups=MultiElectronGroup), 
+        ChainProp(name='HLT_e5_lhtight_e9_etcut_1invmAB3_L1JPSI-1M5-EM7', l1SeedThresholds=['EM3','EM7'], groups=MultiElectronGroup), 
+        ChainProp(name='HLT_e14_lhtight_e4_etcut_1invmAB3_L1JPSI-1M5-EM12', l1SeedThresholds=['EM12','EM3'], groups=MultiElectronGroup), 
+        ChainProp(name='HLT_e5_lhtight_e14_etcut_1invmAB3_L1JPSI-1M5-EM12', l1SeedThresholds=['EM3','EM12'], groups=MultiElectronGroup), 
+        ChainProp(name='HLT_e9_lhtight_noringer_e4_etcut_1invmAB3_L1JPSI-1M5-EM7', l1SeedThresholds=['EM7','EM3'], groups=MultiElectronGroup), 
+        ChainProp(name='HLT_e5_lhtight_noringer_e9_etcut_1invmAB3_L1JPSI-1M5-EM7', l1SeedThresholds=['EM3','EM7'], groups=MultiElectronGroup), 
+        ChainProp(name='HLT_e14_lhtight_noringer_e4_etcut_1invmAB3_L1JPSI-1M5-EM12', l1SeedThresholds=['EM12','EM3'], groups=MultiElectronGroup), 
+        ChainProp(name='HLT_e5_lhtight_noringer_e14_etcut_1invmAB3_L1JPSI-1M5-EM12', l1SeedThresholds=['EM3','EM12'], groups=MultiElectronGroup), 
     ]
 
     TriggerFlags.METSlice.signatures = TriggerFlags.METSlice.signatures() + [
diff --git a/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/MenuComponents.py b/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/MenuComponents.py
index ea35e965b2360d19fb2a2764e77aad7d9ac798f4..aaac56749f9abf18f90284c8155c6c2c8b5e20a1 100644
--- a/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/MenuComponents.py
+++ b/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/MenuComponents.py
@@ -928,7 +928,7 @@ class ChainStep(object):
         log.debug("[ChainStep] onlyJets, sig_set: %s, %s",self.onlyJets, sig_set)
         self.multiplicity = multiplicity
         self.comboHypoCfg=comboHypoCfg
-        self.comboToolConfs=comboToolConfs
+        self.comboToolConfs = list(comboToolConfs)
         self.stepDicts = chainDicts # one dict per leg
         self.isEmpty=(sum(multiplicity)==0 or isEmpty)
         if not self.isEmpty:
diff --git a/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/Physics_pp_run3_v1.py b/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/Physics_pp_run3_v1.py
index 6d11e5fdb824ee4939a0536a89c60c0c08b430e3..3151e490f701b638600e95484e94de9e635f67df 100644
--- a/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/Physics_pp_run3_v1.py
+++ b/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/Physics_pp_run3_v1.py
@@ -231,7 +231,7 @@ def setupMenu():
         #------- Electron+Photon Chains
         # primary e-g chains: electron + photon stay in the same step - these need to be parallel merged!
         ChainProp(name='HLT_e24_lhmedium_g25_medium_02dRAB_L12EM20VH', l1SeedThresholds=['EM20VH','EM20VH'], stream=[PhysicsStream], groups=PrimaryLegGroup+MultiElectronGroup),
-        ChainProp(name='HLT_e24_lhmedium_2g12_loose_02dRAB_L1EM20VH_3EM10VH', l1SeedThresholds=['EM20VH','EM10VH'], stream=[PhysicsStream], groups=PrimaryLegGroup+MultiElectronGroup), # unsure about l1SeedThresholds
+        ChainProp(name='HLT_e24_lhmedium_g12_loose_g12_loose_02dRAB_02dRAC_L1EM20VH_3EM10VH', l1SeedThresholds=['EM20VH','EM10VH','EM10VH'], stream=[PhysicsStream], groups=PrimaryLegGroup+MultiElectronGroup), # unsure about l1SeedThresholds
 
         # Electron LRT chains
         ChainProp(name='HLT_e26_lhloose_nopix_lrttight_L1EM22VHI', groups=PrimaryLegGroup+SingleElectronGroup),
diff --git a/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/SignatureDicts.py b/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/SignatureDicts.py
index 6103c8965b4ac28d18445d1d344bd1307468ac45..51611e330760ae9c86dc6baf44a5141d5af43c81 100644
--- a/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/SignatureDicts.py
+++ b/Trigger/TriggerCommon/TriggerMenuMT/python/HLTMenuConfig/Menu/SignatureDicts.py
@@ -1028,7 +1028,12 @@ UnconventionalTrackingChainParts_Default = {
 #==========================================================
 # Combined Chains
 #==========================================================
-AllowedTopos_comb = ['03dRAB','03dRAB30','02dRAB','50invmAB','60invmAB','afpdijet','18dphiAB','18dphiAC','80mTAC']
+AllowedTopos_comb = [
+    '03dRAB','03dRAB30','02dRAB','02dRAC','50invmAB','60invmAB','afpdijet','18dphiAB','18dphiAC','80mTAC',
+    '1invmAB3','50invmAB130', # Jpsiee, Zee/Zeg
+    '25dphiAA','invmAA80', # Low-mass diphoton
+    '10invmAA70', # Low-mass dimuon
+    ]
 
 # ---- Combined Dictionary of all allowed Values ----
 CombinedChainParts = deepcopy(PhotonChainParts)