// $Id$
//
// Copyright (c) 2025, Rhorry Gauld, Alexander Huss, Giovanni Stagnitto
//
// based on initial version by Fabrizio Caola, Radoslaw Grabarczyk,
// Maxwell Hutt, Gavin P. Salam, Ludovic Scyboz, and Jesse Thaler
//
//----------------------------------------------------------------------
// This file is part of FastJet contrib.
//
// It is free software; you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the
// Free Software Foundation; either version 2 of the License, or (at
// your option) any later version.
//
// It is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
// License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this code. If not, see .
//----------------------------------------------------------------------
#include "fastjet/contrib/GHSAlgo.hh"
#include
// to facilitate use with fjcore
#ifndef __FJC_FLAVINFO_USEFJCORE__
#include "fastjet/ClusterSequence.hh"
#include "fastjet/NNH.hh"
#endif
#include
#include
#include "fastjet/contrib/FlavInfo.hh"
FASTJET_BEGIN_NAMESPACE // defined in fastjet/internal/base.hh
namespace contrib{
using namespace std;
const double _deltaR2_handover =
pow(std::numeric_limits::epsilon(), 0.5);
#ifdef VERBOSEDIJ
void print_PJ(ostream * ostr, const PseudoJet &p, unsigned precision,
bool short_version, bool final_flav) {
(*ostr).precision(precision);
(*ostr) << setw(precision + 8) << p.px() << setw(precision + 8) << p.py()
<< setw(precision + 8) << p.pz() << setw(precision + 8) << p.E()
<< setw(precision + 8) << p.pt() << setw(precision + 8) << p.rap()
<< setw(precision + 8) << p.phi() << setw(precision + 8) << p.m()
<< setw(6) << p.user_index();
#ifdef PRINTMASSFLAVINFO
(*ostr) << setw(6) << p.has_user_info();
#else
(*ostr) << setw(6) << "";
#endif
if (p.has_user_info()) {
if (final_flav)
(*ostr) << setw(14) << FlavHistory::current_flavour_of(p).description();
else
(*ostr) << setw(14) << FlavHistory::initial_flavour_of(p).description();
// cout << json(FlavHistory::current_flavour_of(p)) << endl;
}
if (!short_version) {
(*ostr) << setw(6) << p.cluster_hist_index();
}
}
#endif
struct GHSInfo {
double alpha;
double omega;
unsigned njets;
vector jets;
FlavRecombiner flav_recombiner;
};
class GHSBriefJet {
public:
void init(const PseudoJet &jet, GHSInfo *info) {
_jet = jet;
_info = info;
_pt2 = jet.pt2();
double pt = sqrt(_pt2);
_nx = jet.px() / pt;
_ny = jet.py() / pt;
_rap = jet.rap();
_phi = jet.phi();
constexpr double rap_transition = 0.1;
if (fabs(_rap) < rap_transition) {
// for moderately small rapidities switch to special rapidity formula
//
// rap = 1/2 log[(E+pz)/(E-pz)]
// = 1/2 log[1 + 2pz/(E-pz)]
//
// and use log1p for the latter
_rap = 0.5 * log1p(2 * jet.pz() / (jet.E() - jet.pz()));
}
// now, evalute the beam distance for the PseudoJet
if (is_jet()) {
// no beam distance
_diB = numeric_limits::max();
} else if (is_particle()) {
// beam distance is defined only if the particle has no assigned jet
if (associated_jet() == -1) {
double ptBp = 0, ptBm = 0;
for (const auto &j : _info->jets) {
// _info->jets are PJs not BriefJets (as they must be to have an
// associated cs) so we must recalculate the precise rapidity here.
constexpr double rap_transition = 0.1;
double jrap = j.rap();
if (fabs(jrap) < rap_transition) {
jrap = 0.5 * log1p(2 * j.pz() / (j.E() - j.pz()));
}
double jetrap = jet.rap();
if (fabs(jetrap) < rap_transition) {
jetrap = 0.5 * log1p(2 * jet.pz() / (jet.E() - jet.pz()));
}
double dyj = jrap - jetrap;
ptBp += j.pt() * exp(min(0.0, -dyj));
ptBm += j.pt() * exp(min(0.0, dyj));
}
double alpha = _info->alpha;
double diBp = max(pow(jet.pt(), alpha), pow(ptBp, alpha)) *
min(pow(jet.pt(), 2 - alpha), pow(ptBp, 2 - alpha));
double diBm = max(pow(jet.pt(), alpha), pow(ptBm, alpha)) *
min(pow(jet.pt(), 2 - alpha), pow(ptBm, 2 - alpha));
_diB = min(diBp, diBm);
} else {
_diB = numeric_limits::max();
}
} else {
assert(false &&
"the PseudoJet should either be a particle or a jet, "
"but was neither");
}
}
//> idx in jet if associated [0,...,jets.size()]; -1 if unassociated; 0 if
// jet
int associated_jet() const { return abs(_jet.user_index()) - 1; }
bool is_jet() const { return _jet.user_index() == 1; }
bool is_particle() const { return _jet.user_index() <= 0; }
static double geometrical_distance_squared(const GHSBriefJet *first,
const GHSBriefJet *other) {
// do straight rapidity difference, because we already took
// care of making the rapidity as accurate as possible
double delta_y = first->_rap - other->_rap;
double delta_phi = std::fabs(first->_phi - other->_phi);
if (delta_phi > pi) delta_phi = twopi - delta_phi;
// transition is somewhat arbitrary, but should be such that
// we are in a region where arcsin() is unambiguous; can be
// O(1), but must be < pi/2
constexpr double phi_transition = 0.1;
if (delta_phi < phi_transition) {
// take a cross product of the n's (normalised), which
// is simply equal to sin(delta_phi)
double cross = first->_nx * other->_ny - other->_nx * first->_ny;
// the sign can come out negative, but this isn't an issue
// because we will use it in a context where the sign
// disappears
delta_phi = asin(cross);
}
// omega = 0: we use deltaR^2 explicitly
double omega = first->_info->omega;
if (omega == 0.0) {
return delta_y * delta_y + delta_phi * delta_phi;
} else {
double deltaR2 = delta_y * delta_y + delta_phi * delta_phi;
if (deltaR2 > _deltaR2_handover)
return 2 * ((cosh(omega * delta_y) - 1) / (omega * omega) -
(cos(delta_phi) - 1));
else
return deltaR2;
}
}
double distance(const GHSBriefJet *other_in) {
// make sure that if at least one of them is a particle, then
// the first is always a particle
const GHSBriefJet *first = this;
const GHSBriefJet *other = other_in;
if (other->is_particle()) std::swap(first, other);
// this can only happen if both are jets
if (first->is_jet()) return numeric_limits::max();
// return the particle-particle distance (REVISIT FLAVOUR SUM?)
if (other->is_particle()) {
// give a flav dependence (only opposite flavours can annihilate)
FlavInfo flavA = this->_jet.user_info().current_flavour();
FlavInfo flavB =
other_in->_jet.user_info().current_flavour();
int assA = this->associated_jet();
int assB = other_in->associated_jet();
if ((flavA.is_flavourless() || flavB.is_flavourless()) &&
(assA != assB)) {
return numeric_limits::max();
}
FlavInfo flavsum = flavA + flavB;
_info->flav_recombiner.apply_summation_choice(flavsum);
if (!flavA.is_flavourless() && !flavB.is_flavourless() &&
!flavsum.is_flavourless()) {
return numeric_limits::max();
} else {
return dij(first, other);
}
} else {
// ---- other must be a jet -----
// first check to see if this particle is associated with any jet at all
// (if not there is no distance)
if (first->associated_jet() < 0) return numeric_limits::max();
// then see if it's associated with specifically with other
if (_info->jets[first->associated_jet()].cluster_hist_index() ==
other->_jet.cluster_hist_index()) {
return dij(first, other);
} else {
return numeric_limits::max();
}
}
}
double beam_distance() const { return _diB; }
static double dij(const GHSBriefJet *first, const GHSBriefJet *other) {
double alpha = first->_info->alpha;
double ptf = sqrt(first->_pt2);
double pto = sqrt(other->_pt2);
#ifdef VVERBOSE
cout << " i (pt rap phi ui) = " << sqrt(first->_pt2) << " " << first->_rap
<< " " << first->_phi << " " << first->is_jet() << endl
<< " j (pt rap phi ui) = " << sqrt(other->_pt2) << " " << other->_rap
<< " " << other->_phi << " " << other->is_jet() << endl
<< " with distance = "
<< geometrical_distance_squared(first, other) *
max(pow(ptf, alpha), pow(pto, alpha)) *
min(pow(ptf, 2 - alpha), pow(pto, 2 - alpha))
<< endl;
#endif
return geometrical_distance_squared(first, other) *
max(pow(ptf, alpha), pow(pto, alpha)) *
min(pow(ptf, 2 - alpha), pow(pto, 2 - alpha));
}
private:
PseudoJet _jet;
GHSInfo *_info;
double _diB, _pt2, _rap, _phi, _nx, _ny;
};
LimitedWarning ghs_fiducial_warning(10);
std::vector run_GHS(const std::vector &jets_base,
double ptcut, double alpha, double omega,
const FlavRecombiner &flav_recombiner) {
if (jets_base.size() == 0) return jets_base;
#ifdef VERBOSE
cout << " -- apply pt threshold & define particles -- " << endl;
#endif
const ClusterSequence &cs = *(jets_base[0].associated_cs());
vector inputs(cs.jets().begin(),
cs.jets().begin() + cs.n_particles());
// ghs_fiducial_warning.warn("Watch out: in run_GHS a fiducial selector is
// being applied to jets_base (this should be removed; but then run_GHS needs
// to get a list of all particles)");
Selector select_pt = SelectorPtMin(ptcut);
vector jets_hard = select_pt(jets_base);
if (jets_hard.size() == 0) {
return jets_hard;
}
#ifdef VERBOSE
cout << " -- dressing jets -- " << endl;
#endif
vector all;
vector final_jets = jets_hard;
vector jet_flavs(final_jets.size(), 0);
int njets = final_jets.size();
GHSInfo ghs_info;
ghs_info.jets = final_jets;
ghs_info.njets = final_jets.size();
ghs_info.alpha = alpha;
ghs_info.omega = omega;
ghs_info.flav_recombiner = flav_recombiner;
// make sure that jets have no flavour, and give them an index meaning that
// they are a jet
for (auto &j : final_jets) {
j.set_user_info(new FlavHistory(FlavInfo(0)));
// FlavInfo flavj = j.user_info().current_flavour();
// flavj.update_flavourless_attribute();
j.set_user_index(1); //< the signal that it is a jet
all.push_back(j);
}
for (auto &c : inputs) {
//> value <= 0 indicates that we're dealing with a flavour cluster
//> 0: flavour cluster not associated with a jet
//> -i: flavour cluster associated with jet `i`
c.set_user_index(0); //> start with an unassociated cluster
for (unsigned i = 0; i < final_jets.size(); i++) {
if (c.is_inside(final_jets[i])) {
c.set_user_index(-1 -
i); //> need -1 offset since counting starts at 0
break;
}
}
all.push_back(c);
}
if (all.size() == 0) {
return all;
}
#ifdef VERBOSEDIJ
cout << "initial jets + inputs: (0 = cluster, 1 = jet)" << endl;
for (unsigned int i = 0; i < all.size(); i++) {
cout << i << " ";
print_PJ(&cout, all[i], 5, true, true);
cout << endl;
}
#endif
// all.insert(all.end(), inputs.begin(), inputs.end());
// set up nnh
NNH nnh(all, &ghs_info);
int iA, iB;
while (njets >
0) { // the loop does not change njets, but njets > 0 is necessary
// given that the selector could cut off all jets
double dij = nnh.dij_min(iA, iB);
#ifdef VERBOSEDIJ
cout << setprecision(12);
cout << "dij = " << dij << " between " << iA << " and " << iB << endl;
#endif
// LS-2023-02-10: not sure this is very safe...
// if (dij > 0.9*numeric_limits::max()) {
if (dij == numeric_limits::max()) {
break;
}
if (iB >= 0) {
if (iA > iB) std::swap(iA, iB);
// we must never have two jets
assert(iB >= njets && "second entry must be a particle");
if (iA < njets) {
// if the first is a jet, assign B's flavour to A and then remove B
// (note that through the shared pointer, this also affects the
// flavour of the objects in the NNH object -- which is dangerous --
// one should really remove the jet and add it back in)
// FlavInfo * flav_info =
// dynamic_cast(final_jets[iA].user_info_shared_ptr().get());
//*flav_info = *flav_info + all[iB].user_info();
// FlavInfo flavA =
// final_jets[iA].user_info().current_flavour();
FlavInfo flavB = all[iB].user_info().current_flavour();
jet_flavs[iA] = jet_flavs[iA] + flavB;
flav_recombiner.apply_summation_choice(jet_flavs[iA]);
// final_jets[iA].set_user_info(new FlavHistory(flavA + flavB));
nnh.remove_jet(iB);
#ifdef VERBOSE
cout << "adding flavour of " << iB << " to " << iA << ", removing "
<< iB << endl;
#endif
} else {
//> iA & iB are both flavour inputs
// merge
PseudoJet new_pseudojet = all[iA];
new_pseudojet.reset_momentum(
all[iA] + all[iB]); //<- resetting only the momentum keeps the
//> determine the jet association for the merged cluster:
//> can only be associated with a jet if *both* inputs were
// associated with the *same* jet
if (all[iA].user_index() == all[iB].user_index()) {
new_pseudojet.set_user_index(all[iA].user_index());
} else {
new_pseudojet.set_user_index(0);
}
FlavInfo flav = FlavHistory::current_flavour_of(all[iA]) +
FlavHistory::current_flavour_of(all[iB]);
flav_recombiner.apply_summation_choice(flav);
/// set FlavInfo attribute
new_pseudojet.set_user_info(new FlavHistory(flav));
#ifdef VERBOSE
cout << "two flavoured inputs combine." << endl;
cout << "iA = " << iA;
print_PJ(&cout, all[iA], 12, true, true);
cout << endl;
cout << "iB = " << iB;
print_PJ(&cout, all[iB], 12, true, true);
cout << endl;
cout << "into ";
print_PJ(&cout, new_pseudojet, 12, true, true);
cout << endl;
#endif
all.push_back(new_pseudojet);
nnh.merge_jets(iA, iB, new_pseudojet, all.size() - 1);
}
} else {
// assert(iA >= njets && "for beam clustering, iA must be a
// particle");
#ifdef VERBOSE
cout << "removing " << iA << endl;
#endif
nnh.remove_jet(iA);
}
}
for (unsigned i = 0; i < final_jets.size(); i++) {
jet_flavs[i].update_flavourless_attribute();
final_jets[i].set_user_info(new FlavHistory(FlavInfo(jet_flavs[i])));
// restore user index to what it was
final_jets[i].set_user_index(jets_hard[i].user_index());
}
#ifdef VERBOSE
cout << "final dressed jets = " << endl;
for (auto &a : final_jets) {
print_PJ(&cout, a, 5, true, true);
cout << endl;
}
#endif
return final_jets;
}
} // namespace contrib
FASTJET_END_NAMESPACE