Back to home page

sPhenix code displayed by LXR

 
 

    


File indexing completed on 2026-05-23 08:12:14

0001 #pragma once
0002 
0003 #include <fastjet/PseudoJet.hh>
0004 #include <vector>
0005 #include <map>
0006 #include <string>
0007 #include <stdexcept>
0008 #include <algorithm>
0009 #include <cmath>
0010 #include <format>
0011 
0012 R__LOAD_LIBRARY(libVandyClasses.so)
0013 
0014 // ═════════════════════════════════════════════════════════════════════════════
0015 //  Analysis mode
0016 //    kFull — response and pseudo-data from ALL MC events
0017 //    kHalf — response from random half, pseudo-data from other half
0018 //    kData — real data; use fillData.C instead of fillResponse.C
0019 //    kVtx  — vtx distribution pass only; skips all tower-pair loops
0020 // ═════════════════════════════════════════════════════════════════════════════
0021 enum class Mode { kFull, kHalf, kData, kVtx };
0022 
0023 inline std::string ModeLabel(Mode mode)
0024 {
0025     switch (mode) {
0026         case Mode::kFull: return "fullClosure";
0027         case Mode::kHalf: return "halfClosure";
0028         case Mode::kData: return "dataClosure";
0029         case Mode::kVtx:  return "vtx";
0030         default:          return "unknown";
0031     }
0032 }
0033 
0034 // ═════════════════════════════════════════════════════════════════════════════
0035 //  Dijet pT binning — separate reco and truth spaces
0036 //
0037 //  Reco bins span only the reco-accessible region (lead >= recoLeadMin,
0038 //  subl >= recoSublMin). These are the bins RooUnfold unfolds into and the
0039 //  only bins that appear in the measured histogram.
0040 //
0041 //  Truth bins extend below the reco thresholds to capture the smearing region.
0042 //  Events with truth pT in [trueLeadMin, recoLeadMin) or
0043 //  [trueSublMin, recoSublMin) are pure misses in the response matrix —
0044 //  they correctly account for events that smeared above the reco threshold
0045 //  without requiring RooUnfold to invert a region with no reco data.
0046 //
0047 //  The unfolded output spans the full truth bin space but only bins with
0048 //  lead pT >= recoLeadMin and subl pT >= recoSublMin are reported.
0049 // ═════════════════════════════════════════════════════════════════════════════
0050 
0051 // Reco-side bins: only bins accessible at reco level
0052 const std::vector<double> recoLeadPtBins = { 15, 20, 30, 40, 50, 60, 80 };
0053 //const std::vector<double> recoLeadPtBins = { 30, 40, 50, 60, 80 };
0054 const std::vector<double> recoSublPtBins = {  8, 15, 20, 30, 40, 50, 60, 80 };
0055 //const std::vector<double> recoSublPtBins = {  15, 20, 30, 40, 50, 60, 80 };
0056 const int nRecoLead = (int)recoLeadPtBins.size() - 1;  // 6
0057 const int nRecoSubl = (int)recoSublPtBins.size() - 1;  // 7
0058 
0059 // Truth-side bins: extend into the smearing region below reco thresholds
0060 const std::vector<double> trueLeadPtBins = { 15, 20, 30, 40, 50, 60, 80 };
0061 const std::vector<double> trueSublPtBins = {  8, 15, 20, 30, 40, 50, 60, 80 };
0062 const int nTrueLead = (int)trueLeadPtBins.size() - 1;  // 6
0063 const int nTrueSubl = (int)trueSublPtBins.size() - 1;  // 7
0064 
0065 // Legacy aliases used by dijet pT unfolding (reco-side only)
0066 const std::vector<double>& leadPtBins = recoLeadPtBins;
0067 const std::vector<double>& sublPtBins = recoSublPtBins;
0068 const int nLead = nRecoLead;
0069 const int nSubl = nRecoSubl;
0070 
0071 // ═════════════════════════════════════════════════════════════════════════════
0072 //  Kinematically-filtered flat dijet pT index
0073 //
0074 //  A (iL, iS) pair is valid when  sublPtBins[iS] < leadPtBins[iL+1].
0075 //  This allows same-bin combinations like 30-40/30-40 (a 38/32 GeV dijet is
0076 //  perfectly physical) while excluding impossible pairs like 40-50/50-60.
0077 //
0078 //  The index tables below are built immediately from the bin-edge vectors
0079 //  defined above, as inline variables, so they are guaranteed consistent
0080 //  everywhere and require no explicit initialisation call.
0081 //
0082 //  API — sizes:
0083 //    nRecoFlat   number of valid reco (iL,iS) pairs
0084 //    nTrueFlat   number of valid truth (iL,iS) pairs
0085 //    nRecoFlat3D nRecoFlat * nPairWeight
0086 //    nTrueFlat3D nTrueFlat * nPairWeight
0087 //
0088 //  API — 2D index:
0089 //    RecoFlatIndex(iL,iS)  → compressed flat index, or -1 if forbidden
0090 //    TrueFlatIndex(iL,iS)  → compressed flat index, or -1 if forbidden
0091 //    RecoFlatToIJ(f,iL,iS) → recover (iL,iS) from flat index
0092 //    TrueFlatToIJ(f,iL,iS) → recover (iL,iS) from flat index
0093 //    RecoFlatBinCenter(f)  → bin centre for histogram Fill/Fake/Miss
0094 //    TrueFlatBinCenter(f)  → bin centre for histogram Fill/Miss
0095 //
0096 //  API — 3D index (pair weight as innermost dimension, so pair-weight bins
0097 //        for a given (iL,iS) are always contiguous):
0098 //    RecoFlat3DIndex(iL,iS,iPw), TrueFlat3DIndex(iL,iS,iPw)
0099 //    RecoFlat3DToIJK(iF,iL,iS,iPw), TrueFlat3DToIJK(iF,iL,iS,iPw)
0100 //    RecoFlat3DBinCenter(iF), TrueFlat3DBinCenter(iF)
0101 // ═════════════════════════════════════════════════════════════════════════════
0102 
0103 // ── helper structs ────────────────────────────────────────────────────────────
0104 struct DijetPair  { int iL, iS; };
0105 
0106 // ── build valid pair lists (defined as inline to initialise exactly once) ─────
0107 
0108 // Reco valid pairs
0109 inline std::vector<DijetPair> MakeRecoPairs()
0110 {
0111     std::vector<DijetPair> v;
0112     for (int iL = 0; iL < nRecoLead; ++iL)
0113         for (int iS = 0; iS < nRecoSubl; ++iS)
0114             if (recoSublPtBins[iS] < recoLeadPtBins[iL+1])
0115                 v.push_back({iL, iS});
0116     return v;
0117 }
0118 inline std::map<std::pair<int,int>,int> MakeRecoFlatMap(const std::vector<DijetPair>& pairs)
0119 {
0120     std::map<std::pair<int,int>,int> m;
0121     for (int f = 0; f < (int)pairs.size(); ++f)
0122         m[{pairs[f].iL, pairs[f].iS}] = f;
0123     return m;
0124 }
0125 
0126 // Truth valid pairs
0127 inline std::vector<DijetPair> MakeTruePairs()
0128 {
0129     std::vector<DijetPair> v;
0130     for (int iL = 0; iL < nTrueLead; ++iL)
0131         for (int iS = 0; iS < nTrueSubl; ++iS)
0132             if (trueSublPtBins[iS] < trueLeadPtBins[iL+1])
0133                 v.push_back({iL, iS});
0134     return v;
0135 }
0136 inline std::map<std::pair<int,int>,int> MakeTrueFlatMap(const std::vector<DijetPair>& pairs)
0137 {
0138     std::map<std::pair<int,int>,int> m;
0139     for (int f = 0; f < (int)pairs.size(); ++f)
0140         m[{pairs[f].iL, pairs[f].iS}] = f;
0141     return m;
0142 }
0143 
0144 // ── global index tables (initialised once at program start) ───────────────────
0145 inline const std::vector<DijetPair>          gRecoPairs    = MakeRecoPairs();
0146 inline const std::map<std::pair<int,int>,int> gRecoFlatMap = MakeRecoFlatMap(gRecoPairs);
0147 inline const std::vector<DijetPair>          gTruePairs    = MakeTruePairs();
0148 inline const std::map<std::pair<int,int>,int> gTrueFlatMap = MakeTrueFlatMap(gTruePairs);
0149 
0150 // ── size accessors ────────────────────────────────────────────────────────────
0151 // nPairWeight is defined later in the file, so nRecoFlat3D / nTrueFlat3D are
0152 // deferred to after nPairWeight is defined (see below).
0153 inline int nRecoFlat  () { return (int)gRecoPairs.size(); }
0154 inline int nTrueFlat  () { return (int)gTruePairs.size(); }
0155 
0156 // ── 2D index functions ────────────────────────────────────────────────────────
0157 inline int RecoFlatIndex(int iL, int iS)
0158 {
0159     auto it = gRecoFlatMap.find({iL, iS});
0160     return (it != gRecoFlatMap.end()) ? it->second : -1;
0161 }
0162 inline int TrueFlatIndex(int iL, int iS)
0163 {
0164     auto it = gTrueFlatMap.find({iL, iS});
0165     return (it != gTrueFlatMap.end()) ? it->second : -1;
0166 }
0167 inline void RecoFlatToIJ(int f, int& iL, int& iS)
0168     { iL = gRecoPairs[f].iL; iS = gRecoPairs[f].iS; }
0169 inline void TrueFlatToIJ(int f, int& iL, int& iS)
0170     { iL = gTruePairs[f].iL; iS = gTruePairs[f].iS; }
0171 inline double RecoFlatBinCenter(int f) { return f + 0.5; }
0172 inline double TrueFlatBinCenter(int f) { return f + 0.5; }
0173 
0174 // Legacy aliases — dijet pT unfolding uses reco-side only
0175 inline int    FlatIndex    (int iL, int iS) { return RecoFlatIndex(iL, iS); }
0176 inline void   FlatToIJ     (int f, int& iL, int& iS) { RecoFlatToIJ(f, iL, iS); }
0177 inline double FlatBinCenter(int f) { return RecoFlatBinCenter(f); }
0178 // nFlat is used in a few legacy places — provide as a function for consistency
0179 inline int nFlat() { return nRecoFlat(); }
0180 
0181 // ─────────────────────────────────────────────────────────────────────────────
0182 //  ΔΦ binning — 33 edges = 32 bins
0183 // ─────────────────────────────────────────────────────────────────────────────
0184 const std::vector<double> original_dPhiBins = {
0185     0.0000000,  0.041592654,
0186     0.14159265, 0.24159265, 0.34159265, 0.44159265, 0.54159265,
0187     0.64159265, 0.74159265, 0.84159265, 0.94159265, 1.0415927,
0188     1.1415927,  1.2415927,  1.3415927,  1.4415927,  1.5415927,
0189     1.6415927,  1.7415927,  1.8415927,  1.9415927,  2.0415927,
0190     2.1415927,  2.2415927,  2.3415927,  2.4415927,  2.5415927,
0191     2.6415927,  2.7415927,  2.8415927,  2.9415927,  3.0415927,
0192     3.1415927
0193 };
0194 const std::vector<double> new_dPhiBins = {
0195 //    0.0000000, 0.041592654, 0.14159265,
0196     0.0000000, 0.14159265,
0197     0.34159265, 0.54159265, 0.74159265, 0.94159265,
0198     1.2415927, 1.5415927, 1.8415927, 2.1415927,
0199     2.3415927, 2.5415927, 2.7415927, 2.9415927,
0200     3.0415927, 3.1415927
0201 };
0202 
0203 //const std::vector<double> dPhiBins = original_dPhiBins;
0204 const std::vector<double> dPhiBins = new_dPhiBins;
0205 const int nDphi = (int)dPhiBins.size() - 1;  // 32
0206 
0207 
0208 // ─────────────────────────────────────────────────────────────────────────────
0209 //  Pair weight binning — 20 log-uniform bins from 1e-3 to 1
0210 //
0211 //  Pair weight: w_ij = pT_i * pT_j / <pT>^2
0212 //  Log-uniform spacing ensures good resolution across the dynamic range.
0213 //  Edges are computed at runtime to avoid floating-point literal clutter.
0214 // ─────────────────────────────────────────────────────────────────────────────
0215 const int    nPairWeight      = 17;
0216 const double pairWeightMin    = 0.0;    // physical lower edge of histogram
0217 const double pairWeightMax    = 2.0;
0218 const double pairWeightLogMin = 5e-5;   // lower edge of the log-uniform region
0219 
0220 // Pair weight bins:
0221 //   Bin 0  : [0,    1e-3) — linear underflow bin for very small weights
0222 //   Bins 1-20: log-uniform from 1e-3 to 1.0  (20 bins)
0223 //   Bin 21 : [1.0,  2.0) — linear overflow bin
0224 //
0225 // The underflow bin ensures pairs with pairWeight < 1e-3 are not silently
0226 // dropped; they are counted and can be trimmed by the count threshold in
0227 // wEEC_doUnfolding.C if they have insufficient statistics.
0228 inline std::vector<double> MakePairWeightBins()
0229 {
0230     std::vector<double> edges(nPairWeight + 1);
0231     // Bin 0: [0, 1e-3)
0232     edges[0] = 0.0;
0233     // Bins 1-20: log-uniform from 1e-3 to 1.0
0234     const int    nLogBins = nPairWeight-2;
0235     const double logMin   = std::log10(pairWeightLogMin);
0236     const double logMax   = std::log10(0.2);
0237     for (int i = 0; i <= nLogBins; ++i)
0238         edges[i + 1] = std::pow(10.0, logMin + (logMax - logMin) * i / nLogBins);
0239     // Bin 21: [1.0, 2.0)
0240     edges[nPairWeight] = pairWeightMax;
0241     return edges;
0242 }
0243 const std::vector<double> pairWeightBins = MakePairWeightBins();
0244 
0245 // ═════════════════════════════════════════════════════════════════════════════
0246 //  Cross-section weights  (σ_k / N_gen)
0247 //  Update N_gen if your full sample size differs from 9 600 000
0248 // ═════════════════════════════════════════════════════════════════════════════
0249 static const std::map<int,double> xsec = {
0250     {12,  1.4903e+06},
0251     {20,  6.2623e+04},
0252     {30,  2.5298e+03},
0253     {40,  1.3553e+02},
0254     {50,  7.3113e+00},
0255     {60,  3.3261e-01}
0256 };
0257 inline double getSampleWeight(int jetSample)
0258 {
0259     return xsec.at(jetSample) / 9600000.0;
0260 }
0261 
0262 // ═════════════════════════════════════════════════════════════════════════════
0263 //  pT-hat slice cuts  (applied to max truth jet pT)
0264 // ═════════════════════════════════════════════════════════════════════════════
0265 inline float get_jet_pTLow(int sample)
0266 {
0267     switch (sample) {
0268         case  5: return   7;
0269         case 12: return  14;
0270         case 20: return  21;
0271         case 30: return  32;
0272         case 40: return  42;
0273         case 50: return  52;
0274         case 60: return  62;
0275         default: throw std::invalid_argument("Invalid jet sample");
0276     }
0277 }
0278 inline float get_jet_pTHigh(int sample)
0279 {
0280     switch (sample) {
0281         case  5: return  14;
0282         case 12: return  21;
0283         case 20: return  32;
0284         case 30: return  42;
0285         case 40: return  52;
0286         case 50: return  62;
0287         case 60: return 1000;
0288         default: throw std::invalid_argument("Invalid jet sample");
0289     }
0290 }
0291 
0292 // ═════════════════════════════════════════════════════════════════════════════
0293 //  Dijet validity cuts
0294 // ═════════════════════════════════════════════════════════════════════════════
0295 const double recoLeadMin = 20.0;
0296 const double recoSublMin =  8.0;
0297 const double trueLeadMin = 15.0;
0298 const double trueSublMin =  8.0;
0299 
0300 // No asymmetry cut applied — retaining all kinematically valid dijets
0301 // to avoid biasing the pair weight and ΔΦ distributions.
0302 
0303 // ═════════════════════════════════════════════════════════════════════════════
0304 //  Geometric jet matching ΔR cut
0305 // ═════════════════════════════════════════════════════════════════════════════
0306 const double geoMatchDR = 0.3;
0307 
0308 
0309 
0310 // ═════════════════════════════════════════════════════════════════════════════
0311 //  Reco accessibility predicate
0312 //
0313 //  Returns true when the truth (iL, iS) dijet pT bin has at least partial
0314 //  overlap with the reco-accessible region, i.e. when the upper edge of the
0315 //  truth lead bin exceeds recoLeadMin AND the upper edge of the truth subl
0316 //  bin exceeds recoSublMin.
0317 //
0318 //  Bins that fail this test are pure-miss bins at reco level.  The response
0319 //  matrix correctly encodes them as misses, but the unfolded result in these
0320 //  bins is unconstrained by measured data and should be excluded from all
0321 //  projections and reported results.
0322 // ═════════════════════════════════════════════════════════════════════════════
0323 inline bool IsRecoAccessible(int iL, int iS)
0324 {
0325     return trueLeadPtBins[iL + 1] > 30 &&
0326            trueSublPtBins[iS + 1] > 15;
0327 }
0328 
0329 // ═════════════════════════════════════════════════════════════════════════════
0330 //  General bin-finding
0331 // ═════════════════════════════════════════════════════════════════════════════
0332 inline int FindBin(double val, const std::vector<double>& edges)
0333 {
0334     int bin = (int)(std::upper_bound(edges.begin(), edges.end(), val)
0335                     - edges.begin()) - 1;
0336     return (bin >= 0 && bin < (int)edges.size() - 1) ? bin : -1;
0337 }
0338 
0339 // ═════════════════════════════════════════════════════════════════════════════
0340 //  ΔΦ — returns value in [0, π]
0341 // ═════════════════════════════════════════════════════════════════════════════
0342 inline double DeltaPhi(double phi1, double phi2)
0343 {
0344     double dphi = std::abs(phi1 - phi2);
0345     if (dphi > TMath::Pi()) dphi = 2.0 * TMath::Pi() - dphi;
0346     return dphi;
0347 }
0348 
0349 // ═════════════════════════════════════════════════════════════════════════════
0350 //  ΔR
0351 // ═════════════════════════════════════════════════════════════════════════════
0352 inline double DeltaR(double eta1, double phi1, double eta2, double phi2)
0353 {
0354     double deta = eta1 - eta2;
0355     double dphi = DeltaPhi(phi1, phi2);
0356     return std::sqrt(deta*deta + dphi*dphi);
0357 }
0358 
0359 // ═════════════════════════════════════════════════════════════════════════════
0360 //  Dijet geometric matching
0361 // ═════════════════════════════════════════════════════════════════════════════
0362 inline bool GetGeoMatch(
0363     std::vector<JetInfo>& recoJets,
0364     std::vector<JetInfo>& truthJets,
0365     double rLeadPT, double rSublPT,
0366     double tLeadPT, double tSublPT)
0367 {
0368     // Find the specific jets whose pT matches the EventInfo values exactly.
0369     // This avoids ambiguity from sorting — EventInfo already determined which
0370     // jets are lead and subl, so we just locate them in the vector.
0371     const double pTtol = 0.01;  // GeV — tight, since EventInfo stores same value
0372 
0373     JetInfo* rLead = nullptr;
0374     JetInfo* rSubl = nullptr;
0375     JetInfo* tLead = nullptr;
0376     JetInfo* tSubl = nullptr;
0377 
0378     for (auto& j : recoJets) {
0379         if (!rLead && std::abs(j.pt() - rLeadPT) < pTtol) rLead = &j;
0380         else if (!rSubl && std::abs(j.pt() - rSublPT) < pTtol) rSubl = &j;
0381     }
0382     for (auto& j : truthJets) {
0383         if (!tLead && std::abs(j.pt() - tLeadPT) < pTtol) tLead = &j;
0384         else if (!tSubl && std::abs(j.pt() - tSublPT) < pTtol) tSubl = &j;
0385     }
0386 
0387     // If any of the four jets couldn't be located, can't match
0388     if (!rLead || !rSubl || !tLead || !tSubl) return false;
0389 
0390     fastjet::PseudoJet rL(rLead->px(), rLead->py(), rLead->pz(), rLead->e());
0391     fastjet::PseudoJet rS(rSubl->px(), rSubl->py(), rSubl->pz(), rSubl->e());
0392     fastjet::PseudoJet tL(tLead->px(), tLead->py(), tLead->pz(), tLead->e());
0393     fastjet::PseudoJet tS(tSubl->px(), tSubl->py(), tSubl->pz(), tSubl->e());
0394 
0395     return DeltaR(rL.pseudorapidity(), rL.phi_std(),
0396                   tL.pseudorapidity(), tL.phi_std()) < geoMatchDR &&
0397            DeltaR(rS.pseudorapidity(), rS.phi_std(),
0398                   tS.pseudorapidity(), tS.phi_std()) < geoMatchDR;
0399 }
0400 
0401 // ═════════════════════════════════════════════════════════════════════════════
0402 //  Input file path
0403 // ═════════════════════════════════════════════════════════════════════════════
0404 inline std::string SimFilePath(int jetSample, int seg)
0405 {
0406     return std::format(
0407         "/sphenix/tg/tg01/jets/sgross/VandyDSTs/Jet{0}/"
0408         "VandyDSTs_run2pp_ana521_2025p007_v001_Jet{0}-28-{1:06d}_to-{2:06d}.root",
0409         jetSample, seg, seg + 24);
0410 }
0411 
0412 // ═════════════════════════════════════════════════════════════════════════════
0413 //  Calorimeter geometry
0414 //
0415 //  etaEdges / phiEdges: fixed (unshifted) tower boundaries
0416 //  shiftedEtaEdges: vertex-corrected boundaries, updated by shiftEtaEdges()
0417 //
0418 //  Truth towers use the UNSHIFTED grid (vtx=0) so that reco and truth towers
0419 //  share the same (iEta, iPhi) index space for matched-pair filling.
0420 // ═════════════════════════════════════════════════════════════════════════════
0421 const std::vector<double> etaEdges = {
0422     -1.1000000, -1.0083333, -0.91666667, -0.82500000, -0.73333333,
0423     -0.64166667, -0.55000000, -0.45833333, -0.36666667, -0.27500000,
0424     -0.18333333, -0.091666667, 1.1102230e-16, 0.091666667, 0.18333333,
0425      0.27500000,  0.36666667,  0.45833333,  0.55000000,  0.64166667,
0426      0.73333333,  0.82500000,  0.91666667,  1.0083333,   1.1000000 };
0427 
0428 const std::vector<double> phiEdges = {
0429     -0.053619781, 0.044554989, 0.14272976, 0.24090453, 0.33907930,
0430      0.43725407,  0.53542884,  0.63360361, 0.73177838, 0.82995315,
0431      0.92812792,  1.0263027,   1.1244775,  1.2226522,  1.3208270,
0432      1.4190018,   1.5171765,   1.6153513,  1.7135261,  1.8117009,
0433      1.9098756,   2.0080504,   2.1062252,  2.2043999,  2.3025747,
0434      2.4007495,   2.4989242,   2.5970990,  2.6952738,  2.7934486,
0435      2.8916233,   2.9897981,   3.0879729,  3.1861476,  3.2843224,
0436      3.3824972,   3.4806720,   3.5788467,  3.6770215,  3.7751963,
0437      3.8733710,   3.9715458,   4.0697206,  4.1678953,  4.2660701,
0438      4.3642449,   4.4624197,   4.5605944,  4.6587692,  4.7569440,
0439      4.8551187,   4.9532935,   5.0514683,  5.1496431,  5.2478178,
0440      5.3459926,   5.4441674,   5.5423421,  5.6405169,  5.7386917,
0441      5.8368664,   5.9350412,   6.0332160,  6.1313908,  6.2295655 };
0442 
0443 std::vector<double> shiftedEtaEdges;
0444 const double R = 124.5;
0445 
0446 void shiftEtaEdges(double vtx)
0447 {
0448     shiftedEtaEdges.clear();
0449     for (auto edge : etaEdges) {
0450         double z = R * sinh(edge);
0451         shiftedEtaEdges.push_back(asinh((z - vtx) / R));
0452     }
0453 }
0454 
0455 double getEtaCenter(int etaBin, double vtx)
0456 {
0457     if (vtx != 0.0)
0458         return 0.5 * (shiftedEtaEdges[etaBin] + shiftedEtaEdges[etaBin+1]);
0459     return 0.5 * (etaEdges[etaBin] + etaEdges[etaBin+1]);
0460 }
0461 
0462 double getPhiCenter(int phiBin)
0463 {
0464     return 0.5 * (phiEdges[phiBin] + phiEdges[phiBin+1]);
0465 }
0466 
0467 int getEtaBin(double eta, double vtx)
0468 {
0469     const std::vector<double>& edges = (vtx != 0) ? shiftedEtaEdges : etaEdges;
0470     return FindBin(eta, edges);
0471 }
0472 
0473 int getPhiBin(double phi)
0474 {
0475     while (phi < phiEdges.front()) phi += 2 * TMath::Pi();
0476     while (phi > phiEdges.back())  phi -= 2 * TMath::Pi();
0477     return FindBin(phi, phiEdges);
0478 }
0479 
0480 // ═════════════════════════════════════════════════════════════════════════════
0481 //  wEEC flat 2D index helpers
0482 //
0483 //  The (ΔΦ, pair weight) space is stored as a flattened 1D histogram with
0484 //  nFlat2D = nDphi * nPairWeight bins, where:
0485 //    iFlat2D = iDphi * nPairWeight + iPairWeight
0486 //
0487 //  This matches the layout used by the wEEC RooUnfoldResponse matrices.
0488 //  Convert to a proper TH2D only for drawing.
0489 // ═════════════════════════════════════════════════════════════════════════════
0490 const int nFlat2D = nDphi * nPairWeight;  // 32 * 22 = 704
0491 
0492 inline int   Flat2DIndex(int iDphi, int iPw) { return iDphi * nPairWeight + iPw; }
0493 inline void  Flat2DToIJ (int iF2D, int& iDphi, int& iPw)
0494              { iDphi = iF2D / nPairWeight; iPw = iF2D % nPairWeight; }
0495 inline double Flat2DBinCenter(int iF2D) { return iF2D + 0.5; }
0496 
0497 // Convert a flat 1D wEEC histogram to a proper TH2D for plotting.
0498 // The caller owns the returned histogram.
0499 inline TH2D* Flat1DToTH2D(TH1D* h1D, const char* name, const char* title = "")
0500 {
0501     TH2D* h2D = new TH2D(name, title,
0502                           nDphi,       dPhiBins.data(),
0503                           nPairWeight, pairWeightBins.data());
0504     h2D->Sumw2();
0505     for (int iF2D = 0; iF2D < nFlat2D; ++iF2D) {
0506         int iDphi, iPw; Flat2DToIJ(iF2D, iDphi, iPw);
0507         h2D->SetBinContent(iDphi+1, iPw+1, h1D->GetBinContent(iF2D+1));
0508         h2D->SetBinError  (iDphi+1, iPw+1, h1D->GetBinError  (iF2D+1));
0509     }
0510     return h2D;
0511 }
0512 
0513 // ═════════════════════════════════════════════════════════════════════════════
0514 //  wEEC 3D flat index helpers — separate reco and truth spaces
0515 //
0516 //  Layout: pair weight is the innermost dimension, so all nPairWeight bins for
0517 //  a given (iL,iS) are contiguous. This makes projecting over pair weight a
0518 //  simple stride rather than a scattered gather.
0519 //
0520 //    iFlat3D = TrueFlatIndex(iL,iS) * nPairWeight + iPw
0521 //
0522 //  Only kinematically valid (iL,iS) pairs are included (see 2D index above),
0523 //  so every bin in the 3D histogram has a nonzero response matrix row and
0524 //  RooUnfold never receives a zero-prior truth bin.
0525 //
0526 //  Sizes are functions (not constants) because they depend on nPairWeight,
0527 //  which is defined immediately above, and on nTrueFlat()/nRecoFlat().
0528 // ═════════════════════════════════════════════════════════════════════════════
0529 inline int nRecoFlat3D() { return nRecoFlat() * nPairWeight; }
0530 inline int nTrueFlat3D() { return nTrueFlat() * nPairWeight; }
0531 
0532 // Reco-side flat3D — returns -1 for forbidden (iL,iS) combinations
0533 inline int RecoFlat3DIndex(int iL, int iS, int iPw)
0534 {
0535     int f2 = RecoFlatIndex(iL, iS);
0536     return (f2 >= 0) ? f2 * nPairWeight + iPw : -1;
0537 }
0538 inline void RecoFlat3DToIJK(int iF, int& iL, int& iS, int& iPw)
0539 {
0540     int f2 = iF / nPairWeight;
0541     iPw = iF % nPairWeight;
0542     RecoFlatToIJ(f2, iL, iS);
0543 }
0544 inline double RecoFlat3DBinCenter(int iF) { return iF + 0.5; }
0545 
0546 // Truth-side flat3D — returns -1 for forbidden (iL,iS) combinations
0547 inline int TrueFlat3DIndex(int iL, int iS, int iPw)
0548 {
0549     int f2 = TrueFlatIndex(iL, iS);
0550     return (f2 >= 0) ? f2 * nPairWeight + iPw : -1;
0551 }
0552 inline void TrueFlat3DToIJK(int iF, int& iL, int& iS, int& iPw)
0553 {
0554     int f2 = iF / nPairWeight;
0555     iPw = iF % nPairWeight;
0556     TrueFlatToIJ(f2, iL, iS);
0557 }
0558 inline double TrueFlat3DBinCenter(int iF) { return iF + 0.5; }
0559 
0560 // Project a flat 3D truth histogram for one ΔΦ bin down to a single wEEC
0561 // value, summing only over truth (iL,iS) bins that are reco-accessible.
0562 // Bins below the reco threshold have no measured support and are excluded
0563 // so they don't dilute the inclusive projection.
0564 inline void ProjectWEEC3DSlice(TH1D* h1D, double& val, double& err)
0565 {
0566     double sum = 0.0, sumw2 = 0.0;
0567     for (int ft = 0; ft < nTrueFlat(); ++ft) {
0568         int iL, iS; TrueFlatToIJ(ft, iL, iS);
0569         if (!IsRecoAccessible(iL, iS)) continue;
0570         for (int iPw = 0; iPw < nPairWeight; ++iPw) {
0571             int    iF   = ft * nPairWeight + iPw;
0572             double wCen = 0.5 * (pairWeightBins[iPw] + pairWeightBins[iPw+1]);
0573             double v    = h1D->GetBinContent(iF + 1);
0574             double e    = h1D->GetBinError  (iF + 1);
0575             sum   += wCen * v;
0576             sumw2 += wCen * wCen * e * e;
0577         }
0578     }
0579     val = sum;
0580     err = std::sqrt(sumw2);
0581 }
0582 
0583 // Project for a specific (iL, iS) truth dijet pT bin.
0584 // Returns immediately with val=err=0 if the pair is kinematically forbidden.
0585 // Because pair weight is the innermost dimension, the nPairWeight bins for
0586 // this (iL,iS) are contiguous starting at ft*nPairWeight.
0587 inline void ProjectWEEC3DSliceForBin(TH1D* h1D, int iL, int iS,
0588                                       double& val, double& err)
0589 {
0590     val = 0; err = 0;
0591     int ft = TrueFlatIndex(iL, iS);
0592     if (ft < 0) return;  // kinematically forbidden
0593     double sum = 0.0, sumw2 = 0.0;
0594     int iF0 = ft * nPairWeight;
0595     for (int iPw = 0; iPw < nPairWeight; ++iPw) {
0596         double wCen = 0.5 * (pairWeightBins[iPw] + pairWeightBins[iPw+1]);
0597         double v    = h1D->GetBinContent(iF0 + iPw + 1);
0598         double e    = h1D->GetBinError  (iF0 + iPw + 1);
0599         sum   += wCen * v;
0600         sumw2 += wCen * wCen * e * e;
0601     }
0602     val = sum;
0603     err = std::sqrt(sumw2);
0604 }
0605 
0606 // ── Weighted-mean pair weight variants ───────────────────────────────────────
0607 // These replace the bin-center approximation with the luminosity-weighted mean
0608 // pair weight computed from hPWNum (sum of evtWeight*pairWeight) and hPWDen
0609 // (sum of evtWeight) filled in fillResponse.C.  For cells where hPWDen==0
0610 // (no events), the bin-center is used as a fallback.
0611 
0612 inline void ProjectWEEC3DSlice(TH1D* h1D, double& val, double& err,
0613                                 TH1D* hPWNum, TH1D* hPWDen)
0614 {
0615     double sum = 0.0, sumw2 = 0.0;
0616     for (int ft = 0; ft < nTrueFlat(); ++ft) {
0617         int iL, iS; TrueFlatToIJ(ft, iL, iS);
0618         if (!IsRecoAccessible(iL, iS)) continue;
0619         int iF0 = ft * nPairWeight;
0620         for (int iPw = 0; iPw < nPairWeight; ++iPw) {
0621             int    iF   = iF0 + iPw;
0622             double den  = hPWDen ? hPWDen->GetBinContent(iF + 1) : 0.0;
0623             double wMean = (den > 0)
0624                 ? hPWNum->GetBinContent(iF + 1) / den
0625                 : 0.5 * (pairWeightBins[iPw] + pairWeightBins[iPw+1]);
0626             double v    = h1D->GetBinContent(iF + 1);
0627             double e    = h1D->GetBinError  (iF + 1);
0628             sum   += wMean * v;
0629             sumw2 += wMean * wMean * e * e;
0630         }
0631     }
0632     val = sum;
0633     err = std::sqrt(sumw2);
0634 }
0635 
0636 inline void ProjectWEEC3DSliceForBin(TH1D* h1D, int iL, int iS,
0637                                       double& val, double& err,
0638                                       TH1D* hPWNum, TH1D* hPWDen)
0639 {
0640     val = 0; err = 0;
0641     int ft = TrueFlatIndex(iL, iS);
0642     if (ft < 0) return;
0643     double sum = 0.0, sumw2 = 0.0;
0644     int iF0 = ft * nPairWeight;
0645     for (int iPw = 0; iPw < nPairWeight; ++iPw) {
0646         int    iF    = iF0 + iPw;
0647         double den   = hPWDen ? hPWDen->GetBinContent(iF + 1) : 0.0;
0648         double wMean = (den > 0)
0649             ? hPWNum->GetBinContent(iF + 1) / den
0650             : 0.5 * (pairWeightBins[iPw] + pairWeightBins[iPw+1]);
0651         double v    = h1D->GetBinContent(iF + 1);
0652         double e    = h1D->GetBinError  (iF + 1);
0653         sum   += wMean * v;
0654         sumw2 += wMean * wMean * e * e;
0655     }
0656     val = sum;
0657     err = std::sqrt(sumw2);
0658 }
0659 
0660 // ── Covariance-aware projection overloads ────────────────────────────────────
0661 // These use the full TMatrixD covariance from RooUnfold::Errunfold() so that
0662 // off-diagonal correlations between unfolded bins are properly accounted for
0663 // when projecting onto a subset of the flat-3D space.
0664 //
0665 // For a linear combination P = sum_i w_i * x_i over bins in set S:
0666 //   Var(P) = sum_{i,j in S} w_i * w_j * C_{ij}
0667 //
0668 // The covariance matrix is 0-based with size nTrueFlat3D() × nTrueFlat3D().
0669 // Pass nullptr for cov to fall back to naive diagonal (GetBinError) propagation.
0670 
0671 inline void ProjectWEEC3DSlice(TH1D* h1D, double& val, double& err,
0672                                 const TMatrixD* cov)
0673 {
0674     double sum = 0.0, var = 0.0;
0675     // Collect (flat3D index, weight) pairs for all accessible bins
0676     std::vector<std::pair<int,double>> bins;
0677     for (int ft = 0; ft < nTrueFlat(); ++ft) {
0678         int iL, iS; TrueFlatToIJ(ft, iL, iS);
0679         if (!IsRecoAccessible(iL, iS)) continue;
0680         for (int iPw = 0; iPw < nPairWeight; ++iPw) {
0681             int    iF   = ft * nPairWeight + iPw;
0682             double wCen = 0.5 * (pairWeightBins[iPw] + pairWeightBins[iPw+1]);
0683             sum += wCen * h1D->GetBinContent(iF + 1);
0684             bins.push_back({iF, wCen});
0685         }
0686     }
0687     if (cov) {
0688         for (auto [i, wi] : bins)
0689         for (auto [j, wj] : bins)
0690             var += wi * wj * (*cov)(i, j);
0691     } else {
0692         for (auto [i, wi] : bins) {
0693             double e = h1D->GetBinError(i + 1);
0694             var += wi * wi * e * e;
0695         }
0696     }
0697     val = sum;
0698     err = std::sqrt(std::max(var, 0.0));
0699 }
0700 
0701 inline void ProjectWEEC3DSliceForBin(TH1D* h1D, int iL, int iS,
0702                                       double& val, double& err,
0703                                       const TMatrixD* cov)
0704 {
0705     val = 0; err = 0;
0706     int ft = TrueFlatIndex(iL, iS);
0707     if (ft < 0) return;
0708     double sum = 0.0, var = 0.0;
0709     int iF0 = ft * nPairWeight;
0710     for (int iPw = 0; iPw < nPairWeight; ++iPw) {
0711         int    iF   = iF0 + iPw;
0712         double wCen = 0.5 * (pairWeightBins[iPw] + pairWeightBins[iPw+1]);
0713         sum += wCen * h1D->GetBinContent(iF + 1);
0714     }
0715     if (cov) {
0716         for (int iPw = 0; iPw < nPairWeight; ++iPw) {
0717             int    iF = iF0 + iPw;
0718             double wi = 0.5 * (pairWeightBins[iPw] + pairWeightBins[iPw+1]);
0719             for (int jPw = 0; jPw < nPairWeight; ++jPw) {
0720                 int    jF = iF0 + jPw;
0721                 double wj = 0.5 * (pairWeightBins[jPw] + pairWeightBins[jPw+1]);
0722                 var += wi * wj * (*cov)(iF, jF);
0723             }
0724         }
0725     } else {
0726         for (int iPw = 0; iPw < nPairWeight; ++iPw) {
0727             int    iF = iF0 + iPw;
0728             double wCen = 0.5 * (pairWeightBins[iPw] + pairWeightBins[iPw+1]);
0729             double e    = h1D->GetBinError(iF + 1);
0730             var += wCen * wCen * e * e;
0731         }
0732     }
0733     val = sum;
0734     err = std::sqrt(std::max(var, 0.0));
0735 }
0736 
0737 // Covariance-aware variants with weighted mean pair weight
0738 inline void ProjectWEEC3DSlice(TH1D* h1D, double& val, double& err,
0739                                 TH1D* hPWNum, TH1D* hPWDen,
0740                                 const TMatrixD* cov)
0741 {
0742     double sum = 0.0, var = 0.0;
0743     std::vector<std::pair<int,double>> bins;
0744     for (int ft = 0; ft < nTrueFlat(); ++ft) {
0745         int iL, iS; TrueFlatToIJ(ft, iL, iS);
0746         if (!IsRecoAccessible(iL, iS)) continue;
0747         int iF0 = ft * nPairWeight;
0748         for (int iPw = 0; iPw < nPairWeight; ++iPw) {
0749             int    iF  = iF0 + iPw;
0750             double den = hPWDen ? hPWDen->GetBinContent(iF + 1) : 0.0;
0751             double w   = (den > 0)
0752                 ? hPWNum->GetBinContent(iF + 1) / den
0753                 : 0.5 * (pairWeightBins[iPw] + pairWeightBins[iPw+1]);
0754             sum += w * h1D->GetBinContent(iF + 1);
0755             bins.push_back({iF, w});
0756         }
0757     }
0758     if (cov) {
0759         for (auto [i, wi] : bins)
0760         for (auto [j, wj] : bins)
0761             var += wi * wj * (*cov)(i, j);
0762     } else {
0763         for (auto [i, wi] : bins) {
0764             double e = h1D->GetBinError(i + 1);
0765             var += wi * wi * e * e;
0766         }
0767     }
0768     val = sum;
0769     err = std::sqrt(std::max(var, 0.0));
0770 }
0771 
0772 inline void ProjectWEEC3DSliceForBin(TH1D* h1D, int iL, int iS,
0773                                       double& val, double& err,
0774                                       TH1D* hPWNum, TH1D* hPWDen,
0775                                       const TMatrixD* cov)
0776 {
0777     val = 0; err = 0;
0778     int ft = TrueFlatIndex(iL, iS);
0779     if (ft < 0) return;
0780     double sum = 0.0, var = 0.0;
0781     int iF0 = ft * nPairWeight;
0782     // Build per-iPw weights
0783     std::vector<double> ws(nPairWeight);
0784     for (int iPw = 0; iPw < nPairWeight; ++iPw) {
0785         int    iF  = iF0 + iPw;
0786         double den = hPWDen ? hPWDen->GetBinContent(iF + 1) : 0.0;
0787         ws[iPw]    = (den > 0)
0788             ? hPWNum->GetBinContent(iF + 1) / den
0789             : 0.5 * (pairWeightBins[iPw] + pairWeightBins[iPw+1]);
0790         sum += ws[iPw] * h1D->GetBinContent(iF + 1);
0791     }
0792     if (cov) {
0793         for (int iPw = 0; iPw < nPairWeight; ++iPw)
0794         for (int jPw = 0; jPw < nPairWeight; ++jPw)
0795             var += ws[iPw] * ws[jPw] * (*cov)(iF0 + iPw, iF0 + jPw);
0796     } else {
0797         for (int iPw = 0; iPw < nPairWeight; ++iPw) {
0798             double e = h1D->GetBinError(iF0 + iPw + 1);
0799             var += ws[iPw] * ws[iPw] * e * e;
0800         }
0801     }
0802     val = sum;
0803     err = std::sqrt(std::max(var, 0.0));
0804 }
0805 
0806 // ═════════════════════════════════════════════════════════════════════════════
0807 //  wEEC normalization helpers
0808 // ═════════════════════════════════════════════════════════════════════════════
0809 
0810 // Project a flat 1D wEEC histogram onto the ΔΦ axis by summing bin contents
0811 // weighted by the pair weight bin center in each pair weight slice.
0812 // This correctly accounts for the log-uniform pair weight binning — a plain
0813 // sum would integrate w * bin_width rather than w itself.
0814 // Returns a new TH1D owned by the caller.
0815 inline TH1D* ProjectWEEC(TH1D* h1D, const char* name)
0816 {
0817     TH1D* hProj = new TH1D(name, ";#Delta#phi;wEEC",
0818                             nDphi, dPhiBins.data());
0819     hProj->Sumw2();
0820     for (int iDphi = 0; iDphi < nDphi; ++iDphi) {
0821         double sum   = 0.0;
0822         double sumw2 = 0.0;
0823         for (int iPw = 0; iPw < nPairWeight; ++iPw) {
0824             int    iF2D = Flat2DIndex(iDphi, iPw);
0825             double wCen = 0.5 * (pairWeightBins[iPw] + pairWeightBins[iPw+1]);
0826             double val  = h1D->GetBinContent(iF2D + 1);
0827             double err  = h1D->GetBinError  (iF2D + 1);
0828             sum   += wCen * val;
0829             sumw2 += wCen * wCen * err * err;
0830         }
0831         hProj->SetBinContent(iDphi + 1, sum);
0832         hProj->SetBinError  (iDphi + 1, std::sqrt(sumw2));
0833     }
0834     return hProj;
0835 }
0836 
0837 // Normalize a 1D ΔΦ histogram by integral then bin width.
0838 // Used only for the final projected wEEC result.
0839 inline bool NormalizeWEEC(TH1D* h)
0840 {
0841     double integral = h->Integral();
0842     if (integral <= 0) return false;
0843     h->Scale(1.0 / integral);
0844     h->Scale(1.0, "width");
0845     return true;
0846 }
0847 
0848 // ═════════════════════════════════════════════════════════════════════════════
0849 //  Vertex z reweighting
0850 //
0851 //  The vtx_z distribution in data differs from MC.  A per-bin correction is
0852 //  applied when filling the response matrix for data unfolding so that the
0853 //  MC vtx_z shape matches data.
0854 //
0855 //  Binning: 20 bins of 1 cm width covering [-10, 10) cm, matching the vtx_z
0856 //  acceptance cut applied in both fillResponse.C and fillData.C.
0857 //
0858 //  vtxWeights[] is populated by makeVtxWeights.C after it forms the ratio
0859 //  hVtxData / hVtxMC (both normalized to unit area within [-10, 10) cm).
0860 //  Paste the printed array contents from makeVtxWeights.C output here.
0861 //
0862 //  Until makeVtxWeights.C has been run, all weights default to 1.0
0863 //  (no correction applied).
0864 // ═════════════════════════════════════════════════════════════════════════════
0865 constexpr double vtxZMin   = -10.0;
0866 constexpr double vtxZMax   =  10.0;
0867 constexpr int    nVtxBins  =  20;     // 1 cm per bin
0868 constexpr double vtxBinWidth = (vtxZMax - vtxZMin) / nVtxBins;  // 1.0 cm
0869 
0870 // ── vtx weight array ─────────────────────────────────────────────────────────
0871 // Bin i covers [vtxZMin + i*vtxBinWidth, vtxZMin + (i+1)*vtxBinWidth).
0872 // Replace this array with the output of makeVtxWeights.C once available.
0873 constexpr double vtxWeights[nVtxBins] = {
0874     0.962696,  // bin  0: [-10, -9) cm
0875     0.987321,  // bin  1: [-9, -8) cm
0876     1.006085,  // bin  2: [-8, -7) cm
0877     1.020161,  // bin  3: [-7, -6) cm
0878     1.034171,  // bin  4: [-6, -5) cm
0879     1.037306,  // bin  5: [-5, -4) cm
0880     1.046184,  // bin  6: [-4, -3) cm
0881     1.040758,  // bin  7: [-3, -2) cm
0882     1.041977,  // bin  8: [-2, -1) cm
0883     1.042028,  // bin  9: [-1, 0) cm
0884     1.027416,  // bin 10: [0, 1) cm
0885     1.030098,  // bin 11: [1, 2) cm
0886     1.012770,  // bin 12: [2, 3) cm
0887     1.012976,  // bin 13: [3, 4) cm
0888     0.996425,  // bin 14: [4, 5) cm
0889     0.975807,  // bin 15: [5, 6) cm
0890     0.966945,  // bin 16: [6, 7) cm
0891     0.944144,  // bin 17: [7, 8) cm
0892     0.921764,  // bin 18: [8, 9) cm
0893     0.888391,  // bin 19: [9, 10) cm
0894 };
0895 
0896 // Return the vtx reweight factor for a given vtx_z value.
0897 // Returns 1.0 for any vtx_z outside [-10, 10) cm (should not occur after the
0898 // acceptance cut, but guarded for safety).
0899 inline double GetVtxWeight(double vtx_z)
0900 {
0901     if (vtx_z < vtxZMin || vtx_z >= vtxZMax) return 0.0;
0902     int bin = static_cast<int>((vtx_z - vtxZMin) / vtxBinWidth);
0903     if (bin < 0 || bin >= nVtxBins) return 0.0;
0904     return vtxWeights[bin];
0905 }