Back to home page

sPhenix code displayed by LXR

 
 

    


File indexing completed on 2026-10-16 08:28:08

0001 #include "LaserClusterTruthMatcher.h"
0002 
0003 #include <trackbase/LaserClusterContainer.h>
0004 #include <trackbase/LaserClusterContainerv1.h>
0005 #include <trackbase/LaserCluster.h>
0006 #include <trackbase/LaserClusterv5.h>
0007 #include <trackbase/TpcDefs.h>
0008 #include <trackbase/TrkrDefs.h>
0009 
0010 #include <cdbobjects/CDBTTree.h>
0011 
0012 #include <fun4all/Fun4AllReturnCodes.h>
0013 #include <phool/PHCompositeNode.h>
0014 #include <phool/getClass.h>
0015 
0016 
0017 #include <TCanvas.h>
0018 #include <TColor.h>
0019 #include <TFile.h>
0020 #include <TGraph.h>
0021 #include <TGraph2D.h>
0022 #include <TH1D.h>
0023 #include <TH2D.h>
0024 #include <TLine.h>
0025 #include <TMath.h>
0026 #include <TStyle.h>
0027 #include <TTree.h>
0028 
0029 #include <algorithm>
0030 #include <cmath>
0031 #include <format>
0032 #include <iomanip>
0033 #include <iostream>
0034 #include <numeric>
0035 #include <set>
0036 
0037 // ============================================================================
0038 //  row/stripe finding + reco-to-truth row matching -- unchanged from the
0039 //  standalone macro (assignClustersToTruth.C), just kept file-local here.
0040 //  See that macro for the extensive comments on WHY each piece works the way
0041 //  it does; only the driving code around it (module vs. macro main) changed.
0042 // ============================================================================
0043 
0044 namespace
0045 {
0046   constexpr double kPetalDefault = TMath::Pi() / 9.0;  // 20 degrees
0047 
0048   struct Cluster
0049   {
0050     double R;
0051     double phi;
0052     double adc;
0053     TrkrDefs::cluskey key;  // index into the module's per-side working vector, NOT a tree entry
0054   };
0055 
0056   struct Result
0057   {
0058     std::vector<double> grid;
0059     std::vector<double> density;
0060     std::vector<double> peakR;
0061     std::vector<double> bound;
0062   };
0063 
0064   double layerPitch(double R)
0065   {
0066     if (R < 40.0){ return 0.625; }
0067     if (R < 60.0){ return 1.25; }
0068     return 1.125;
0069   }
0070 
0071   double clusSigma(const Cluster &c, double sigma_ref, double adc_ref)
0072   {
0073     double s = sigma_ref * sqrt(adc_ref / std::max(c.adc, 1.0));
0074     s = std::max(s, 0.5 * layerPitch(c.R));
0075     s = std::min(s, 1.0);
0076     return s;
0077   }
0078 
0079   Result findRows(const std::vector<Cluster> &cl,
0080                           double sigma_ref = 0.15, double promfrac = 0.05,
0081                           double rlo = 28.0, double rhi = 78.0, int ngrid = 4000)
0082   {
0083     Result out;
0084     if (cl.empty()){ return out; }
0085 
0086     std::vector<double> adcs;
0087     adcs.reserve(cl.size());
0088     for (const auto &c : cl){ adcs.push_back(c.adc); }
0089     std::nth_element(adcs.begin(), adcs.begin() + adcs.size() / 2, adcs.end());
0090     const double adc_ref = std::max(adcs[adcs.size() / 2], 1.0);
0091 
0092     const double dr = (rhi - rlo) / ngrid;
0093     out.grid.resize(ngrid);
0094     out.density.assign(ngrid, 0.0);
0095     for (int b = 0; b < ngrid; ++b){ out.grid[b] = rlo + (b + 0.5) * dr; }
0096 
0097     for (const auto &c : cl)
0098     {
0099       if (c.R < rlo || c.R > rhi){ continue; }
0100       const double s = clusSigma(c, sigma_ref, adc_ref);
0101       const int lo = std::max(0, (int)((c.R - 4 * s - rlo) / dr));
0102       const int hi = std::min(ngrid - 1, (int)((c.R + 4 * s - rlo) / dr));
0103       for (int b = lo; b <= hi; ++b)
0104       {
0105         const double d = out.grid[b] - c.R;
0106         out.density[b] += exp(-0.5 * d * d / (s * s)) / s;
0107       }
0108     }
0109 
0110     std::vector<int> pk;
0111     for (int i = 1; i < ngrid - 1; ++i)
0112     {
0113       if (out.density[i] <= out.density[i - 1] || out.density[i] < out.density[i + 1]){ continue; }
0114       double minL = out.density[i];
0115       for (int j = i - 1; j >= 0; --j)
0116       {
0117         if (out.density[j] > out.density[i]){ break; }
0118         minL = std::min(minL, out.density[j]);
0119       }
0120       double minR = out.density[i];
0121       for (int j = i + 1; j < ngrid; ++j)
0122       {
0123         if (out.density[j] > out.density[i]){ break; }
0124         minR = std::min(minR, out.density[j]);
0125       }
0126       if (out.density[i] - std::max(minL, minR) > promfrac * out.density[i]){ pk.push_back(i); }
0127     }
0128 
0129     for (int i : pk){ out.peakR.push_back(out.grid[i]); }
0130 
0131     for (size_t ir = 0; ir + 1 < pk.size(); ++ir)
0132     {
0133       int imin = pk[ir];
0134       for (int b = pk[ir]; b <= pk[ir + 1]; ++b)
0135       {
0136         if (out.density[b] < out.density[imin]){ imin = b; }
0137       }
0138       out.bound.push_back(out.grid[imin]);
0139     }
0140     return out;
0141   }
0142 
0143   int rowOf(double R, const std::vector<double> &bound)
0144   {
0145     return std::upper_bound(bound.begin(), bound.end(), R) - bound.begin();
0146   }
0147 
0148   std::vector<double> refine(const std::vector<Cluster> &cl, const Result &res)
0149   {
0150     const size_t n = res.peakR.size();
0151     std::vector<double> sw(n, 0.0);
0152     std::vector<double> swr(n, 0.0);
0153     for (const auto &c : cl)
0154     {
0155       if (c.R < res.grid.front() || c.R > res.grid.back()){ continue; }
0156       const int ir = rowOf(c.R, res.bound);
0157       sw[ir] += c.adc;
0158       swr[ir] += c.adc * c.R;
0159     }
0160     std::vector<double> R(n, 0.0);
0161     for (size_t i = 0; i < n; ++i){ R[i] = (sw[i] > 0.0) ? swr[i] / sw[i] : res.peakR[i]; }
0162     return R;
0163   }
0164 
0165   // Folds a phi value to be between lo and lo+petal
0166   double foldPetal(double phi, double lo, double petal)
0167   {
0168     double p = fmod(phi - lo, petal);
0169     if (p < 0.0){ p += petal; }
0170     return lo + p;
0171   }
0172 
0173   double circDiff(double a, double b, double petal)
0174   {
0175     double d = a - b;
0176     while (d > 0.5 * petal){ d -= petal; }
0177     while (d < -0.5 * petal){ d += petal; }
0178     return d;
0179   }
0180 
0181   struct Stripes
0182   {
0183     std::vector<double> grid;
0184     std::vector<double> density;
0185     std::vector<double> peakPhi;
0186   };
0187 
0188   std::map<int, TruthRowPattern> buildTruthRowPattern(CDBTTree &cdbttree, int side)
0189   {
0190     std::map<int, TruthRowPattern> truthRows;
0191     for(int row=0; row<32; row++)
0192     {
0193       std::vector<std::pair<int,double>> entries;
0194       for (int iphi = 0; iphi < 12; iphi++)
0195       {
0196         unsigned int truthIndex = (side ? 18 : 0)*10000 + (row*100) + iphi;
0197         double phiVal = cdbttree.GetDoubleValue(truthIndex, "truthPhi");
0198         if (std::isnan(phiVal)){ continue; }
0199         entries.emplace_back(iphi, phiVal);
0200       }
0201       std::sort(entries.begin(), entries.end(), [](const auto &a, const auto &b){ return a.second < b.second; });
0202 
0203       TruthRowPattern truthRow;
0204       for (auto &[iphi, phiVal] : entries)
0205       {
0206         truthRow.stripePhi.push_back(phiVal);
0207         truthRow.stripeIPhi.push_back(iphi);
0208       }
0209       truthRows[row] = truthRow;
0210     }
0211     return truthRows;
0212   }
0213 
0214   // tuning knobs -- identical meaning to the macro's local consts
0215   constexpr double sigR = 0.10;
0216   constexpr double sigGap = 0.15;
0217   constexpr double stripeEdgeFloorFrac = 0.25;
0218   constexpr double mergeFrac = 0.3;  // peaks closer than mergeFrac * row spacing are one feature
0219   constexpr double lamSearchGate = 0.04;
0220   constexpr int lamRowHalfWindow = 3;
0221   constexpr double lamMatchTol = 0.010;
0222   constexpr double lamMatchFrac = 0.7;
0223   constexpr int lamRecoverySearchRows = 8;
0224   constexpr double lamRecoveryMatchTol = 0.005;
0225   constexpr double lamRecoveryFloorFrac = 0.10;
0226   const double phiPetalLo[2] = {-TMath::Pi() / 18.0, 0.0};
0227 
0228   //! Find stripe peaks in one row's folded-phi KDE.
0229   //! Seeds from the tallest intetior peak and walks outward in steps of ~recoDphi
0230   //! to find the rest, then extends past the walk's ends by scanning for any
0231   //! remaining local peaks (isLocalPeak) above a height floor set by the
0232   //! already-found peaks (medPeakHeight * edgeFloorFrac).
0233   Stripes findStripes(const std::vector<Cluster> &clusters, double lo,
0234                       double sigma_ref = 0.004,   // rad, median-ADC cluster
0235                       double sigma_min = 0.0015,  // rad, ~half a pad pitch
0236                       int ngrid = 720, double petal = kPetalDefault,
0237                       double recoDphi = kPetalDefault / 11,
0238                       double edgeFloorFrac = 0.5)
0239   {
0240     Stripes out;
0241     if(clusters.empty()){ return out; }
0242 
0243     std::vector<double> adcs;
0244     adcs.reserve(clusters.size());
0245     for(const auto &c : clusters) { adcs.push_back(c.adc); }
0246     std::nth_element(adcs.begin(), adcs.begin() + adcs.size() / 2, adcs.end());
0247     double adc_ref = std::max(adcs[adcs.size() / 2], 1.0);
0248 
0249     //! set up grid and density for Gaussian KDE
0250     double dp = petal / ngrid;
0251     out.grid.resize(ngrid);
0252     out.density.assign(ngrid, 0.0);
0253     for(int b=0; b<ngrid; b++)
0254     {
0255       out.grid[b] = lo + (b+0.5) * dp;
0256     }
0257 
0258     for(const auto &c : clusters)
0259     {
0260       const double p = foldPetal(c.phi, lo, petal);
0261       //! Set width of Gaussian for KDE.
0262       //! Higher ADC clusters have lower width
0263       //! Bounded by sigma_min and petal/8
0264       double s = sigma_ref * sqrt(adc_ref / std::max(c.adc, 1.0));
0265       s = std::max(s, sigma_min);
0266       s = std::min(s, petal / 8.0);
0267 
0268       //! c0 = grid point closest to cluster phi
0269       const int c0 = (int) ((p - lo) / dp);
0270       //! half -> convert 4 standard deviations into a number of bins
0271       const int half = (int) ceil(4.0 * s / dp);
0272       for(int k=-half; k<=half; k++)
0273       {
0274         const int b = (((c0 + k) % ngrid) + ngrid) % ngrid; // double modulo to wrap around the petal boundary
0275         const double d = circDiff(out.grid[b], p, petal); // signed shortest distance between p and grid point on circle (dealing with wrapping)
0276         out.density[b] += exp(-0.5 * d * d / (s * s)) / s;
0277       }
0278     }
0279 
0280     //! easy accessors for grid coordinates and density values
0281     auto dens = [&](int i){ return out.density[((i % ngrid) + ngrid) % ngrid]; };
0282 
0283     //! seed: tallest peak in the central half of the row (away from the edges)
0284     int maxCand = -1;
0285     double maxCont = 0.0;
0286     for(int i=(int)floor(ngrid/4.0); i<(int)ceil(3.0*ngrid/4.0); i++)
0287     {
0288       double cont = dens(i);
0289       if(cont > maxCont)
0290       {
0291         maxCont = cont;
0292         maxCand = i;
0293       }
0294     }
0295 
0296     if(maxCand < 0){ return out; }
0297     std::vector<int> cand;
0298     cand.push_back(maxCand);
0299 
0300     //! walk outward in steps of ~recoDphi, searching a window around each step
0301     int searchStep = (int) round(recoDphi / dp);
0302     int minSearch = (int) floor(0.8 * recoDphi / dp);
0303     int maxSearch = (int) ceil(1.2 * recoDphi / dp);
0304     int searchWidth = std::max(maxSearch - searchStep, searchStep - minSearch);
0305 
0306     int nLoop = 0;
0307     //! Starting from last peak in phi, search for new peaks at larger phi
0308     //! by moving one recoDphi step and searching in a window around that
0309     while(true)
0310     {
0311       int prevIdx = cand.back();
0312       int j0 = prevIdx + searchStep - searchWidth;
0313       int j1 = std::min(prevIdx + searchStep + searchWidth, ngrid-1);
0314       if(j0 > j1){ break; }
0315 
0316       int localCand = -1;
0317       double localMax = 0.0;
0318       for(int j=j0; j<=j1; j++)
0319       {
0320         double cont = dens(j);
0321         if(cont > localMax)
0322         {
0323           localMax = cont;
0324           localCand = j;
0325         }
0326       }
0327       if(localCand < 0 || nLoop > 20){ break; }
0328       cand.push_back(localCand);
0329       nLoop++;
0330     }
0331 
0332     nLoop = 0;
0333     //! Starting from first peak in phi, search for new peaks at smaller phi
0334     //! by moving one recoDphi step and searching in a window around that
0335     while(true)
0336     {
0337       int prevIdx = cand.front();
0338       int j0 = std::max(prevIdx - searchStep - searchWidth, 0);
0339       int j1 = prevIdx - searchStep + searchWidth;
0340       if(j0 > j1){ break; }
0341 
0342       int localCand = -1;
0343       double localMax = 0.0;
0344       for(int j=j0; j<=j1; j++)
0345       {
0346         double cont = dens(j);
0347         if(cont > localMax)
0348         {
0349           localMax = cont;
0350           localCand = j;
0351         }
0352       }
0353       if(localCand < 0 || nLoop > 20){ break; }
0354       cand.insert(cand.begin(), localCand);
0355       nLoop++;
0356     }
0357 
0358     std::sort(cand.begin(), cand.end());
0359 
0360     //! Check first and last peaks to ensure they don't continue rising on either side (peak split by petal wrapping)
0361     const int checkSamples = 8;  // samples checked on each side to confirm a local peak
0362     auto isEdgeStable = [&](int idx)
0363     {
0364       for (int k = 1; k <= checkSamples; ++k)
0365       {
0366         if (dens(idx - k) > dens(idx - k + 1))
0367         {
0368           return false;
0369         }
0370         if (dens(idx + k) > dens(idx + k - 1))
0371         {
0372           return false;
0373         }
0374       }
0375       return true;
0376     };
0377 
0378     //! Iteratively remove first or last peak if it is rising toward wrap boundary
0379     while (cand.size() > 1 && !isEdgeStable(cand.front())){ cand.erase(cand.begin()); }
0380     while (cand.size() > 1 && !isEdgeStable(cand.back())){ cand.pop_back(); }
0381 
0382     auto isLocalMax = [&](int idx)
0383     {
0384       return dens(idx) >= dens(idx - 1) && dens(idx) >= dens(idx + 1);
0385     };
0386 
0387     double medPeakHeight = 0.0;
0388     if(!cand.empty())
0389     {
0390       std::vector<double> candH;
0391       candH.reserve(cand.size());
0392       for(const auto &c : cand)
0393       {
0394         candH.push_back(dens(c));
0395       }
0396       std::nth_element(candH.begin(), candH.begin() + candH.size() / 2, candH.end());
0397       medPeakHeight = candH[candH.size() / 2];
0398     }
0399 
0400     //! Find the single tallest peak in range [i0, i1]
0401     auto scanForPeak = [&](int i0, int i1) -> int   // inclusive range, i0 <= i1
0402     {
0403       int best = -1;
0404       double bestH = 0.0;
0405       for(int i=i0; i<=i1; i++)
0406       {
0407         if(!isLocalMax(i)){ continue; }
0408         if(dens(i) > bestH)
0409         {
0410           bestH = dens(i);
0411           best = i;
0412         }
0413       }
0414       return best;
0415     };
0416 
0417     //! Recursively scan [i0,i1] for peaks, not just the tallest one.
0418     //! Start with highest peak in range, then split range around that peak and exclude it
0419     //! to recursively identify all peaks above threshold
0420     //! Used to identify lamination sitting next to a taller ordinary stripe in the same
0421     //! region that would would otherwise be missed
0422     auto scanRangeForPeaks = [&](int i0, int i1, bool prepend)
0423     {
0424       std::vector<std::pair<int,int>> ranges = { {i0,i1} };
0425       std::vector<int> found;
0426 
0427       while(!ranges.empty())
0428       {
0429         auto [a, b] = ranges.back();
0430         ranges.pop_back();
0431         if(a > b){ continue; }
0432 
0433         int best = scanForPeak(a, b);
0434         if(best < 0 || dens(best) <= edgeFloorFrac * medPeakHeight){ continue; }
0435 
0436         found.emplace_back(best);
0437         ranges.emplace_back(a, best - 1);
0438         ranges.emplace_back(best + 1, b);
0439       }
0440 
0441       std::sort(found.begin(), found.end());
0442       if(prepend){ cand.insert(cand.begin(), found.begin(), found.end()); }
0443       else{ cand.insert(cand.end(), found.begin(), found.end()); }
0444     };
0445 
0446     //! Search between first grid point and first peak and the last peak and last grid point
0447     //! for any additional peaks that may have been missed (searching for laminations).
0448     if(!cand.empty())
0449     {
0450       if(cand.front() > 0){ scanRangeForPeaks(0, cand.front() - 1, true); }
0451       if(cand.back() < ngrid - 1){ scanRangeForPeaks(cand.back() + 1, ngrid - 1, false); }
0452     }
0453 
0454     //! Backstop: collapse any two candidates sitting closer together than mergeFrac of this
0455     //! row's stripe spacing into one, keeping the taller. Guards against a single physical
0456     //! stripe getting split into two nearby candidates (KDE under-smoothing, or a shoulder that
0457     //! independently clears the isLocalMax/floor tests above) regardless of what produced it --
0458     //! including a lamination sitting right at the petal wrap seam, whose two flanks land near
0459     //! index 0 and index ngrid-1: isEdgeStable above only trims the walk-found candidates, and
0460     //! scanRangeForPeaks can then re-add the other flank as a seemingly independent "new" peak
0461     //! near phi~0 while recovering missed laminations further in. So this pass treats cand as
0462     //! circular: it merges adjacent-in-index pairs first, then checks the wrap gap from the
0463     //! last candidate back around to the first and merges that too if it's equally close.
0464     if (recoDphi > 0.0 && cand.size() > 1)
0465     {
0466       const int mergeGap = std::max(1, (int) std::round(mergeFrac * recoDphi / dp));
0467 
0468       bool mergedAny = true;
0469       while (mergedAny && cand.size() > 1)
0470       {
0471         mergedAny = false;
0472 
0473         std::vector<int> merged;
0474         merged.push_back(cand.front());
0475         for (size_t ci = 1; ci < cand.size(); ++ci)
0476         {
0477           if (cand[ci] - merged.back() < mergeGap)
0478           {
0479             if (dens(cand[ci]) > dens(merged.back())){ merged.back() = cand[ci]; }
0480             mergedAny = true;
0481           }
0482           else
0483           {
0484             merged.push_back(cand[ci]);
0485           }
0486         }
0487 
0488         //! Circular wrap: gap from the last candidate forward (mod ngrid) to the first.
0489         if (merged.size() > 1)
0490         {
0491           const int wrapGap = (merged.front() + ngrid) - merged.back();
0492           if (wrapGap < mergeGap)
0493           {
0494             if (dens(merged.front()) > dens(merged.back())){ merged.erase(merged.end() - 1); }
0495             else{ merged.erase(merged.begin()); }
0496             mergedAny = true;
0497           }
0498         }
0499 
0500         cand = std::move(merged);
0501       }
0502     }
0503 
0504 
0505     for(int i : cand)
0506     {
0507       out.peakPhi.push_back(out.grid[i]);
0508     }
0509     std::sort(out.peakPhi.begin(), out.peakPhi.end());
0510     return out;
0511   } // end findStripes
0512 
0513   //! Find the peak within searchGate of "edge" whose position is most stable across a window
0514   //! of neighbouring rows -- matches some peak in at least matchFrac of the rows within matchTol.
0515   //! A lamination sits at a near-fixed phi, so it's stable across many rows; a real stripe's
0516   //! position shifts for each row, so a single neighbour can spuriously match but a wide
0517   //! window won't.
0518   int findLaminationNear(const std::vector<std::vector<double>> &peaksPerRow, int rowIdx, double edge, double searchGate,
0519                           int rowHalfWindow, double matchTol, double matchFrac, double petal)
0520   {
0521     const auto &peaks = peaksPerRow[rowIdx];
0522     const int nrow = (int) peaksPerRow.size();
0523 
0524     int bestIdx = -1;
0525     int bestMatches = -1;
0526     for(size_t idx=0; idx<peaks.size(); idx++)
0527     {
0528       if(fabs(circDiff(peaks[idx], edge, petal)) > searchGate){ continue; }
0529 
0530       int nMatched = 0;
0531       int nChecked = 0;
0532       for(int j=std::max(0, rowIdx - rowHalfWindow);
0533           j<=std::min(nrow - 1, rowIdx + rowHalfWindow);
0534           j++)
0535       {
0536         if(j == rowIdx || peaksPerRow[j].empty()){ continue; }
0537         nChecked++;
0538         double best = 1e9;
0539         for(double q : peaksPerRow[j])
0540         {
0541           best = std::min(best, fabs(circDiff(peaks[idx], q, petal)));
0542         }
0543         if(best < matchTol){ nMatched++; }
0544       }
0545       if(nChecked == 0){ continue; }
0546       if((double) nMatched / nChecked >= matchFrac && nMatched > bestMatches)
0547       {
0548         bestMatches = nMatched;
0549         bestIdx = (int) idx;
0550       }
0551     }
0552     return bestIdx;
0553   }       
0554   
0555   //! Snapshot of what findLaminationNear already confirmed for one edge, used
0556   //! as prediction sources for the recovery pass below. Captured BEFORE
0557   //! recovery runs and never updated during it, so recovery for one row can
0558   //! never depend on another row's recovered (rather than independently
0559   //! confirmed) result -- keeps recovery non-cascading, so uncertainty can't
0560   //! compound across rows.
0561   struct LamEdgeSnapshot
0562   {
0563     std::vector<bool> confirmed;
0564     std::vector<double> pos;  // valid only where confirmed is true
0565   };
0566 
0567   LamEdgeSnapshot snapshotLamEdge(const std::vector<std::vector<double>> &peaksPerRow, const std::vector<int> &edgeIdx)
0568   {
0569     LamEdgeSnapshot snap;
0570     int nrow = (int) peaksPerRow.size();
0571     snap.confirmed.assign(nrow, false);
0572     snap.pos.assign(nrow, 0.0);
0573     for(int i=0; i<nrow; i++)
0574     {
0575       if(edgeIdx[i] < 0){ continue; }
0576       snap.confirmed[i] = true;
0577       snap.pos[i] = peaksPerRow[i][edgeIdx[i]];
0578     }
0579     return snap;
0580   }
0581 
0582   //! Predict row i's lamination position for one edge from the nearest ORIGINALLY-confirmed
0583   //! neighbouring rows (within maxSearchRows on each side): linear interpolation if confirmed
0584   //! rows exist on both sides, extrapolation (nearest value) if only one side has data.
0585   std::pair<double, bool> predictLamPos(const LamEdgeSnapshot &snap, int row, int maxSearchRows, double petal)
0586   {
0587     int nrow = (int) snap.confirmed.size();
0588     int below = -1;
0589     int above = -1;
0590     for(int j=row-1; j>=std::max(0, row-maxSearchRows); j--)
0591     {
0592       if(snap.confirmed[j])
0593       {
0594         below = j;
0595         break;
0596       }
0597     }
0598     for(int j=row+1; j<=std::min(nrow-1, row+maxSearchRows); j++)
0599     {
0600       if(snap.confirmed[j])
0601       {
0602         above = j;
0603         break;
0604       }
0605     }
0606 
0607     if(below >= 0 && above >= 0)
0608     {
0609       double frac = double(row - below) / (double)(above - below);
0610       return { snap.pos[below] + frac * circDiff(snap.pos[above], snap.pos[below], petal), true};
0611     }
0612     if(below >= 0){ return {snap.pos[below], true}; }
0613     if(above >= 0){ return {snap.pos[above], true}; }
0614     return {0.0, false};
0615   }
0616 
0617   //! median density at this row's oen existing peak positions -- used as a 
0618   //! row-local scale for the recovery floor below.
0619   double medianPeakDensity(const Stripes &stripeRow, const std::vector<double> &peaks)
0620   {
0621     if(peaks.empty() || stripeRow.grid.size() < 2){ return 0.0; }
0622     const double dp = stripeRow.grid[1] - stripeRow.grid[0];
0623     std::vector<double> h;
0624     for(double p : peaks)
0625     {
0626       int idx = (int) std::round((p - stripeRow.grid[0]) / dp);
0627       idx = std::max(0, std::min((int) stripeRow.grid.size() - 1, idx));
0628       h.push_back(stripeRow.density[idx]);
0629     }
0630     std::nth_element(h.begin(), h.begin() + h.size() / 2, h.end());
0631     return h[h.size() / 2];
0632   }
0633 
0634   //! Recover a lamination for one row/edge that fineLaminationNear missed, using a position predicted from
0635   //! independently-confirmed neighbouring rows.
0636   //! (a) checks whether an EXISTING peak is already close to the prediction -- recovers a peak that WAS found
0637   //! but failed the cross-row stability test.
0638   //! (b) if no existing peak is close, scans the row's own raw LDE near the prediction for the tallest bin and,
0639   //! if it clears absFloor, inserts it as a NEW peak -- recovers a lamination that was fully abosorbed into
0640   //! a taller neighbour and never became a distinct peak
0641   bool recoverLamination(std::vector<double> &peaks, std::vector<bool> &isLam, const Stripes &stripeRow,
0642                           double predicted, double matchTol, double petal, double absFloor, double recoDphi)
0643   {
0644     for(size_t k=0; k<peaks.size(); k++)
0645     {
0646       if(fabs(circDiff(peaks[k], predicted, petal)) < matchTol)
0647       {
0648         isLam[k] = true;
0649         return true;
0650       }
0651     }
0652 
0653     if(stripeRow.grid.size() < 2){ return false; }
0654     const double dp = stripeRow.grid[1] - stripeRow.grid[0];
0655     const int half = std::max(1, (int) (matchTol / dp));
0656     const int ngrid = (int) stripeRow.grid.size();
0657 
0658     int c0 = -1;
0659     double bestD = 1e300;
0660     for(int b=0; b<ngrid; b++)
0661     {
0662       double d = fabs(circDiff(stripeRow.grid[b], predicted, petal));
0663       if(d < bestD)
0664       {
0665         bestD = d;
0666         c0 = b;
0667       }
0668     }
0669     if(c0 < 0){ return false; }
0670 
0671     int bestBin = -1;
0672     double bestH = 0.0;
0673     for(int k=-half; k<=half; k++)
0674     {
0675       int b = ((c0 + k) % ngrid + ngrid) % ngrid;
0676       if(stripeRow.density[b] > bestH)
0677       {
0678         bestH = stripeRow.density[b];
0679         bestBin = b;
0680       }
0681     }
0682     if(bestBin < 0 || bestH < absFloor){ return false; }
0683 
0684     double newPeakPos = stripeRow.grid[bestBin];
0685 
0686     //! The rescan found the lamination at the cross-row-consistent position, but this row can
0687     //! also have a separate secondary peak just next to it (e.g. a distortion-shifted shoulder of
0688     //! the lamination) that findStripes kept as an ordinary stripe. That peak is part of the same
0689     //! physical feature, so merge it INTO the lamination: drop any existing peak within mergeFrac
0690     //! of this row's stripe spacing of the recovered lamination, then insert the lamination at the
0691     //! rescan position. Clusters that were nearest the dropped peak will then fall to the lamination.
0692     if (recoDphi > 0.0)
0693     {
0694       const double mergeTol = mergeFrac * recoDphi;
0695       for(size_t k=peaks.size(); k-- > 0;)
0696       {
0697         //! never absorb a peak already flagged as the other edge's lamination (the two edges
0698         //! are circularly adjacent across the wrap)
0699         if(!isLam[k] && fabs(circDiff(peaks[k], newPeakPos, petal)) < mergeTol)
0700         {
0701           peaks.erase(peaks.begin() + k);
0702           isLam.erase(isLam.begin() + k);
0703         }
0704       }
0705     }
0706 
0707     size_t insertAt = std::lower_bound(peaks.begin(), peaks.end(), newPeakPos) - peaks.begin();
0708 
0709     peaks.insert(peaks.begin() + insertAt, newPeakPos);
0710     isLam.insert(isLam.begin() + insertAt, true);
0711     return true;
0712   }
0713   
0714   struct Cost
0715   {
0716     double nMismatch{1e300};
0717     double gapChi2{1e300};
0718     double rChi2{1e300};
0719   };
0720 
0721   //! lexicographic ordering: stripe-count mismatch first, then gap chi2, then R chi2
0722   bool operator<(const Cost &a, const Cost &b)
0723   {
0724     if (a.nMismatch != b.nMismatch){ return a.nMismatch < b.nMismatch; }
0725     if (a.gapChi2 != b.gapChi2){ return a.gapChi2 < b.gapChi2; }
0726     return a.rChi2 < b.rChi2;
0727   }
0728 
0729   Cost operator+(const Cost &a, const Cost &b)
0730   {
0731     return {a.nMismatch + b.nMismatch, a.gapChi2 + b.gapChi2, a.rChi2 + b.rChi2};
0732   }
0733 
0734 
0735   const Cost INF = {1e300, 1e300, 1e300};
0736 
0737   struct MatchResult
0738   {
0739     std::vector<int> truthOf;
0740     std::vector<int> skipped;
0741     Cost dp{1e300, 1e300, 1e300};
0742   };
0743 
0744   Cost pairCost(double recoR, int recoN, const TruthRow &t, double sigmaR)
0745   {
0746     Cost c{0.0, 0.0, 0.0};
0747     if(recoN > 0 && t.nstripes > 0)
0748     {
0749       double d = recoN - t.nstripes;
0750       c.nMismatch = d * d;
0751     }
0752     double dR = recoR - t.R;
0753     c.rChi2 = dR * dR / (sigmaR * sigmaR);
0754     return c;
0755   }
0756 
0757   //! Cost of the reco row-to-row R gap disagreeing with the corresponding truth gap. Uses actual truth R centers
0758   //! so a skip spanning a module boundary is judged against the true, larger gap rather than needing
0759   //! special-case handling.
0760   inline double gapCost(double recoR_prev, double recoR_curr, double truthR_prev, double truthR_curr, double sigmaGap)
0761   {
0762     double recoGap = recoR_curr - recoR_prev;
0763     double truthGap = truthR_curr - truthR_prev;
0764     double d = recoGap - truthGap;
0765     return d * d / (sigmaGap * sigmaGap);
0766   }
0767 
0768   //! dp[i][j]: best Cost matching reco rows 0..i, with row i matched to truth row j exactly (needed so gapCost knows
0769   //! the previous match's truth row)
0770   void matchDP(const std::vector<double> &recoR, const std::vector<int> &recoN, const std::vector<TruthRow> &truth, double sigmaR, double sigmaGap, MatchResult &best, int verbosity)
0771   {
0772     const int nr = recoR.size();
0773     const int nt = truth.size();
0774     best = MatchResult();
0775     if(nr == 0 || nt == 0 || nr > nt)
0776     {
0777       if(verbosity){ std::cout << "rowmatch: " << nr << " reco rows vs " << nt << " truth rows -- cannot align" << std::endl; }
0778       return;
0779     }
0780 
0781     std::vector<std::vector<Cost>> dp(nr, std::vector<Cost>(nt, INF));
0782     std::vector<std::vector<int>> prevJ(nr, std::vector<int>(nt, -1));
0783 
0784     for(int j=0; j<nt; j++)
0785     {
0786       dp[0][j] = pairCost(recoR[0], recoN[0], truth[j], sigmaR);  // no gap cost for the first match
0787     }
0788 
0789     for(int i=1; i<nr; i++)
0790     {
0791       for(int j=i; j<nt; j++)
0792       {
0793         Cost matchP = pairCost(recoR[i], recoN[i], truth[j], sigmaR);
0794         for(int jp = i - 1; jp < j; jp++)
0795         {
0796           if(!(dp[i-1][jp] < INF)){ continue; }
0797           double gc = gapCost(recoR[i-1], recoR[i], truth[jp].R, truth[j].R, sigmaGap);
0798           Cost cand = dp[i-1][jp] + Cost{matchP.nMismatch, gc, matchP.rChi2};
0799           if(cand < dp[i][j])
0800           {
0801             dp[i][j] = cand;
0802             prevJ[i][j] = jp;
0803           }
0804         }
0805       }
0806     }
0807 
0808     int bestJ = -1;
0809     Cost bestCost = INF;
0810     for(int j=nr-1; j<nt; j++)
0811     {
0812       if(dp[nr-1][j] < bestCost)
0813       {
0814         bestCost = dp[nr-1][j];
0815         bestJ = j;
0816       }
0817     }
0818 
0819     if(bestJ < 0){ return; }
0820 
0821     std::vector<int> truthOf(nr);
0822     int i = nr - 1;
0823     int j = bestJ;
0824     while(i>=0)
0825     {
0826       truthOf[i] = j;
0827       int pj = (i > 0 ? prevJ[i][j] : -1);
0828       i--;
0829       j = pj;
0830     }
0831 
0832     std::vector<bool> used(nt, false);
0833     for(int t : truthOf){ used[t] = true; }
0834     std::vector<int> skipped;
0835     for(int t=0; t<nt; t++)
0836     {
0837       if(!used[t]){ skipped.push_back(t); }
0838     }
0839 
0840     best = {truthOf, skipped, bestCost};
0841     return;
0842   }
0843 
0844   //! Rebase a row's real (non-lamination) peaks so the lamination sits at `lo`, wrap into [lo, lo+petal), then match against
0845   //! truthRowPattern's pattern if the count and spacing agree. Returns one entry per input peak (lamination and unmatched
0846   //! entries get -1); origIdx tracks which output slot each repased peak came from.
0847   std::vector<int> alignPhiPeaksToTruth(const std::vector<double> &peaks, const std::vector<bool> &isLam, double dPhi, double lo, double petal, const TruthRowPattern &truthRowPattern)
0848   {
0849     std::vector<double> peaksToUse;
0850     std::vector<int> origIdx;
0851     double lamPhi = 0.0;
0852     bool haveLam = false;
0853     for(size_t i=0; i<isLam.size(); i++)
0854     {
0855       if(isLam[i])
0856       {
0857         lamPhi = peaks[i];
0858         haveLam = true;
0859         continue;
0860       }
0861       peaksToUse.push_back(peaks[i]);
0862       origIdx.push_back((int) i);
0863     }
0864 
0865     std::vector<int> iphiAssignment(peaks.size(), -1);
0866     if(!haveLam || peaksToUse.empty()){ return iphiAssignment; }
0867 
0868     for(auto &p : peaksToUse)
0869     {
0870       p = p - lamPhi + lo;
0871       while(p < lo){ p += petal; }
0872       while(p >= lo + petal){ p -= petal; }
0873     }
0874 
0875     std::vector<std::size_t> phiSortIdx(peaksToUse.size());
0876     std::iota(phiSortIdx.begin(), phiSortIdx.end(), 0);
0877 
0878     std::sort(phiSortIdx.begin(), phiSortIdx.end(), [&](std::size_t a, std::size_t b){ return peaksToUse[a] < peaksToUse[b]; });
0879 
0880     if(peaksToUse.size() == truthRowPattern.stripePhi.size())
0881     {
0882       bool allGood = true;
0883       for(size_t p = 0; p+1 < peaksToUse.size(); p++)
0884       {
0885         double gap = peaksToUse[phiSortIdx[p+1]] - peaksToUse[phiSortIdx[p]];
0886         if(gap > 1.2 * dPhi || gap < 0.8 * dPhi)
0887         {
0888           allGood = false;
0889           break;
0890         }
0891       }
0892       if(allGood)
0893       {
0894         for(size_t p = 0; p < peaksToUse.size(); p++)
0895         {
0896           iphiAssignment[origIdx[phiSortIdx[p]]] = truthRowPattern.stripeIPhi[p];
0897         }
0898       }
0899     }
0900     return iphiAssignment;
0901   }
0902   
0903   struct PetalCand
0904   {
0905     double dPhi;
0906     int petal;    
0907   };
0908 
0909   int nearestPeak(double foldedPhi, const std::vector<double> &peaks, double petal)
0910   {
0911     int best = 0;
0912     double bestD = 1e300;
0913     for(size_t k = 0; k<peaks.size(); k++)
0914     {
0915       double d = fabs(circDiff(foldedPhi, peaks[k], petal));
0916       if(d < bestD)
0917       {
0918         bestD = d;
0919         best = (int) k;
0920       }
0921     }
0922     return best;
0923   }
0924 
0925   unsigned int encodeTruthIndex(int side, int petalIdx, int row, int stripeIdx)
0926   {
0927     int pp = (side == 0) ? petalIdx : petalIdx + 18;
0928     return (unsigned int) (pp*10000 + row*100 + stripeIdx);
0929   }
0930 
0931   std::map<TrkrDefs::cluskey, std::pair<int,bool>> matchSide(const std::vector<Cluster>& clusters, CDBTTree &cdbttree, const std::map<int, TruthRowPattern> &truthRowPatterns, const std::vector<TruthRow> &truth, int side, std::string QABase, int verbosity)
0932   {
0933     const char *sname = side ? "North" : "South";
0934     const double lo = phiPetalLo[side];
0935     const double petal = kPetalDefault;
0936 
0937     std::map<TrkrDefs::cluskey, std::pair<int,bool>> keyMatch;
0938     for(const auto &c : clusters)
0939     {
0940       keyMatch[c.key] = {-999,false};
0941     }
0942 
0943     if (clusters.empty())
0944     {
0945       std::cout << "LaserClusterTruthMatcher - matchSide: no clusters found for side " << sname << std::endl;
0946       return keyMatch;
0947     }
0948 
0949     // ---- rows in R ----
0950     Result rowResult = findRows(clusters, 0.15, 0.05);
0951     std::vector<double> rowR = refine(clusters, rowResult);
0952     const int nrow = (int)rowR.size();
0953     if (nrow == 0)
0954     {
0955       std::cout << "LaserClusterTruthMatcher: found 0 rows for side " << sname << std::endl;
0956       return keyMatch;
0957     }
0958 
0959     // ---- assign clusters to rows ----
0960     std::vector<std::vector<Cluster>> rowClus(nrow);
0961     for (const auto &c : clusters)
0962     {
0963       rowClus[rowOf(c.R, rowResult.bound)].push_back(c);
0964     }
0965 
0966     if(!QABase.empty())
0967     {
0968       const auto &palette = TColor::GetPalette();
0969       const Int_t nColors = palette.GetSize();
0970 
0971       std::vector<TH1D *> hRADCrow(nrow);
0972       TCanvas *c1 = new TCanvas();
0973       gStyle->SetOptStat(0);
0974       TH1D *hRADC = new TH1D(std::format("hRADC_{}", sname).c_str(),
0975                         std::format("R of {} ADC-Weighted Aggregated Clusters;R [cm]",
0976                                     sname).c_str(),
0977                         400, 28, 78);      
0978       for(int i=0; i<nrow; i++)
0979       {
0980         hRADCrow[i] = new TH1D(std::format("hRADC_{}_row{}", sname, i).c_str(),
0981                         std::format("R of {} ADC-Weighted Aggregated Clusters;R [cm]",
0982                                     sname).c_str(),
0983                         400, 28, 78);
0984         const Int_t code = palette.At(i * (nColors / std::max(nrow, 1)));
0985         hRADCrow[i]->SetLineColor(code);
0986         hRADCrow[i]->SetFillColor(code);
0987 
0988         for(const auto& c : rowClus[i])
0989         {
0990           hRADCrow[i]->Fill(c.R, c.adc);
0991         }
0992 
0993         hRADC->Add(hRADCrow[i]);
0994       }
0995 
0996       hRADC->Draw();
0997       for(int i=0; i<nrow; i++){ hRADCrow[i]->Draw("HIST SAME"); }
0998 
0999 
1000       for (double b : rowResult.bound)
1001       {
1002         TLine *l = new TLine(b, 0, b, hRADC->GetMaximum());
1003         l->SetBit(kCanDelete);
1004         l->SetLineColor(kRed);
1005         l->Draw("same");
1006       }
1007       c1->SaveAs(std::format("{}_RPeaks_{}.pdf",QABase, sname).c_str());
1008 
1009       delete hRADC;
1010       for(auto *hr : hRADCrow){ delete hr; }
1011       delete c1;
1012     }
1013 
1014     // ---- pass 1: stripe spacing per row from the neighbour-gap spectrum ----
1015 
1016     std::vector<TH1D *> hDPhi(nrow);
1017     std::vector<double> recoDphi(nrow, -1.0);
1018 
1019     for(int i=0; i<nrow; i++)
1020     {
1021       hDPhi[i] = new TH1D(std::format("hDPhi_{}_row{}",sname,i).c_str(),
1022                           std::format("#Delta#phi, {} row {};#Delta#phi [rad]",sname, i).c_str(),
1023                           1000, 0, petal);
1024 
1025       std::sort(rowClus[i].begin(), rowClus[i].end(),
1026                 [](const Cluster &a, const Cluster &b){ return a.phi < b.phi; });
1027       
1028       for(int j=0; j<(int)rowClus[i].size(); j++)
1029       {
1030         const double dPhi = (j == (int) rowClus[i].size() - 1)
1031           ? rowClus[i][0].phi + 2*TMath::Pi() - rowClus[i][j].phi // wrap around in 2pi
1032           : rowClus[i][j+1].phi - rowClus[i][j].phi;
1033         hDPhi[i]->Fill(dPhi);
1034       }
1035       if(hDPhi[i]->GetEntries() > 20)
1036       {
1037         recoDphi[i] = hDPhi[i]->GetBinCenter(hDPhi[i]->GetMaximumBin());
1038       }
1039     }
1040 
1041     if(!QABase.empty())
1042     {
1043       TCanvas *c1 = new TCanvas();
1044       c1->SaveAs(std::format("{}_dPhi_{}.pdf[",QABase, sname).c_str());
1045       for(int i=0; i<nrow; i++)
1046       {
1047         c1->Clear();
1048         hDPhi[i]->Draw();
1049         c1->SaveAs(std::format("{}_dPhi_{}.pdf",QABase, sname).c_str());
1050       }
1051       c1->SaveAs(std::format("{}_dPhi_{}.pdf]",QABase, sname).c_str());
1052 
1053       delete c1;
1054     }
1055 
1056     for(auto *hp : hDPhi){ delete hp; }
1057 
1058     // ---- pass 2a: stripe peak positions per row, stepping by dPhi ----
1059     std::vector<Stripes> stripes(nrow);
1060     std::vector<std::vector<double>> peaksPerRow(nrow);
1061 
1062     for(int i=0; i<nrow; i++)
1063     {
1064       //! too few clusters to measure a stripe spacing: leave this row without
1065       //! peaks (its clusters stay unmatched) instead of walking with no valid step
1066       if(recoDphi[i] <= 0.0)
1067       {
1068         if(verbosity)
1069         {
1070           std::cout << "  row " << i << ": no stripe spacing (" << rowClus[i].size()
1071                     << " clusters), skipping stripe finding" << std::endl;
1072         }
1073         continue;
1074       }
1075       stripes[i] = findStripes(rowClus[i], lo, 0.004, 0.0015, 720,
1076                                 petal, recoDphi[i], stripeEdgeFloorFrac);
1077       peaksPerRow[i] = stripes[i].peakPhi;                            
1078     }
1079 
1080     // ---- pass 2b: classify each row's first/last peaks as lamination or stripe ----
1081     std::vector<std::vector<bool>> isLamRow(nrow);
1082     std::vector<int> recoN(nrow, 0);
1083     std::vector<int> lamIdxPerRow(nrow, -1);
1084 
1085     for(int i=0; i<nrow; i++)
1086     {
1087       isLamRow[i] = std::vector<bool>(peaksPerRow[i].size(), false);
1088 
1089       //! The petal is folded with period `petal`, so its two edges (lo and lo+petal) are the
1090       //! same point on the circle -- one lamination per petal, found by a single search.
1091       int lamIdx = findLaminationNear(peaksPerRow, i, lo, lamSearchGate, lamRowHalfWindow,
1092                                       lamMatchTol, lamMatchFrac, petal);
1093       if (lamIdx >= 0){ isLamRow[i][lamIdx] = true; }
1094       lamIdxPerRow[i] = lamIdx;
1095 
1096       recoN[i] = (int) std::count(isLamRow[i].begin(), isLamRow[i].end(), false);
1097 
1098       if(verbosity > 1)
1099       {
1100         std::cout << "  row " << std::setw(2) << i
1101                 << "   R = "      << std::setw(8) << rowR[i]
1102                 << "   clusters " << std::setw(5) << (int) rowClus[i].size()
1103                 << "   spacing "  << std::setw(10) << recoDphi[i]
1104                 << "   peaks "    << std::setw(3) << (int) peaksPerRow[i].size()
1105                 << "   stripes "  << std::setw(3) << recoN[i]
1106                 << "   lam "      << ((int) peaksPerRow[i].size() - recoN[i])
1107                 << std::endl;
1108       }
1109     }
1110 
1111     // ---- pass 2c: recover laminations findLaminationNear missed, using
1112     // positions predicted from independently-confirmed neighbouring rows ----
1113     auto snapLam = snapshotLamEdge(peaksPerRow, lamIdxPerRow);
1114 
1115     for(int i=0; i<nrow; i++)
1116     {
1117       if(lamIdxPerRow[i] < 0)
1118       {
1119         auto [pred, have] = predictLamPos(snapLam, i, lamRecoverySearchRows, petal);
1120         if(have)
1121         {
1122           double floor = lamRecoveryFloorFrac * medianPeakDensity(stripes[i], peaksPerRow[i]);
1123           const bool recovered = recoverLamination(peaksPerRow[i], isLamRow[i], stripes[i], pred,
1124                                                    lamRecoveryMatchTol, petal, floor, recoDphi[i]);
1125           if(verbosity)
1126           {
1127             std::cout << "  row " << i << " lamination "
1128                       << (recovered ? "recovered" : "NOT recovered")
1129                       << " near predicted " << pred << std::endl;
1130           }
1131         }
1132       }
1133       recoN[i] = std::count(isLamRow[i].begin(), isLamRow[i].end(), false);
1134     }
1135 
1136     if(!QABase.empty())
1137     {
1138       TCanvas *c1 = new TCanvas();
1139       gStyle->SetOptStat(0);
1140       c1->SaveAs(std::format("{}_phiPeaks_{}.pdf[",QABase, sname).c_str());
1141       for(int i=0; i<nrow; i++)
1142       {
1143         if(stripes[i].grid.empty()){ continue; }        
1144         TGraph *gr = new TGraph(stripes[i].grid.size(), stripes[i].grid.data(), stripes[i].density.data());
1145         gr->SetTitle(std::format("{} row {} (R = {:.2f}, spacing = {:.4f}, stripes = {});"
1146                               "folded #phi [rad];density",
1147                               sname, i, rowR[i], recoDphi[i], recoN[i]).c_str());
1148         gr->SetBit(kCanDelete);                              
1149         gr->GetXaxis()->SetRangeUser(lo, lo+petal);
1150         gr->SetMarkerStyle(20);
1151         gr->SetMarkerSize(0.5);
1152         gr->Draw("ALP");
1153         for (size_t ip = 0; ip < peaksPerRow[i].size(); ip++)
1154         {
1155           const double p = peaksPerRow[i][ip];
1156           TLine *l = new TLine(p, 0, p, gr->GetHistogram()->GetMaximum());
1157           l->SetBit(kCanDelete);
1158           l->SetLineColor(isLamRow[i][ip] ? kRed : kBlue);
1159           l->Draw("same");
1160         }
1161         c1->SaveAs(std::format("{}_phiPeaks_{}.pdf",QABase, sname).c_str());
1162       }
1163       c1->SaveAs(std::format("{}_phiPeaks_{}.pdf]",QABase, sname).c_str());
1164       delete c1;
1165     }
1166 
1167     // ---- step 3: match to truth R pattern using number of stripes in each row (uses gaps and dR as backups) ----
1168     if(truth.empty())
1169     {
1170       if(verbosity){ std::cout << "no truth table for " << sname << ", skipping matching" << std::endl; }
1171       return keyMatch;
1172     }
1173 
1174     MatchResult best;
1175     matchDP(rowR, recoN, truth, sigR, sigGap, best, verbosity);
1176     if(best.truthOf.empty())
1177     {
1178       if(verbosity){ std::cout << "no good matching found for " << sname << ", returning without matches" << std::endl; }
1179       return keyMatch;
1180     }
1181 
1182     if(verbosity > 1)
1183     {
1184       std::cout << "--- " << sname << " truth matching ---" << std::endl;
1185       std::cout << "  best nMismatch  " << best.dp.nMismatch << "  (primary)" << std::endl;
1186       std::cout << "  best gapChi2    " << best.dp.gapChi2   << "  (spacing tiebreak)" << std::endl;
1187       std::cout << "  best rChi2      " << best.dp.rChi2     << "  (last resort)" << std::endl;
1188 
1189       std::cout << "  unmatched truth rows:";
1190       if (best.skipped.empty()){ std::cout << " none"; }
1191       for (int t : best.skipped)
1192       {
1193         std::cout << " " << t << " (R=" << truth[t].R << ")";
1194       }
1195       std::cout << std::endl;
1196 
1197       for (int i = 0; i < nrow; i++)
1198       {
1199         const TruthRow &t = truth[best.truthOf[i]];
1200         const double dR = rowR[i] - t.R;
1201         std::cout << "  reco " << std::setw(2) << i
1202                   << " -> truth " << std::setw(2) << best.truthOf[i]
1203                   << "   dR = " << std::setw(10) << dR;
1204         if (recoDphi[i] > 0.0 && t.dphi > 0.0)
1205         {
1206           std::cout << "   dphi ratio = " << recoDphi[i] / t.dphi;
1207         }
1208         if (recoN[i] > 0 && t.nstripes > 0)
1209         {
1210           std::cout << "   stripes " << recoN[i] << " vs " << t.nstripes;
1211         }
1212         std::cout << std::endl;
1213       }
1214     }
1215     
1216     // ---- step 4: truth iphi assignment for each peak in each row ----
1217     std::vector<std::vector<int>> iphiAssignments;
1218     int nRowsFullyAligned = 0;
1219     int nRowsWithRealPeaks = 0;
1220     for(int i=0; i<nrow; i++)
1221     {
1222       std::vector<int> iphiAssignment = alignPhiPeaksToTruth(peaksPerRow[i], isLamRow[i], recoDphi[i],
1223                                                               lo, petal, truthRowPatterns.at(best.truthOf[i]));
1224 
1225       bool haveRealPeak = false;
1226       bool allAssigned = true;
1227       for(size_t k=0; k<isLamRow[i].size(); k++)
1228       {
1229         if(isLamRow[i][k]){ continue; }
1230         haveRealPeak = true;
1231         if(iphiAssignment[k] < 0){ allAssigned = false; }
1232       }          
1233       if(haveRealPeak)
1234       {
1235         nRowsWithRealPeaks++;
1236         if(allAssigned){ nRowsFullyAligned++; }
1237       }
1238       
1239       if(verbosity > 1)
1240       {
1241         std::cout << "   row " << std::setw(3) << i << " " << std::setw(8) << "peak" << std::setw(12) << "assignment" << std::endl;
1242         for (int j = 0; j < (int) peaksPerRow[i].size(); j++)
1243         {
1244           std::cout << "           " << std::setw(8) << peaksPerRow[i][j] << std::setw(12) << iphiAssignment[j] << std::endl;
1245         }
1246       }
1247       iphiAssignments.push_back(iphiAssignment);
1248     }
1249 
1250     if(verbosity){ std::cout << "  phi alignment: " << nRowsFullyAligned << "/" << nRowsWithRealPeaks << " rows fully assigned" << std::endl; }
1251 
1252     // ---- step 5a: per cluster truth assignment ----
1253     //! For every cluster, find its (row, iphi); lamination never get a phi-slot and never enert
1254     //! the petal search at all. For everything else, rank ALL 18 petal candidated by phi distance
1255     //! (not just the closest)
1256     std::vector<int> clusterRow(clusters.size(), -1);
1257     std::vector<int> clusterIphi(clusters.size(), -1);
1258     std::vector<bool> clusterIsLam(clusters.size(), false);
1259     std::vector<std::vector<PetalCand>> clusterPetalCands(clusters.size());
1260 
1261     for(size_t ci = 0; ci < clusters.size(); ci++)
1262     {
1263       const auto &c = clusters[ci];
1264       const int row = rowOf(c.R, rowResult.bound);
1265       clusterRow[ci] = row;
1266 
1267       if(peaksPerRow[row].empty()){ continue; }  // clusterIphi stays -1 -> unmatched
1268       int peak = nearestPeak(foldPetal(c.phi, lo, petal), peaksPerRow[row], petal);
1269       if(isLamRow[row][peak])
1270       {
1271         clusterIsLam[ci] = true;
1272         clusterIphi[ci] = -1;
1273         continue; // lamination cluster -- never attempt a truth match
1274       }
1275 
1276       const int iphi = iphiAssignments[row][peak];
1277       clusterIphi[ci] = iphi;
1278       if(iphi < 0){ continue; }
1279 
1280       std::vector<PetalCand> cands;
1281       cands.reserve(18);
1282       for(int p=0; p<18; p++)
1283       {
1284         unsigned int currentIndex = encodeTruthIndex(side, p, best.truthOf[row], iphi);
1285         double candTruthPhi = cdbttree.GetDoubleValue(currentIndex, "truthPhi");
1286         double dPhi = std::abs(circDiff(c.phi, candTruthPhi, 2.0*TMath::Pi()));
1287         cands.push_back({dPhi, p});
1288       }
1289       std::sort(cands.begin(), cands.end(), [](const PetalCand &a, const PetalCand &b){ return a.dPhi < b.dPhi; });
1290       clusterPetalCands[ci] = std::move(cands);
1291     }
1292 
1293     std::map<std::pair<int,int>, std::vector<int>> groups;
1294     for(size_t ci = 0; ci < clusters.size(); ci++)
1295     {
1296       if(clusterIphi[ci] < 0){ continue; }
1297       groups[{clusterRow[ci], clusterIphi[ci]}].push_back((int) ci);
1298     }
1299 
1300     std::vector<int> resolvedPetal(clusters.size(), -1);
1301     for(auto &[key, members] : groups)
1302     {
1303       std::sort(members.begin(), members.end(), [&](int a, int b)
1304       {
1305         return clusters[a].adc > clusters[b].adc; // highest ADC claims first
1306       });
1307 
1308       std::set<int> takenPetals;
1309       for(int ci : members)
1310       {
1311         for(const auto &c : clusterPetalCands[ci])
1312         {
1313           if(c.dPhi > petal / 2.0){ break; }
1314           if(takenPetals.contains(c.petal)){ continue; }
1315           resolvedPetal[ci] = c.petal;
1316           takenPetals.insert(c.petal);
1317           break;
1318         }
1319       }
1320     }
1321 
1322     for(int ci = 0; ci<(int)clusters.size(); ci++)
1323     {
1324       if (clusterIsLam[ci])
1325       {
1326         keyMatch[clusters[ci].key] = {-999, true};
1327       }
1328       else if (clusterIphi[ci] < 0)
1329       {
1330         keyMatch[clusters[ci].key] = {-999, false};
1331       }
1332       else if (resolvedPetal[ci] >= 0)
1333       {
1334         unsigned int matchedIndex = encodeTruthIndex(side, resolvedPetal[ci], best.truthOf[clusterRow[ci]], clusterIphi[ci]);
1335         keyMatch[clusters[ci].key] = {matchedIndex, false};
1336       }
1337     }
1338     return keyMatch;
1339   }// end matchSide
1340 
1341   struct RowPoints
1342   {
1343     std::vector<double> phi;
1344     std::vector<double> dR;
1345     std::vector<double> dPhi;
1346     std::vector<double> adc;
1347     double recoRow{0};
1348   };
1349 
1350   inline double wrapTwoPi(double phi)            // -> [0, 2pi)
1351   {
1352     phi = std::fmod(phi, 2*TMath::Pi());
1353     return (phi < 0) ? phi + 2*TMath::Pi() : phi;
1354   }
1355 
1356   // y(phi) for one row at phi0: Gaussian-kernel local-linear, periodic on [0, 2pi).
1357   // Requires phi[k] already in [0, 2pi) (the module does this when filling the points).
1358   // h in rad.
1359   double localLinear(const std::vector<double>& phi, const std::vector<double>& y,
1360                     const std::vector<double>& w, double phi0, double h, double& wSum)
1361   {
1362     phi0 = wrapTwoPi(phi0);   // histogram edge bins can sit slightly outside [0, 2pi)
1363 
1364     double S0=0;
1365     double S1=0;
1366     double S2=0;
1367     double T0=0;
1368     double T1=0;
1369     for (size_t k = 0; k < phi.size(); ++k)
1370     {
1371       double d = phi[k] - phi0;                  // in (-2pi, 2pi) for inputs in [0, 2pi)
1372       if      (d >  TMath::Pi()){ d -= 2*TMath::Pi(); }           // shortest signed distance around the circle
1373       else if (d < -TMath::Pi()){ d += 2*TMath::Pi(); }
1374       if (std::abs(d) > 4*h){ continue; }
1375 
1376       const double K = w[k] * std::exp(-0.5*d*d/(h*h));
1377       S0 += K;
1378       S1 += K*d;
1379       S2 += K*d*d;
1380       T0 += K*y[k];
1381       T1 += K*d*y[k];
1382     }
1383     wSum = S0;                                   // effective weight -> fill hentries
1384     if (S0 <= 0){ return NAN; }
1385     const double det = S0*S2 - S1*S1;
1386     if (det < 1e-12 * S0*S0*h*h){ return T0/S0; }   // too few points: local-constant fallback
1387     return (S2*T0 - S1*T1) / det;                // local-linear intercept
1388   }
1389 
1390   void fill_guarding_bins(TpcDistortionCorrectionContainer *dcc)
1391   {
1392     for(int side=0; side<2; side++)
1393     {
1394       for(const auto& h : {dcc->m_hDRint[side], dcc->m_hDPint[side], dcc->m_hentries[side]})
1395       {
1396         // fill guarding phi bins
1397         /*
1398         * we use 2pi periodicity to do that:
1399         * - last valid bin is copied to first guarding bin;
1400         * - first valid bin is copied to last guarding bin
1401         */
1402         const auto phibins = h->GetNbinsX();
1403         const auto rbins = h->GetNbinsY();
1404         for (int ir = 0; ir < rbins; ++ir)
1405         {
1406           // copy last valid bin to first guarding bin
1407           h->SetBinContent(1, ir + 1, h->GetBinContent(phibins - 1, ir + 1));
1408           h->SetBinError(1, ir + 1, h->GetBinError(phibins - 1, ir + 1));
1409 
1410           // copy first valid bin to last guarding bin
1411           h->SetBinContent(phibins, ir + 1, h->GetBinContent(2, ir + 1));
1412           h->SetBinError(phibins, ir + 1, h->GetBinError(2, ir + 1));
1413         }
1414       }
1415     }
1416   }
1417 
1418 }  // namespace
1419 
1420 LaserClusterTruthMatcher::LaserClusterTruthMatcher(const std::string &name)
1421   : SubsysReco(name)
1422 {
1423 }
1424 
1425 int LaserClusterTruthMatcher::getNodes(PHCompositeNode *topNode)
1426 {
1427   m_laserClusterContainer = findNode::getClass<LaserClusterContainer>(topNode, m_laserClusterNodeName);
1428   if (!m_laserClusterContainer)
1429   {
1430     std::cout << PHWHERE << " no LaserClusterContainer named " << m_laserClusterNodeName
1431               << " on the node tree" << std::endl;
1432     return Fun4AllReturnCodes::ABORTRUN;
1433   }
1434 
1435   PHNodeIterator iter(topNode);
1436 
1437   // Looking for the DST node
1438   PHCompositeNode *dstNode = dynamic_cast<PHCompositeNode *>(iter.findFirst("PHCompositeNode", "DST"));
1439   if (!dstNode)
1440   {
1441     std::cout << PHWHERE << "DST Node missing, doing nothing." << std::endl;
1442     return Fun4AllReturnCodes::ABORTRUN;
1443   }
1444 
1445   auto *laserClusterContainerOut = findNode::getClass<LaserClusterContainer>(dstNode, m_laserClusterNodeNameOut);
1446   if (!laserClusterContainerOut)
1447   {
1448     PHNodeIterator dstiter(dstNode);
1449     PHCompositeNode *DetNode =
1450         dynamic_cast<PHCompositeNode *>(dstiter.findFirst("PHCompositeNode", "TRKR"));
1451     if (!DetNode)
1452     {
1453       DetNode = new PHCompositeNode("TRKR");
1454       dstNode->addNode(DetNode);
1455     }
1456 
1457     laserClusterContainerOut = new LaserClusterContainerv1;
1458     PHIODataNode<PHObject> *LaserClusterContainerNodeOut =
1459         new PHIODataNode<PHObject>(laserClusterContainerOut, m_laserClusterNodeNameOut, "PHObject");
1460     DetNode->addNode(LaserClusterContainerNodeOut);
1461   }
1462 
1463   m_laserClusterHelper.set_useZ(false);
1464   m_laserClusterHelper.set_useDouble(true);
1465   m_laserClusterHelper.set_useGlobal(m_useGlobal);
1466   m_laserClusterHelper.set_useGarfield(m_usePHGarfieldDistortions);
1467   if(m_usePHGarfieldDistortions)
1468   {
1469     m_laserClusterHelper.set_garfield_cmvoltage(m_garfield_cmvoltage);
1470     m_laserClusterHelper.set_garfield_zerofield(m_garfield_zerofield);
1471     if(m_manual_garfield_keffside0){ m_laserClusterHelper.set_garfield_keffside0(m_garfield_keffside0); }
1472     if(m_manual_garfield_keffside1){ m_laserClusterHelper.set_garfield_keffside1(m_garfield_keffside1); }
1473     m_laserClusterHelper.set_garfield_stepns(m_garfield_stepns);
1474   }
1475   m_laserClusterHelper.loadNodes(topNode);
1476 
1477   const float phiMin = m_phiMin - (m_phiMax - m_phiMin) / m_phibins;
1478   const float phiMax = m_phiMax + (m_phiMax - m_phiMin) / m_phibins;
1479 
1480   const float rMin = m_rMin - (m_rMax - m_rMin) / m_rbins;
1481   const float rMax = m_rMax + (m_rMax - m_rMin) / m_rbins;
1482 
1483   const std::array<const std::string, 2> extension = {{"_negz", "_posz"}};
1484 
1485   m_dcc_out = new TpcDistortionCorrectionContainer();
1486   m_dcc_out->m_dimensions = 2;
1487 
1488   // create all histograms
1489   for (int i = 0; i < 2; ++i)
1490   {
1491     delete m_dcc_out->m_hDPint[i];
1492     m_dcc_out->m_hDPint[i] = new TH2F(std::format("hIntDistortionP{}", extension[i]).c_str(), std::format("hIntDistortionP{}", extension[i]).c_str(), m_phibins + 2, phiMin, phiMax, m_rbins + 2, rMin, rMax);
1493     delete m_dcc_out->m_hDRint[i];
1494     m_dcc_out->m_hDRint[i] = new TH2F(std::format("hIntDistortionR{}", extension[i]).c_str(), std::format("hIntDistortionR{}", extension[i]).c_str(), m_phibins + 2, phiMin, phiMax, m_rbins + 2, rMin, rMax);
1495     delete m_dcc_out->m_hDZint[i];
1496     m_dcc_out->m_hDZint[i] = new TH2F(std::format("hIntDistortionZ{}", extension[i]).c_str(), std::format("hIntDistortionZ{}", extension[i]).c_str(), m_phibins + 2, phiMin, phiMax, m_rbins + 2, rMin, rMax);
1497     delete m_dcc_out->m_hentries[i];
1498     m_dcc_out->m_hentries[i] = new TH2I(std::format("hEntries{}", extension[i]).c_str(), std::format("hEntries{}", extension[i]).c_str(), m_phibins + 2, phiMin, phiMax, m_rbins + 2, rMin, rMax);
1499   }
1500 
1501 
1502   return Fun4AllReturnCodes::EVENT_OK;
1503 }
1504 
1505 int LaserClusterTruthMatcher::InitRun(PHCompositeNode *topNode)
1506 {
1507 
1508   // truth pattern is reloaded every run since it's cheap;
1509   delete m_cdbttree;
1510   m_cdbttree = new CDBTTree(m_truthFile);
1511   m_cdbttree->LoadCalibrations();
1512 
1513   for(int side=0; side<2; side++)
1514   {
1515     m_truthRowPatterns[side] = buildTruthRowPattern(*m_cdbttree, side);
1516     m_truthRows[side].clear();
1517     for(int row=0; row<(int)m_truthRowPatterns[side].size(); row++)
1518     {
1519       TruthRow newRow;
1520       unsigned int truthIndex = (side ? 18 : 0)*10000 + (row*100) + 0;
1521       newRow.R = m_cdbttree->GetDoubleValue(truthIndex, "truthR");
1522       const auto &sp = m_truthRowPatterns[side][row].stripePhi;
1523       newRow.dphi = (sp.size() >= 2) ? sp[1] - sp[0] : -1.0;
1524       newRow.nstripes = static_cast<int>(sp.size());
1525 
1526       m_truthRows[side].push_back(newRow);
1527     }
1528   }
1529 
1530   return getNodes(topNode);
1531 }
1532 
1533 int LaserClusterTruthMatcher::process_event(PHCompositeNode *topNode)
1534 {
1535   // the aggregated clusters represent one run's worth of laser data
1536   // collapsed into a single pseudo-event, so this whole-side pipeline only
1537   // needs to run once per side per call here -- there is no meaningful
1538   // "next event" for this data
1539 
1540   LaserClusterContainer *laserClusterContainerOut = findNode::getClass<LaserClusterContainer>(topNode, m_laserClusterNodeNameOut);
1541   if (!laserClusterContainerOut)
1542   {
1543     std::cout << PHWHERE << " no output LaserClusterContainer named " << m_laserClusterNodeNameOut
1544               << " on the node tree" << std::endl;
1545     return Fun4AllReturnCodes::ABORTRUN;
1546   }
1547 
1548 
1549   std::vector<Cluster> clusters[2]{};
1550   
1551   auto clusrange = m_laserClusterContainer->getClusters();
1552   for (auto cmitr = clusrange.first; cmitr != clusrange.second; ++cmitr)
1553   {
1554     const auto &[cmkey, cmclus_orig] = *cmitr;
1555     LaserCluster *cmclus = cmclus_orig;
1556 
1557     if (auto *outClus = dynamic_cast<LaserCluster *>(cmclus->CloneMe()))
1558     {
1559       outClus->setTruthIndex(-999);
1560       outClus->setIsLamination(false);
1561       laserClusterContainerOut->addClusterSpecifyKey(cmkey, outClus);
1562     }
1563 
1564     int side = TpcDefs::getSide(cmkey);
1565 
1566     Acts::Vector3 pos;
1567     if(m_usePHGarfieldDistortions){ pos = m_laserClusterHelper.getClusterCentroidWithPHGarfield(cmclus); }
1568     else{ pos = m_laserClusterHelper.getClusterCentroid(cmclus); }
1569     if(pos.hasNaN())
1570     {
1571       continue;
1572     }
1573 
1574     double R = sqrt(pos[0]*pos[0] + pos[1]*pos[1]);
1575     double phi = atan2(pos[1], pos[0]);
1576 
1577     clusters[side].push_back({R, phi, cmclus->getAdcDouble(), cmkey});
1578   }
1579 
1580   TFile *distortionOutputfile = new TFile(m_distortionOutputfile.c_str(), "RECREATE");
1581   TGraph2D *gr_dR{nullptr};
1582   TGraph2D *gr_dPhi{nullptr};
1583   TGraph *gr_recoPoints{nullptr};
1584   TGraph *gr_truthPoints{nullptr};
1585 
1586   TGraph2D *gr_dR_toInterp{nullptr};
1587   TGraph2D *gr_dPhi_toInterp{nullptr};
1588 
1589   TGraph *gR{nullptr};
1590   TGraph *gP{nullptr};
1591   
1592 
1593   TTree *QATree = nullptr;
1594   double recoR = 0.0;
1595   double recoPhi = 0.0;
1596   double truthR = 0.0;
1597   double truthPhi = 0.0;
1598   bool sideVar = false;
1599   bool lamVar = false;
1600   int truthIndex = 0;
1601   if(!m_QABase.empty())
1602   {
1603     QATree = new TTree("QATree","Matching QA TTree");
1604     QATree->Branch("recoR",&recoR);
1605     QATree->Branch("recoPhi",&recoPhi);
1606     QATree->Branch("truthR",&truthR);
1607     QATree->Branch("truthPhi",&truthPhi);
1608     QATree->Branch("side",&sideVar);
1609     QATree->Branch("isLam",&lamVar);
1610     QATree->Branch("truthIndex",&truthIndex);
1611   }
1612 
1613 
1614   for(int side=0; side<2; side++)
1615   {
1616     gr_dR = new TGraph2D();
1617     gr_dPhi = new TGraph2D();
1618     gr_recoPoints = new TGraph();
1619     gr_truthPoints = new TGraph();
1620 
1621     std::map<int, RowPoints> rowPts;
1622 
1623     Result rowResult = findRows(clusters[side], 0.15, 0.05);
1624     std::vector<double> rowR = refine(clusters[side], rowResult);
1625 
1626     std::map<TrkrDefs::cluskey, std::pair<int,bool>> matched = matchSide(clusters[side], *m_cdbttree, m_truthRowPatterns[side], m_truthRows[side], side, m_QABase, Verbosity());
1627     for(auto &[key, matchedIndex] : matched)
1628     {
1629       LaserCluster *clus = laserClusterContainerOut->findCluster(key);
1630       if(clus)
1631       {
1632         clus->setTruthIndex(matchedIndex.first);
1633         clus->setIsLamination(matchedIndex.second);
1634 
1635         int matchedTruthRow = (matchedIndex.first - (10000*(matchedIndex.first / 10000)))/100;
1636 
1637         Acts::Vector3 pos;
1638         if(m_usePHGarfieldDistortions){ pos = m_laserClusterHelper.getClusterCentroidWithPHGarfield(clus); }
1639         else{ pos = m_laserClusterHelper.getClusterCentroid(clus); }
1640         if(pos.hasNaN())
1641         {
1642           continue;
1643         }
1644 
1645         recoR = sqrt(pos[0]*pos[0] + pos[1]*pos[1]);
1646         recoPhi = atan2(pos[1], pos[0]);
1647         if(recoPhi < 0){ recoPhi += 2*TMath::Pi(); }
1648         if(matchedIndex.first >= 0)
1649         {
1650           truthR = m_cdbttree->GetDoubleValue(matchedIndex.first, "truthR");
1651           truthPhi = m_cdbttree->GetDoubleValue(matchedIndex.first, "truthPhi");
1652         }
1653         else
1654         {
1655           truthR = -999;
1656           truthPhi = -999;
1657         }
1658         if(truthPhi < 0){ truthPhi += 2*TMath::Pi(); }
1659 
1660         int recoRow = rowOf(recoR, rowResult.bound);
1661 
1662 
1663         if(!m_QABase.empty())
1664         {
1665           sideVar = (bool)side;
1666           lamVar = matchedIndex.second;
1667           truthIndex = matchedIndex.first;
1668           QATree->Fill();
1669         }
1670 
1671 
1672         double dR = recoR - truthR;
1673         double dPhi = recoPhi - truthPhi;
1674         if(dPhi > TMath::Pi()){ dPhi -= 2*TMath::Pi(); }
1675         else if(dPhi < -TMath::Pi()){ dPhi += 2*TMath::Pi(); }
1676         if(!matchedIndex.second && matchedIndex.first >= 0 && matchedTruthRow != 23 && matchedTruthRow != 15)
1677         {
1678 
1679           auto &rp = rowPts[matchedTruthRow];
1680           rp.phi.push_back(recoPhi);                      // already in [0, 2pi)
1681           rp.dR.push_back(dR);
1682           rp.dPhi.push_back(dPhi);
1683           rp.adc.push_back(1.0);
1684           //rp.adc.push_back(std::sqrt(clus->getAdcDouble()));
1685           rp.recoRow = rowR[recoRow];
1686 
1687           gr_dR->AddPoint(recoPhi, recoR, dR);
1688           gr_dPhi->AddPoint(recoPhi, recoR, dPhi);
1689           gr_recoPoints->AddPoint(recoPhi, recoR);
1690           gr_truthPoints->AddPoint(truthPhi, truthR);
1691 
1692           if(recoPhi < TMath::Pi()/12)
1693           {
1694             gr_dR->AddPoint(recoPhi+2*TMath::Pi(), recoR, dR);
1695             gr_dPhi->AddPoint(recoPhi+2*TMath::Pi(), recoR, dPhi);
1696           }
1697           else if(recoPhi > 23*TMath::Pi()/12)
1698           {
1699             gr_dR->AddPoint(recoPhi-2*TMath::Pi(), recoR, dR);
1700             gr_dPhi->AddPoint(recoPhi-2*TMath::Pi(), recoR, dPhi);
1701           }
1702           
1703         }
1704       }
1705     }
1706 
1707     const double hCm = 3.0;       // bandwidth in cm; tune this
1708     const double minW = 2.0;      // minimum effective weight for a row to count at this phi
1709 
1710 
1711     gr_dR_toInterp = (TGraph2D*)gr_dR->Clone();
1712     gr_dPhi_toInterp = (TGraph2D*)gr_dPhi->Clone();
1713 
1714     for(int i=2; i<=m_dcc_out->m_hDRint[side]->GetNbinsX()-1; i++)
1715     {
1716       const double phiVal = m_dcc_out->m_hDRint[side]->GetXaxis()->GetBinCenter(i);
1717 
1718       std::vector<double> Rs;
1719       std::vector<double> dRs;
1720       std::vector<double> dPs;         // one entry per row with data here
1721       for (auto &[row, rp] : rowPts)
1722       {
1723         double wS = 0.0;
1724         const double binW = m_dcc_out->m_hDRint[side]->GetXaxis()->GetBinWidth(1);
1725         const double h = std::max(hCm / rp.recoRow, 0.5 * binW);
1726         const double vR = localLinear(rp.phi, rp.dR, rp.adc, phiVal, h, wS);
1727         if (std::isnan(vR) || wS < minW){ continue; }
1728         const double vP = localLinear(rp.phi, rp.dPhi, rp.adc, phiVal, h, wS);
1729         Rs.push_back(rp.recoRow);
1730         dRs.push_back(vR);
1731         dPs.push_back(vP);
1732       }
1733       if (Rs.size() < 2){ continue; }              // leave the column empty
1734 
1735       gR = new TGraph(Rs.size(), Rs.data(), dRs.data());   // rows are already ordered by R (std::map key)
1736       gP = new TGraph(Rs.size(), Rs.data(), dPs.data());
1737 
1738       for(int j=2; j<=m_dcc_out->m_hDRint[side]->GetNbinsY()-1; j++)
1739       {
1740         const double RVal = m_dcc_out->m_hDRint[side]->GetYaxis()->GetBinCenter(j);
1741         const bool inside = (RVal >= Rs.front() && RVal <= Rs.back());
1742         if(inside)
1743         {
1744           m_dcc_out->m_hDRint[side]->SetBinContent(i, j, gR->Eval(RVal));
1745           m_dcc_out->m_hDPint[side]->SetBinContent(i, j, gP->Eval(RVal));
1746           m_dcc_out->m_hentries[side]->SetBinContent(i, j, Rs.size());
1747         }
1748       }
1749       if(!m_QABase.empty())
1750       {
1751         distortionOutputfile->cd();
1752         gR->SetTitle(std::format("TPC {};R_{{reco}} [cm];#DeltaR [cm]",(side ? "North" : "South")).c_str());
1753         gR->Write(std::format("dR_phiBin{}_{}z",i,(side == 1 ? "pos" : "neg")).c_str());
1754         gP->SetTitle(std::format("TPC {};R_{{reco}} [cm];#Delta#phi [rad]",(side ? "North" : "South")).c_str());
1755         gP->Write(std::format("dPhi_phiBin{}_{}z",i,(side == 1 ? "pos" : "neg")).c_str());
1756       }
1757 
1758       delete gR;
1759       delete gP;
1760     }
1761 
1762     /*
1763     for (int i = 2; i <= m_dcc_out->m_hDRint[side]->GetNbinsX() - 1; i++)
1764     {
1765       double phiVal = m_dcc_out->m_hDRint[side]->GetXaxis()->GetBinCenter(i);
1766       for (int j = 1; j <= m_dcc_out->m_hDRint[side]->GetNbinsY(); j++)
1767       {
1768         double RVal = m_dcc_out->m_hDRint[side]->GetYaxis()->GetBinCenter(j);
1769         m_dcc_out->m_hDRint[side]->SetBinContent(i, j, gr_dR_toInterp->Interpolate(phiVal,RVal));
1770         m_dcc_out->m_hDPint[side]->SetBinContent(i, j, gr_dPhi_toInterp->Interpolate(phiVal,RVal));
1771       }
1772     }
1773     */
1774 
1775     fill_guarding_bins(m_dcc_out);
1776 
1777     distortionOutputfile->cd();
1778     for (const auto& h : {m_dcc_out->m_hDRint[side], m_dcc_out->m_hDPint[side], m_dcc_out->m_hDZint[side], m_dcc_out->m_hentries[side]})
1779     {
1780       if (h)
1781       {
1782         std::cout << "writing in " << m_distortionOutputfile << "   histogram " << h->GetName() << std::endl;
1783         h->Write();
1784       }
1785     }
1786     gr_recoPoints->Write(std::format("gr_recoPoints_{}z", (side == 1 ? "pos" : "neg")).c_str());
1787     gr_truthPoints->Write(std::format("gr_truthPoints_{}z", (side == 1 ? "pos" : "neg")).c_str());
1788     gr_dR->Write(std::format("gr_dr_{}z", (side == 1 ? "pos" : "neg")).c_str());
1789     gr_dPhi->Write(std::format("gr_dPhi_{}z", (side == 1 ? "pos" : "neg")).c_str());
1790 
1791     gr_dR_toInterp->Write(std::format("gr_dr_toInterp_{}z", (side == 1 ? "pos" : "neg")).c_str());
1792     gr_dPhi_toInterp->Write(std::format("gr_dPhi_toInterp_{}z", (side == 1 ? "pos" : "neg")).c_str());
1793 
1794     //gR->Write(std::format("g_dR_{}z", (side == 1 ? "pos" : "neg")).c_str());
1795     //gP->Write(std::format("g_dP_{}z", (side == 1 ? "pos" : "neg")).c_str());
1796 
1797   }
1798   if(!m_QABase.empty())
1799   {
1800     QATree->Write();
1801   }
1802   distortionOutputfile->Close();
1803 
1804   return Fun4AllReturnCodes::EVENT_OK;
1805 }
1806 
1807 int LaserClusterTruthMatcher::End(PHCompositeNode * /*topNode*/)
1808 {
1809   delete m_cdbttree;
1810   m_cdbttree = nullptr;
1811   return Fun4AllReturnCodes::EVENT_OK;
1812 }