Skip to content

CM Distortion Updates#4169

Merged
osbornjd merged 11 commits intosPHENIX-Collaboration:masterfrom
bkimelman:laserClusterTuning
Feb 9, 2026
Merged

CM Distortion Updates#4169
osbornjd merged 11 commits intosPHENIX-Collaboration:masterfrom
bkimelman:laserClusterTuning

Conversation

@bkimelman
Copy link
Copy Markdown
Contributor

@bkimelman bkimelman commented Feb 6, 2026

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work for users)
  • Requiring change in macros repository (Please provide links to the macros pull request in the last section)
  • I am a member of GitHub organization of sPHENIX Collaboration, EIC, or ECCE (contact Chris Pinkenburg to join)

What kind of change does this PR introduce? (Bug fix, feature, ...)

Updated laser clustering to look for dumbbell shaped proto-clusters and split them to keep only the portion with the highest ADC hit, which gets turned into a cluster. This has minimal impact in field on data, but is important to prevent cluster merging when the magnetic field is off.

Stripe matching now uses measured stripe pattern from field off data as truth (loaded from a CDBTTree) rather than ideal geometry. Also new methods to remove outliers from distortion calculation (flag false by default) as well as do a manual interpolation using weighted average where distance squared from the bin center is the weight (also has the flag as false by default).

Many updates to lamination fitting. Additional metric for goodness of fit (not fully implemented yet). New constant fit option added (with toggle) for use in field off data. Nominal lamination positions shifted by offset determined from field off data (currently hard-coded for each side).
New method for determining radial distortions to first order by assuming linear distortion as function of R. Scan parameter space of slope and y-intercept and use along with average lamination correction in phi to distort truth positions (again from measurements in field off data), calculate 1/chi2 by summing bin content in small window around each distorted truth position, highest 1/chi2 gives the best fit which is then put into distortion map for all phi values. Map of parameter space is stored in distortion file to keep track of scan.

TODOs (if applicable)

Links to other PRs in macros and calibration repositories (if applicable)

CM Distortion Updates

Motivation & Context
Reduce reconstruction artifacts (notably in field‑off laser runs) and improve TPC central membrane (CM) distortion estimation by moving to data‑driven stripe/lamination information, making clustering more robust against merged “dumbbell” proto‑clusters, and adding flexible, more-inspectable distortion interpolation and a first‑order radial distortion scan that produces QA and best‑fit parameters.

Key changes

  • Laser clustering (offline/packages/tpc/LaserClusterizer.cc)
    • Detect and split weakly‑connected/dumbbell proto‑clusters via splitWeaklyConnectedRegion(), keeping the subregion with the highest‑ADC hit; integrates into findConnectedRegions3 and cluster formation. Adds verbosity/logging controls.
  • Stripe matching & distortion interpolation (offline/packages/tpccalib/TpcCentralMembraneMatching.{cc,h})
    • Load measured stripe/truth positions from a CDBTTree (data‑driven stripe pattern) instead of assuming ideal geometry.
    • New configurable flags and setters: totalDistMode (use raw_pos in distortion path), skipOutliers (outlier removal for distortion calc; default off), manualInterp (manual, distance‑squared weighted interpolation; default off), and set_stripePatternFile(...).
    • Implements per‑side outlier skipping (peak finding + Gaussian fits) and an alternate manual interpolation path; clones and writes per‑side interpolation graphs (gr_dR_toInterp, gr_dPhi_toInterp) and added diagnostics/histograms.
    • Adjusts process_event/End flows to support new modes and to preserve additional interpolation outputs.
  • Lamination fitting & radial distortion estimation (offline/packages/tpccalib/TpcLaminationFitting.{cc,h})
    • Introduces per‑lamination/side nominal phi shifts and hard‑coded lamination offsets derived from field‑off data; behavior branches for field‑off vs field‑on runs.
    • New public doGlobalRMatching(int side): first‑order radial distortion scan (assumes linear distortion vs radius). Scans slope/intercept space, applies radial distortions plus average lamination phi corrections, scores fits (sum of bin contents near distorted truth positions using 1/χ²‑style weighting), selects best fit, writes parameters to the distortion map, and stores the parameter‑space QA surface.
    • InterpolatePhiDistortions signature changed to operate from internal state (no external TH2 input); expands laminationTree branches (lamOffset, RMSE) and QA outputs.
  • API and config changes
    • Added setters for new flags and stripe pattern file; changed InterpolatePhiDistortions(...) signature and added doGlobalRMatching(int).

Potential risk areas

  • Reconstruction behavior: Cluster splitting and switching to measured stripe truth can change hit/cluster associations and downstream calibration or physics results—validate on both field‑on and field‑off samples.
  • IO / calibration dependence: New reliance on CDBTTree stripe data and currently hard‑coded lamination offsets means missing/mismatched calibration inputs or stale hard‑coded values can prevent proper initialization or introduce incorrect corrections.
  • Configuration complexity: Multiple new flags increase the behavioral surface; unclear or unintended flag combinations could silently alter outputs.
  • Performance: Parameter‑space scans, extra interpolation passes, outlier detection, and added histograming increase CPU/memory cost—benchmark for production workflows.
  • Thread‑safety & statefulness: Added internal state (truth maps, additional histograms, gr_*_toInterp arrays, lamination offsets) and modified InitRun/End flows require review for safe use in multi‑threaded processing.
  • Maintainability: Hard‑coded per‑side offsets and spread‑out behavior increase maintenance burden and risk of stale values.

Possible future improvements

  • Move stripe patterns and lamination offsets into CDB calibrations (remove hard‑coded values).
  • Finalize and formalize additional goodness‑of‑fit metrics (e.g., likelihoods, robust estimators) and standardize scoring.
  • Add automated validation and regression tests and recommended default flag settings for field‑on vs field‑off modes.
  • Optimize/parallelize parameter scans and cache intermediate QA to reduce runtime cost.
  • Review and harden thread‑safety for multi‑threaded reconstruction.

Note: AI assistance can make mistakes—please inspect the code, run tests, and validate outputs before relying on this summary or merging.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 6, 2026

📝 Walkthrough

Walkthrough

Adds graph-based region splitting and verbosity-controlled refinement to LaserClusterizer; loads CDB truth and adds total-distortion, outlier-skipping, and manual-interpolation modes in TpcCentralMembraneMatching; updates TpcLaminationFitting to internal interpolation, field-off handling, parameter scans, and a global R-matching routine.

Changes

Cohort / File(s) Summary
LaserClusterizer region refinement
offline/packages/tpc/LaserClusterizer.cc
Adds private helper splitWeaklyConnectedRegion(const std::vector<hitData>&, std::vector<std::vector<hitData>>&, int aVerbosity) (anonymous namespace). Updates findConnectedRegions3 signature to accept int aVerbosity, delegates normal-region refinement to the splitter, sorts refined subregions prioritizing the one containing maxKey, and initializes clusHits from that refined region; adds verbosity-driven logging.
TpcCentralMembraneMatching: CDB truth & interpolation modes
offline/packages/tpccalib/TpcCentralMembraneMatching.h, offline/packages/tpccalib/TpcCentralMembraneMatching.cc
Integrates external truth map loading via CDBTTree populating m_truth_index/m_truth_pos. Adds flags and setters (m_totalDistMode, m_skipOutliers, m_manualInterp, set_stripePatternFile). Alters InitRun/process_event/End flows to support raw-position-based reco when m_totalDistMode is set, implements per-side outlier detection (peak finding + Gaussian fits), optional outlier skipping, and a manual weighted-neighbor interpolation path. Clones/writes per-side interpolation graphs (gr_dR_toInterp, gr_dPhi_toInterp) and expands diagnostic logging.
TpcLaminationFitting: internal interpolation & global R-matching
offline/packages/tpccalib/TpcLaminationFitting.h, offline/packages/tpccalib/TpcLaminationFitting.cc
Adds includes and per-side/per-lamination bookkeeping (m_laminationIdeal, m_laminationOffset, RMSE, petal histograms). Changes InterpolatePhiDistortions signature to parameterless and adds public doGlobalRMatching(int side). Introduces field-off vs field-on handling, internal interpolation using lamination data, grid-search global R-matching, expanded QA outputs (m_bestRMatch, m_parameterScan, lamOffset, RMSE), and set_stripePatternFile(...).
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Comment on lines +176 to +201
std::function<void(int)> dfs = [&](int u)
{
disc[u] = low[u] = ++time;
for(auto v : adj[u])
{
if(disc[v] == -1)
{
parent[v] = u;
dfs(v);
low[u] = std::min(low[u], low[v]);
if(low[v] > disc[u])
{
bridges.emplace_back(u,v);
}
}
else if(v != parent[u])
{
low[u] = std::min(low[u], disc[v]);
}
}
};

for(int i=0; i<N; i++)
{
if(disc[i] == -1) dfs(i);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Recursive DFS risks stack overflow on large proto-clusters.

The dfs lambda recurses to a depth equal to the connected-component size. TPC proto-clusters before splitting can easily reach hundreds to thousands of hits. With the default pthread stack (typically 2–8 MB), a deep recursion chain will overflow the stack and crash the process.

Consider converting to an iterative DFS using an explicit std::stack — the algorithm translates directly and eliminates the risk.

Sketch of iterative approach
-    std::function<void(int)> dfs = [&](int u)
-    {
-      disc[u] = low[u] = ++time;
-      for(auto v : adj[u])
-      {
-        if(disc[v] == -1)
-        {
-          parent[v] = u;
-          dfs(v);
-          low[u] = std::min(low[u], low[v]);
-          if(low[v] > disc[u])
-          {
-            bridges.emplace_back(u,v);
-          }
-        }
-        else if(v != parent[u])
-        {
-          low[u] = std::min(low[u], disc[v]);
-        }
-      }
-    };
-
-    for(int i=0; i<N; i++)
-    {
-      if(disc[i] == -1) dfs(i);
-    }
+    // Iterative Tarjan bridge-finding using explicit stack
+    struct Frame { int u; size_t idx; };
+    std::stack<Frame> stk;
+    for(int i = 0; i < N; i++)
+    {
+      if(disc[i] != -1) continue;
+      stk.push({i, 0});
+      disc[i] = low[i] = ++timer;
+      while(!stk.empty())
+      {
+        auto &[u, idx] = stk.top();
+        if(idx < adj[u].size())
+        {
+          int v = adj[u][idx++];
+          if(disc[v] == -1)
+          {
+            parent[v] = u;
+            disc[v] = low[v] = ++timer;
+            stk.push({v, 0});
+          }
+          else if(v != parent[u])
+          {
+            low[u] = std::min(low[u], disc[v]);
+          }
+        }
+        else
+        {
+          stk.pop();
+          if(!stk.empty())
+          {
+            int p = stk.top().u;
+            low[p] = std::min(low[p], low[u]);
+            if(low[u] > disc[p])
+            {
+              bridges.emplace_back(p, u);
+            }
+          }
+        }
+      }
+    }

Comment on lines +822 to +856
int TpcLaminationFitting::doGlobalRMatching(int side)
{

std::vector<double> distortedPhi;
TF1 *tmpLamFit = (TF1*)m_fLamination[0][side]->Clone();

double meanB = 0.0;

if(m_fieldOff)
{
tmpLamFit->SetParameters(0.0, 0.0);
meanB = -999.99;
}
else
{
double meanA = 0.0;
double meanC = 0.0;
double meanOffset = 0.0;
int nGoodFits = 0;
for(int l = 0; l < 18; l++)
{
if(!m_laminationGoodFit[l][side])
{
continue;
}
meanA += m_fLamination[l][side]->GetParameter(0);
meanB += m_fLamination[l][side]->GetParameter(1);
meanC += m_fLamination[l][side]->GetParameter(2);
meanOffset += m_laminationOffset[l][side];
nGoodFits++;
}
if(nGoodFits == 0)
{
return Fun4AllReturnCodes::EVENT_OK;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

tmpLamFit leaks, and early return leaves m_bestRMatch[side] null — later dereferenced in End().

Two issues in doGlobalRMatching:

  1. Memory leak (line 826): tmpLamFit is created via Clone() but never freed. Add delete tmpLamFit; before each return.

  2. Null-pointer crash (line 853-856): When nGoodFits == 0, the function returns EVENT_OK without allocating m_bestRMatch[side]. Back in End() at line 1191, m_bestRMatch[s]->Write() is called unconditionally, resulting in a null dereference. This will crash whenever field is on and all lamination fits are bad (e.g., low statistics run).

Suggested fix
     if(nGoodFits == 0)
     {
+      delete tmpLamFit;
       return Fun4AllReturnCodes::EVENT_OK;
     }

And at the end of the function, before the final return:

+  delete tmpLamFit;
   return Fun4AllReturnCodes::EVENT_OK;
 }

And guard the write in End():

-    m_bestRMatch[s]->Write();
+    if(m_bestRMatch[s]) { m_bestRMatch[s]->Write(); }

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
offline/packages/tpccalib/TpcLaminationFitting.cc (2)

988-993: ⚠️ Potential issue | 🔴 Critical

Division by zero when scalers[0][2] is zero.

scalers is zero-initialized. If the GL1 database returns no row with index == 0, or if the raw scaler count for that channel is genuinely zero, line 991 performs scalers[3][2] / scalers[0][2] → IEEE 754 infinity (or NaN if both are zero). That m_ZDC_coincidence then propagates into fitLaminations as the ZDC rate used to seed fit parameters (Af[s]->Eval(m_ZDC_coincidence), etc.), producing garbage fits or downstream failures.

Suggested guard
   delete resultSet;

+  if(scalers[0][2] == 0)
+  {
+    std::cerr << "GL1 raw scaler for index 0 is zero for run " << m_runnumber
+              << ". Cannot compute ZDC coincidence rate, aborting." << std::endl;
+    return Fun4AllReturnCodes::ABORTRUN;
+  }
+
   m_ZDC_coincidence = (1.0*scalers[3][2]/scalers[0][2])/(106e-9);

557-574: ⚠️ Potential issue | 🟠 Major

Leaked TGraph and TF1 objects inside the 18 × 2 lamination loop.

gr, proj (line 557–558), fitSeed (line 571), and localFit (line 574) are heap-allocated every iteration but never freed. Over one fitLaminations call that is 36 × 4 = 144 leaked objects. TGraph is not owned by ROOT's directory system, so these genuinely accumulate. Af[2] and Bf[2] (lines 479–480) also leak.

Add cleanup at loop end and function exit
       m_distanceToFit[l][s] = distToFunc / nBinsUsed;
       m_nBinsFit[l][s] = nBinsUsed;
       if(c>0) m_fitRMSE[l][s] = sqrt(wc / c);
       else m_fitRMSE[l][s] = -999;
       if (nBinsUsed < 10 || distToFunc / nBinsUsed > 1.0 || nBinsUsed_R_lt_45 < 5)
       {
         m_laminationGoodFit[l][s] = false;
       }
       else
       {
         m_laminationGoodFit[l][s] = true;
       }
+
+      delete gr;
+      delete proj;
+      delete fitSeed;
+      delete localFit;
     }
   }

+  delete Af[0];
+  delete Af[1];
+  delete Bf[0];
+  delete Bf[1];
+
   return Fun4AllReturnCodes::EVENT_OK;
 }

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
offline/packages/tpccalib/TpcLaminationFitting.cc (1)

997-997: ⚠️ Potential issue | 🟠 Major

Division by zero when scaler data is missing or the clock scaler is zero.

scalers is zero-initialized. If the DB query returns no rows (e.g., run not yet in the scaler table), or if scalers[0][2] (raw clock scaler) happens to be zero, this line produces inf or NaN. That value then seeds the lamination fit parameters via Af[s]->Eval(m_ZDC_coincidence), silently corrupting all fits for the run.

Suggested guard
+  if(scalers[0][2] == 0)
+  {
+    std::cerr << "TpcLaminationFitting::End - raw clock scaler is zero for run "
+              << m_runnumber << ", cannot compute ZDC rate. Aborting." << std::endl;
+    return Fun4AllReturnCodes::ABORTRUN;
+  }
   m_ZDC_coincidence = (1.0*scalers[3][2]/scalers[0][2])/(106e-9);

@sphenix-jenkins-ci
Copy link
Copy Markdown

Build & test report

Report for commit 5a3e1cdeba1b90900e461d50e1d2406118d38aa6:
Jenkins on fire


Automatically generated by sPHENIX Jenkins continuous integration
sPHENIX             jenkins.io

@sphenix-jenkins-ci
Copy link
Copy Markdown

Build & test report

Report for commit dfe01e0187ff9d1492c33ac0890b8e81b328ad7e:
Jenkins on fire


Automatically generated by sPHENIX Jenkins continuous integration
sPHENIX             jenkins.io

@sphenix-jenkins-ci
Copy link
Copy Markdown

Build & test report

Report for commit 29702424b82425068a93f50429329862be50cce2:
Jenkins on fire


Automatically generated by sPHENIX Jenkins continuous integration
sPHENIX             jenkins.io

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
offline/packages/tpccalib/TpcLaminationFitting.cc (2)

557-636: ⚠️ Potential issue | 🟠 Major

Memory leak: four ROOT objects allocated per lamination iteration are never freed.

gr, proj, fitSeed, and localFit are new'd each iteration of the (s, l) double loop (36 iterations) but none are deleted. Over a long-running job this is 144 leaked ROOT objects per call to fitLaminations().

Proposed fix — delete at end of inner loop body
       m_distanceToFit[l][s] = distToFunc / nBinsUsed;
       m_nBinsFit[l][s] = nBinsUsed;
       ...
+      delete gr;
+      delete proj;
+      delete fitSeed;
+      delete localFit;
     }
   }

Add these before the closing brace of the for (int l …) loop (after the good-fit decision around line 708).


995-998: ⚠️ Potential issue | 🔴 Critical

Division by zero if scalers[0][2] is zero.

If the GL1 scaler index 0 has a raw count of zero (e.g., very short or empty run), scalers[0][2] is 0 and line 997 divides by it, producing inf or nan for m_ZDC_coincidence. This value then seeds the fit parameters via Af[s]->Eval(m_ZDC_coincidence) / Bf[s]->Eval(…), corrupting all lamination fits for the run.

Proposed guard
   delete resultSet;
 
+  if (scalers[0][2] == 0)
+  {
+    std::cerr << "TpcLaminationFitting: GL1 raw scaler[0] is zero for run " << m_runnumber << ", cannot compute ZDC rate" << std::endl;
+    return Fun4AllReturnCodes::ABORTRUN;
+  }
   m_ZDC_coincidence = (1.0*scalers[3][2]/scalers[0][2])/(106e-9);

Comment on lines 694 to +701
}

m_distanceToFit[l][s] = distToFunc / nBinsUsed;
m_nBinsFit[l][s] = nBinsUsed;
if (nBinsUsed < 10 || distToFunc / nBinsUsed > 1.0)
if(c>0) { m_fitRMSE[l][s] = sqrt(wc / c);
} else { m_fitRMSE[l][s] = -999;
}
if (nBinsUsed < 10 || distToFunc / nBinsUsed > 1.0 || nBinsUsed_R_lt_45 < 5)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Division by zero when nBinsUsed == 0.

If no bin along the fit passes the content threshold, nBinsUsed stays at 0 and line 696 divides by it. The same zero value feeds the comparison on line 701 (distToFunc / nBinsUsed > 1.0). This is undefined behavior and will produce nan/inf that propagates into m_distanceToFit.

Proposed fix
+      if (nBinsUsed == 0)
+      {
+        m_distanceToFit[l][s] = -999;
+        m_nBinsFit[l][s] = 0;
+        m_fitRMSE[l][s] = -999;
+        m_laminationGoodFit[l][s] = false;
+        continue;
+      }
       m_distanceToFit[l][s] = distToFunc / nBinsUsed;
       m_nBinsFit[l][s] = nBinsUsed;

@sphenix-jenkins-ci
Copy link
Copy Markdown

Build & test report

Report for commit 54c8a9eda7c85ed830305b4448cb65efa73ff376:
Jenkins passed


Automatically generated by sPHENIX Jenkins continuous integration
sPHENIX             jenkins.io

@sphenix-jenkins-ci
Copy link
Copy Markdown

Build & test report

Report for commit 4dfbee8874259f8c65b43ada8fc5e678d96604a4:
Jenkins passed


Automatically generated by sPHENIX Jenkins continuous integration
sPHENIX             jenkins.io

@sphenix-jenkins-ci
Copy link
Copy Markdown

Build & test report

Report for commit 00ecb7f952ed535bec20cb1cdde6f8807bfd790d:
Jenkins passed


Automatically generated by sPHENIX Jenkins continuous integration
sPHENIX             jenkins.io

@osbornjd
Copy link
Copy Markdown
Contributor

osbornjd commented Feb 9, 2026

The previous commits are clang-tidy fixes, merging this now so that we can get it into the new build and start working with it

1 similar comment
@osbornjd
Copy link
Copy Markdown
Contributor

osbornjd commented Feb 9, 2026

The previous commits are clang-tidy fixes, merging this now so that we can get it into the new build and start working with it

@osbornjd osbornjd merged commit d6f29e5 into sPHENIX-Collaboration:master Feb 9, 2026
3 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants