Repository: SND-LHC/sndsw
Pull request #256: add new function getSciFiHitsMeanPerStation to sndSciFiTools.*
Author: login: schuetha
Created: 2025-03-14T08:37:49Z | Merged: 2025-05-19T09:55:47Z

Add new function `getSciFiHitsMeanPerStation` to sndSciFiTools.h and sndSciFiTools.cxx


Diff:
diff --git a/analysis/tools/sndSciFiTools.cxx b/analysis/tools/sndSciFiTools.cxx
index e9452923d1..21b005ed81 100644
--- a/analysis/tools/sndSciFiTools.cxx
+++ b/analysis/tools/sndSciFiTools.cxx
@@ -541,3 +541,67 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
 
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
+
+double computeMean(const std::vector<double>& values)
+{
+    double sum = std::accumulate(values.begin(), values.end(), 0.0);
+    double mean = sum / values.size();
+
+    return mean;
+}
+
+std::pair<double, double>
+snd::analysis_tools::find_centre_of_gravity_per_station(const TClonesArray* digiHits, int station)
+{
+    if (!digiHits) 
+    {
+         LOG(ERROR) << "Error: digiHits is null in find_centre_of_gravity_per_station";
+    }
+
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+    TVector3 A, B;
+    
+    for (auto* obj : *digiHits)
+    {
+        auto* hit = dynamic_cast<sndScifiHit*>(obj);
+
+        if (!hit || !hit->isValid()) 
+        {
+            continue;
+        }
+
+        if (hit->GetStation() != station) 
+        {
+            continue;
+        }
+
+        hit->GetSiPMPosition(hit->GetDetectorID(), A, B);
+
+        if (hit->isVertical())
+        {
+            x_positions.push_back((A.X() + B.X()) * 0.5);
+        }
+
+        else
+        {
+            y_positions.push_back((A.Y() + B.Y()) * 0.5);
+        }     
+    }
+    
+    if ( x_positions.empty() ) 
+    {
+      LOG(ERROR) << "Error: No hits enter.";
+    }
+
+    double meanX = computeMean(x_positions);
+
+    if ( y_positions.empty() ) 
+    {
+      LOG(ERROR) << "Error: No hits enter.";
+    }
+
+    double meanY = computeMean(y_positions);
+
+    return {meanX, meanY};
+}
\ No newline at end of file
diff --git a/analysis/tools/sndSciFiTools.h b/analysis/tools/sndSciFiTools.h
index d8559fbe32..7811523ac6 100644
--- a/analysis/tools/sndSciFiTools.h
+++ b/analysis/tools/sndSciFiTools.h
@@ -73,8 +73,10 @@ namespace snd {
     // selection to be made relying on a single orientation, then "orientation" can be provided
     // in the for of 0 or 1, for the horizontal and vertical orientations, respectively
     int showerInteractionWall(const TClonesArray &digiHits, const std::map<std::string, float> &selection_parameters, int method=0, std::string setup="TI18");
-    //Foregoing the selection_parameters option runs with the default values
+    // Foregoing the selection_parameters option runs with the default values
     int showerInteractionWall(const TClonesArray &digiHits, int method=0, std::string setup="TI18");
 
+    // Find the Center of Particle Showering on the SciFi plane
+    std::pair<double, double> find_centre_of_gravity_per_station(const TClonesArray* digiHits, int station);
   }
 }


Reviews:
user: login: olantwin
body: Looks like a great start, but clearly there are a few things missing or to be addressed.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-14T10:03:49Z
user: login: olantwin
body: OK, now the code is complete and looks like it should work.

Still a few nitpicks, but (and maybe a missing semicolon? does it compile?) but basically OK.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-17T09:53:30Z
user: login: olantwin
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-17T10:03:18Z
user: login: schuetha
body: @olantwin
Hi Oliver, I’ve updated the code based on your suggestions in the comments. I’m now curious about how I can test my code to check if it works correctly. I’m not very experienced with C++, so any guidance would be really helpful. Thanks you in advance!
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-17T10:10:42Z
user: login: olantwin
body: Apart from some minor formatting issues clang-format can take care of, this looks good from my side.

I still don't like the massive, named lambda.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-21T16:44:16Z
user: login: olantwin
body: I'm not quite sure whether I understand the computeMean function properly, it seems to just bin the values and return the centre of the most populated bin.

If this is the desired behaviour, we should rename the function to reflect what it does (and maybe use e.g. TH1F to do the histogramming etc. instead of reimplementing it).

If it isn't, we should actually compute the mean.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-24T11:12:06Z
user: login: siilieva
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T09:07:43Z
user: login: schuetha
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-31T06:40:17Z
user: login: schuetha
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-31T06:51:56Z
user: login: olantwin
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-31T07:06:53Z
user: login: siilieva
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-31T07:31:09Z
user: login: siilieva
body: left two comments - otherwise looks good to me
state: DISMISSED
author_association: NONE
submitted_at: 2025-05-02T13:07:04Z
user: login: olantwin
body: Looks generally fine.

Some nitpicks, and one imho significant issue: mean({}) != 0.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-05-05T05:51:25Z
user: login: siilieva
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-05T07:27:37Z
user: login: olantwin
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-05T08:07:01Z
user: login: schuetha
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-08T06:15:40Z
user: login: olantwin
body: Looks good to me now.

@siilieva 
state: APPROVED
author_association: NONE
submitted_at: 2025-05-15T12:26:22Z
user: login: siilieva
state: APPROVED
author_association: NONE
submitted_at: 2025-05-19T09:47:58Z

Review comments:
diff_hunk: @@ -541,3 +541,67 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
 
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
+
+std::pair<double, double> getSciFiHitsMeanPerStation(const TClonesArray* digiHits, int station) 
+{
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+    double bin_width = 0.025; // Bin width in cm
+
+    if (!digiHits) 
+    {
+        std::cerr << "Error: digiHits is null in getSciFiHitsMeanPerStation" << std::endl;
+        return std::make_pair(0.0, 0.0);
+    }
+
+    int nHits = digiHits->GetEntries();
+    TVector3 A, B;
+
+    for (int i = 0; i < nHits; ++i) 
+    {
+        auto hit = dynamic_cast<sndScifiHit*>(digiHits->At(i));
+        if (!hit || !hit->isValid()) continue;
+        if (hit->GetStation() != station) continue;
+
+        int detID = hit->GetDetectorID();
+        
+        hit->GetSiPMPosition(detID, A, B); // A is left/top and B is right/bottom.
+        
+        if (hit->isVertical()) 
+        {
+            double avg_x = (A.X() + B.X()) / 2.0;
+            x_positions.push_back(avg_x);
+        } 
+        
+        else
+        {
+            double avg_y = (A.Y() + B.Y()) / 2.0;
+            y_positions.push_back(avg_y);
+        }
+    }
+
+    auto computeMode = [bin_width](const std::vector<double>& values) -> double {
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: Because this is a named function, and quite long, it should be a regular function.

It's also unused?
created_at: 2025-03-14T09:03:20Z
author_association: NONE
diff_hunk: @@ -541,3 +541,67 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
 
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
+
+std::pair<double, double> getSciFiHitsMeanPerStation(const TClonesArray* digiHits, int station) 
+{
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+    double bin_width = 0.025; // Bin width in cm
+
+    if (!digiHits) 
+    {
+        std::cerr << "Error: digiHits is null in getSciFiHitsMeanPerStation" << std::endl;
+        return std::make_pair(0.0, 0.0);
+    }
+
+    int nHits = digiHits->GetEntries();
+    TVector3 A, B;
+
+    for (int i = 0; i < nHits; ++i) 
+    {
+        auto hit = dynamic_cast<sndScifiHit*>(digiHits->At(i));
+        if (!hit || !hit->isValid()) continue;
+        if (hit->GetStation() != station) continue;
+
+        int detID = hit->GetDetectorID();
+        
+        hit->GetSiPMPosition(detID, A, B); // A is left/top and B is right/bottom.
+        
+        if (hit->isVertical()) 
+        {
+            double avg_x = (A.X() + B.X()) / 2.0;
+            x_positions.push_back(avg_x);
+        } 
+        
+        else
+        {
+            double avg_y = (A.Y() + B.Y()) / 2.0;
+            y_positions.push_back(avg_y);
+        }
+    }
+
+    auto computeMode = [bin_width](const std::vector<double>& values) -> double {
+    if (values.empty()) return 0.0;
+
+    double min_val = *std::min_element(values.begin(), values.end());
+    double max_val = *std::max_element(values.begin(), values.end());
+
+    double start = static_cast<double>(static_cast<int>(min_val - 1));
+    double end   = static_cast<double>(static_cast<int>(max_val + 1));
+
+    int nbins = static_cast<int>(std::ceil((end - start) / bin_width));
+    if(nbins <= 0) return 0.0;
+
+    std::vector<int> counts(nbins, 0);
+    for (double value : values) 
+    {
+         int bin_index = static_cast<int>((value - start) / bin_width);
+         if (bin_index >= 0 && bin_index < nbins)
+             counts[bin_index]++;
+    }
+
+    int max_index = std::distance(counts.begin(), std::max_element(counts.begin(), counts.end()));
+
+    return start + (max_index + 0.5) * bin_width;
+};
+}
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: The function doesn't actually seem to return anything, when no errors occur.
created_at: 2025-03-14T09:28:06Z
author_association: NONE
diff_hunk: @@ -541,3 +541,67 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
 
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
+
+std::pair<double, double> getSciFiHitsMeanPerStation(const TClonesArray* digiHits, int station) 
+{
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+    double bin_width = 0.025; // Bin width in cm
+
+    if (!digiHits) 
+    {
+        std::cerr << "Error: digiHits is null in getSciFiHitsMeanPerStation" << std::endl;
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: ```suggestion
        LOG(ERROR) << "Error: digiHits is null in getSciFiHitsMeanPerStation";
```
created_at: 2025-03-14T09:28:53Z
author_association: NONE
diff_hunk: @@ -541,3 +541,67 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
 
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
+
+std::pair<double, double> getSciFiHitsMeanPerStation(const TClonesArray* digiHits, int station) 
+{
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+    double bin_width = 0.025; // Bin width in cm
+
+    if (!digiHits) 
+    {
+        std::cerr << "Error: digiHits is null in getSciFiHitsMeanPerStation" << std::endl;
+        return std::make_pair(0.0, 0.0);
+    }
+
+    int nHits = digiHits->GetEntries();
+    TVector3 A, B;
+
+    for (int i = 0; i < nHits; ++i) 
+    {
+        auto hit = dynamic_cast<sndScifiHit*>(digiHits->At(i));
+        if (!hit || !hit->isValid()) continue;
+        if (hit->GetStation() != station) continue;
+
+        int detID = hit->GetDetectorID();
+        
+        hit->GetSiPMPosition(detID, A, B); // A is left/top and B is right/bottom.
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: Is this guaranteed to set A, B to something sensible?

If not, there could be a risk that A, B are taken from the previous hit.
created_at: 2025-03-14T09:29:55Z
author_association: NONE
diff_hunk: @@ -541,3 +541,67 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
 
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
+
+std::pair<double, double> getSciFiHitsMeanPerStation(const TClonesArray* digiHits, int station) 
+{
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+    double bin_width = 0.025; // Bin width in cm
+
+    if (!digiHits) 
+    {
+        std::cerr << "Error: digiHits is null in getSciFiHitsMeanPerStation" << std::endl;
+        return std::make_pair(0.0, 0.0);
+    }
+
+    int nHits = digiHits->GetEntries();
+    TVector3 A, B;
+
+    for (int i = 0; i < nHits; ++i) 
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: As you don't need the index, it would be better to use a range-for.
created_at: 2025-03-14T09:31:26Z
author_association: NONE
diff_hunk: @@ -541,3 +541,67 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
 
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
+
+std::pair<double, double> getSciFiHitsMeanPerStation(const TClonesArray* digiHits, int station) 
+{
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+    double bin_width = 0.025; // Bin width in cm
+
+    if (!digiHits) 
+    {
+        std::cerr << "Error: digiHits is null in getSciFiHitsMeanPerStation" << std::endl;
+        return std::make_pair(0.0, 0.0);
+    }
+
+    int nHits = digiHits->GetEntries();
+    TVector3 A, B;
+
+    for (int i = 0; i < nHits; ++i) 
+    {
+        auto hit = dynamic_cast<sndScifiHit*>(digiHits->At(i));
+        if (!hit || !hit->isValid()) continue;
+        if (hit->GetStation() != station) continue;
+
+        int detID = hit->GetDetectorID();
+        
+        hit->GetSiPMPosition(detID, A, B); // A is left/top and B is right/bottom.
+        
+        if (hit->isVertical()) 
+        {
+            double avg_x = (A.X() + B.X()) / 2.0;
+            x_positions.push_back(avg_x);
+        } 
+        
+        else
+        {
+            double avg_y = (A.Y() + B.Y()) / 2.0;
+            y_positions.push_back(avg_y);
+        }
+    }
+
+    auto computeMode = [bin_width](const std::vector<double>& values) -> double {
+    if (values.empty()) return 0.0;
+
+    double min_val = *std::min_element(values.begin(), values.end());
+    double max_val = *std::max_element(values.begin(), values.end());
+
+    double start = static_cast<double>(static_cast<int>(min_val - 1));
+    double end   = static_cast<double>(static_cast<int>(max_val + 1));
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: Why this casting back and forth?

Even if the conversions are needed, for numerical types casts seem to be the wrong way to do it.
created_at: 2025-03-14T09:34:56Z
author_association: NONE
diff_hunk: @@ -541,3 +541,67 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
 
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
+
+std::pair<double, double> getSciFiHitsMeanPerStation(const TClonesArray* digiHits, int station) 
+{
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+    double bin_width = 0.025; // Bin width in cm
+
+    if (!digiHits) 
+    {
+        std::cerr << "Error: digiHits is null in getSciFiHitsMeanPerStation" << std::endl;
+        return std::make_pair(0.0, 0.0);
+    }
+
+    int nHits = digiHits->GetEntries();
+    TVector3 A, B;
+
+    for (int i = 0; i < nHits; ++i) 
+    {
+        auto hit = dynamic_cast<sndScifiHit*>(digiHits->At(i));
+        if (!hit || !hit->isValid()) continue;
+        if (hit->GetStation() != station) continue;
+
+        int detID = hit->GetDetectorID();
+        
+        hit->GetSiPMPosition(detID, A, B); // A is left/top and B is right/bottom.
+        
+        if (hit->isVertical()) 
+        {
+            double avg_x = (A.X() + B.X()) / 2.0;
+            x_positions.push_back(avg_x);
+        } 
+        
+        else
+        {
+            double avg_y = (A.Y() + B.Y()) / 2.0;
+            y_positions.push_back(avg_y);
+        }
+    }
+
+    auto computeMode = [bin_width](const std::vector<double>& values) -> double {
+    if (values.empty()) return 0.0;
+
+    double min_val = *std::min_element(values.begin(), values.end());
+    double max_val = *std::max_element(values.begin(), values.end());
+
+    double start = static_cast<double>(static_cast<int>(min_val - 1));
+    double end   = static_cast<double>(static_cast<int>(max_val + 1));
+
+    int nbins = static_cast<int>(std::ceil((end - start) / bin_width));
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: why cast instead of conversion
created_at: 2025-03-14T09:35:24Z
author_association: NONE
diff_hunk: @@ -73,8 +73,10 @@ namespace snd {
     // selection to be made relying on a single orientation, then "orientation" can be provided
     // in the for of 0 or 1, for the horizontal and vertical orientations, respectively
     int showerInteractionWall(const TClonesArray &digiHits, const std::map<std::string, float> &selection_parameters, int method=0, std::string setup="TI18");
-    //Foregoing the selection_parameters option runs with the default values
+    // Foregoing the selection_parameters option runs with the default values
     int showerInteractionWall(const TClonesArray &digiHits, int method=0, std::string setup="TI18");
 
+    // Find the Center of Particle Showering on the SciFi plane
+    std::pair<double, double> getSciFiHitsMeanPerStation(const TClonesArray* digiHits, const sndScifi* Scifi);
path: analysis/tools/sndSciFiTools.h
user: login: olantwin
body: Maybe `find_centre_of_gravity_per_station` or something like that might be a more descriptive name?
created_at: 2025-03-14T10:03:18Z
author_association: NONE
diff_hunk: @@ -541,3 +541,67 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
 
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
+
+std::pair<double, double> getSciFiHitsMeanPerStation(const TClonesArray* digiHits, int station) 
+{
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+    double bin_width = 0.025; // Bin width in cm
+
+    if (!digiHits) 
+    {
+        std::cerr << "Error: digiHits is null in getSciFiHitsMeanPerStation" << std::endl;
+        return std::make_pair(0.0, 0.0);
+    }
+
+    int nHits = digiHits->GetEntries();
+    TVector3 A, B;
+
+    for (int i = 0; i < nHits; ++i) 
+    {
+        auto hit = dynamic_cast<sndScifiHit*>(digiHits->At(i));
+        if (!hit || !hit->isValid()) continue;
+        if (hit->GetStation() != station) continue;
+
+        int detID = hit->GetDetectorID();
+        
+        hit->GetSiPMPosition(detID, A, B); // A is left/top and B is right/bottom.
+        
+        if (hit->isVertical()) 
+        {
+            double avg_x = (A.X() + B.X()) / 2.0;
+            x_positions.push_back(avg_x);
+        } 
+        
+        else
+        {
+            double avg_y = (A.Y() + B.Y()) / 2.0;
+            y_positions.push_back(avg_y);
+        }
+    }
+
+    auto computeMode = [bin_width](const std::vector<double>& values) -> double {
+    if (values.empty()) return 0.0;
+
+    double min_val = *std::min_element(values.begin(), values.end());
+    double max_val = *std::max_element(values.begin(), values.end());
+
+    double start = static_cast<double>(static_cast<int>(min_val - 1));
+    double end   = static_cast<double>(static_cast<int>(max_val + 1));
+
+    int nbins = static_cast<int>(std::ceil((end - start) / bin_width));
+    if(nbins <= 0) return 0.0;
+
+    std::vector<int> counts(nbins, 0);
+    for (double value : values) 
+    {
+         int bin_index = static_cast<int>((value - start) / bin_width);
+         if (bin_index >= 0 && bin_index < nbins)
+             counts[bin_index]++;
+    }
+
+    int max_index = std::distance(counts.begin(), std::max_element(counts.begin(), counts.end()));
+
+    return start + (max_index + 0.5) * bin_width;
+};
+}
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: Did you maybe forget to commit something?
created_at: 2025-03-14T10:03:20Z
author_association: NONE
diff_hunk: @@ -542,66 +542,78 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
 
-std::pair<double, double> getSciFiHitsMeanPerStation(const TClonesArray* digiHits, int station) 
+std::pair<double, double> getSciFiHitsMeanPerStation(const TClonesArray* digiHits, int station)
 {
-    std::vector<double> x_positions;
-    std::vector<double> y_positions;
-    double bin_width = 0.025; // Bin width in cm
-
     if (!digiHits) 
     {
-        std::cerr << "Error: digiHits is null in getSciFiHitsMeanPerStation" << std::endl;
-        return std::make_pair(0.0, 0.0);
+        LOG(ERROR) << "Error: digiHits is null in getSciFiHitsMeanPerStation";
+        return {0.0, 0.0};
     }
 
-    int nHits = digiHits->GetEntries();
-    TVector3 A, B;
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+    double bin_width = 0.025
 
-    for (int i = 0; i < nHits; ++i) 
+    for (auto* obj : *digiHits) 
     {
-        auto hit = dynamic_cast<sndScifiHit*>(digiHits->At(i));
-        if (!hit || !hit->isValid()) continue;
-        if (hit->GetStation() != station) continue;
+        auto* hit = dynamic_cast<sndScifiHit*>(obj);
 
-        int detID = hit->GetDetectorID();
-        
-        hit->GetSiPMPosition(detID, A, B); // A is left/top and B is right/bottom.
-        
-        if (hit->isVertical()) 
+        if (!hit || !hit->isValid()) 
         {
-            double avg_x = (A.X() + B.X()) / 2.0;
-            x_positions.push_back(avg_x);
-        } 
-        
+            continue;
+        }
+
+        if (hit->GetStation() != station) 
+        {
+            continue;
+        }
+
+        TVector3 A, B;
+        hit->GetSiPMPosition(hit->GetDetectorID(), A, B);
+
+        if (hit->isVertical())
+        {
+            x_positions.push_back((A.X() + B.X()) * 0.5);
+        }
+
         else
         {
-            double avg_y = (A.Y() + B.Y()) / 2.0;
-            y_positions.push_back(avg_y);
+            y_positions.push_back((A.Y() + B.Y()) * 0.5);
         }
+        
     }
 
     auto computeMode = [bin_width](const std::vector<double>& values) -> double {
     if (values.empty()) return 0.0;
 
-    double min_val = *std::min_element(values.begin(), values.end());
-    double max_val = *std::max_element(values.begin(), values.end());
+      double min_val = *std::min_element(values.begin(), values.end());
+      double max_val = *std::max_element(values.begin(), values.end());
 
-    double start = static_cast<double>(static_cast<int>(min_val - 1));
-    double end   = static_cast<double>(static_cast<int>(max_val + 1));
+      double start = static_cast<double>(static_cast<int>(min_val - 1));
+      double end   = static_cast<double>(static_cast<int>(max_val + 1));
 
-    int nbins = static_cast<int>(std::ceil((end - start) / bin_width));
-    if(nbins <= 0) return 0.0;
+      int nbins = static_cast<int>(std::ceil((end - start) / bin_width));
+      if(nbins <= 0) return 0.0;
 
-    std::vector<int> counts(nbins, 0);
-    for (double value : values) 
-    {
+      std::vector<int> counts(nbins, 0);
+
+      for (double value : values) 
+      {
          int bin_index = static_cast<int>((value - start) / bin_width);
          if (bin_index >= 0 && bin_index < nbins)
-             counts[bin_index]++;
-    }
+            counts[bin_index]++;
+      }
+      
+      auto max_it = std::max_element(counts.begin(), counts.end());
+      int max_index = std::distance(counts.begin(), max_it);
 
-    int max_index = std::distance(counts.begin(), std::max_element(counts.begin(), counts.end()));
+      double bin_center = start + (max_index + 0.5) * bin_width;
+      return bin_center;
+    
+    }
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: Semicolon missing here?
created_at: 2025-03-17T09:50:26Z
author_association: NONE
diff_hunk: @@ -542,66 +542,78 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
 
-std::pair<double, double> getSciFiHitsMeanPerStation(const TClonesArray* digiHits, int station) 
+std::pair<double, double> getSciFiHitsMeanPerStation(const TClonesArray* digiHits, int station)
 {
-    std::vector<double> x_positions;
-    std::vector<double> y_positions;
-    double bin_width = 0.025; // Bin width in cm
-
     if (!digiHits) 
     {
-        std::cerr << "Error: digiHits is null in getSciFiHitsMeanPerStation" << std::endl;
-        return std::make_pair(0.0, 0.0);
+        LOG(ERROR) << "Error: digiHits is null in getSciFiHitsMeanPerStation";
+        return {0.0, 0.0};
     }
 
-    int nHits = digiHits->GetEntries();
-    TVector3 A, B;
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+    double bin_width = 0.025
 
-    for (int i = 0; i < nHits; ++i) 
+    for (auto* obj : *digiHits) 
     {
-        auto hit = dynamic_cast<sndScifiHit*>(digiHits->At(i));
-        if (!hit || !hit->isValid()) continue;
-        if (hit->GetStation() != station) continue;
+        auto* hit = dynamic_cast<sndScifiHit*>(obj);
 
-        int detID = hit->GetDetectorID();
-        
-        hit->GetSiPMPosition(detID, A, B); // A is left/top and B is right/bottom.
-        
-        if (hit->isVertical()) 
+        if (!hit || !hit->isValid()) 
         {
-            double avg_x = (A.X() + B.X()) / 2.0;
-            x_positions.push_back(avg_x);
-        } 
-        
+            continue;
+        }
+
+        if (hit->GetStation() != station) 
+        {
+            continue;
+        }
+
+        TVector3 A, B;
+        hit->GetSiPMPosition(hit->GetDetectorID(), A, B);
+
+        if (hit->isVertical())
+        {
+            x_positions.push_back((A.X() + B.X()) * 0.5);
+        }
+
         else
         {
-            double avg_y = (A.Y() + B.Y()) / 2.0;
-            y_positions.push_back(avg_y);
+            y_positions.push_back((A.Y() + B.Y()) * 0.5);
         }
+        
     }
 
     auto computeMode = [bin_width](const std::vector<double>& values) -> double {
     if (values.empty()) return 0.0;
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: This should be indented at the same depth as the following lined.

I'd prefer once statement per line.
created_at: 2025-03-17T09:51:32Z
author_association: NONE
diff_hunk: @@ -541,3 +541,67 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
 
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
+
+std::pair<double, double> getSciFiHitsMeanPerStation(const TClonesArray* digiHits, int station) 
+{
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+    double bin_width = 0.025; // Bin width in cm
+
+    if (!digiHits) 
+    {
+        std::cerr << "Error: digiHits is null in getSciFiHitsMeanPerStation" << std::endl;
+        return std::make_pair(0.0, 0.0);
+    }
+
+    int nHits = digiHits->GetEntries();
+    TVector3 A, B;
+
+    for (int i = 0; i < nHits; ++i) 
+    {
+        auto hit = dynamic_cast<sndScifiHit*>(digiHits->At(i));
+        if (!hit || !hit->isValid()) continue;
+        if (hit->GetStation() != station) continue;
+
+        int detID = hit->GetDetectorID();
+        
+        hit->GetSiPMPosition(detID, A, B); // A is left/top and B is right/bottom.
+        
+        if (hit->isVertical()) 
+        {
+            double avg_x = (A.X() + B.X()) / 2.0;
+            x_positions.push_back(avg_x);
+        } 
+        
+        else
+        {
+            double avg_y = (A.Y() + B.Y()) / 2.0;
+            y_positions.push_back(avg_y);
+        }
+    }
+
+    auto computeMode = [bin_width](const std::vector<double>& values) -> double {
+    if (values.empty()) return 0.0;
+
+    double min_val = *std::min_element(values.begin(), values.end());
+    double max_val = *std::max_element(values.begin(), values.end());
+
+    double start = static_cast<double>(static_cast<int>(min_val - 1));
+    double end   = static_cast<double>(static_cast<int>(max_val + 1));
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: Here none of the 4 casts seem necessary.
created_at: 2025-03-17T10:03:18Z
author_association: NONE
diff_hunk: @@ -541,3 +541,89 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
 
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
+
+std::pair<double, double>
+snd::analysis_tools::find_centre_of_gravity_per_station(const TClonesArray* digiHits, int station)
+{
+    if (!digiHits) 
+    {
+        LOG(ERROR) << "Error: digiHits is null in find_centre_of_gravity_per_station";
+        return {0.0, 0.0};
+    }
+
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+    double bin_width = 0.025;
+
+    for (auto* obj : *digiHits) 
+    {
+        auto* hit = dynamic_cast<sndScifiHit*>(obj);
+
+        if (!hit || !hit->isValid()) 
+        {
+            continue;
+        }
+
+        if (hit->GetStation() != station) 
+        {
+            continue;
+        }
+
+        TVector3 A, B;
+        hit->GetSiPMPosition(hit->GetDetectorID(), A, B);
+
+        if (hit->isVertical())
+        {
+            x_positions.push_back((A.X() + B.X()) * 0.5);
+        }
+
+        else
+        {
+            y_positions.push_back((A.Y() + B.Y()) * 0.5);
+        }
+      
+    }
+
+    auto computeMode = [bin_width](const std::vector<double>& values) -> double 
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: This is a fairly big function, it should not be a lambda.
created_at: 2025-03-21T16:43:28Z
author_association: NONE
diff_hunk: @@ -542,6 +542,39 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
 
+double computeMean(const std::vector<double>& values, double bin_width)
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: Could you please explain what this algorithm is doing?

The more I look at this, the more confused I get.
created_at: 2025-03-24T07:53:20Z
author_association: NONE
diff_hunk: @@ -542,6 +542,39 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
 
+double computeMean(const std::vector<double>& values, double bin_width)
+{
+    if (values.empty()) {
+        return 0.0;
+    }
+
+    double min_val = *std::min_element(values.begin(), values.end());
+    double max_val = *std::max_element(values.begin(), values.end());
+
+    double start = static_cast<double>(static_cast<int>(min_val - 1));
+    double end   = static_cast<double>(static_cast<int>(max_val + 1));
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: It might be clearer to truncate `min_val`/`max_val` here, instead of truncating `min_val + 1` and `max_val + 1`.
No need to cast back to double.
created_at: 2025-03-24T07:57:41Z
author_association: NONE
diff_hunk: @@ -542,6 +542,39 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
 
+double computeMean(const std::vector<double>& values, double bin_width)
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: It seems that all the truncating only makes sense if you rely on the bin_width to be integral, with a `bin_width == 1`
created_at: 2025-03-24T07:58:56Z
author_association: NONE
diff_hunk: @@ -542,6 +542,39 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
 
+double computeMean(const std::vector<double>& values, double bin_width)
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: Rather than calculating the mean, this seems to be histogramming the values and returning the centre of the bin with the most entries. Is that the intention?
created_at: 2025-03-24T11:09:55Z
author_association: NONE
diff_hunk: @@ -542,6 +542,39 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
 
+double computeMean(const std::vector<double>& values, double bin_width)
path: analysis/tools/sndSciFiTools.cxx
user: login: siilieva
body: @schuetha, one has to compute the mean without binning, so please remove it.
 In the sw we can work with any large-enough precision of the shower center. Sorting out the reasonable precision of that quantity can be left to the code calling the function.
created_at: 2025-03-26T09:07:43Z
author_association: NONE
diff_hunk: @@ -542,6 +542,39 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
 
+double computeMean(const std::vector<double>& values, double bin_width)
path: analysis/tools/sndSciFiTools.cxx
user: login: schuetha
body: Hi @olantwin , thank you very much for checking my PR thoroughly. Indeed, our initial intention was to compute the mean number of hits in each SciFi station to estimate the shower center. Based on our last discussion with @siilieva, we understood that the approach should involve 
- first binning the data with a bin size corresponding to the SciFi module (250 μm)
- identifying the bin with the largest number of entries
- and then using the center of that bin as the shower 
Sorry for not making it clear from the beginning. You can also refer to the slides I showed Simona during our last discussion for a better idea of what I am trying to implement in the sw (https://github.com/user-attachments/files/19530714/Center.of.Particle.Showering.on.the.SciFi.plane.pdf).

created_at: 2025-03-31T06:40:17Z
author_association: NONE
diff_hunk: @@ -542,6 +542,39 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
 
+double computeMean(const std::vector<double>& values, double bin_width)
path: analysis/tools/sndSciFiTools.cxx
user: login: schuetha
body: I'm going to update the computeMean function so that it calculates the mean position directly from the unbinned data. Do I understand correctly that this is what we need ? , @siilieva 
created_at: 2025-03-31T06:51:56Z
author_association: NONE
diff_hunk: @@ -542,6 +542,39 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
 
+double computeMean(const std::vector<double>& values, double bin_width)
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: There are (at least) three approaches that might work, and in the end we'll have to have a look how well they work in the analysis:
1. Use the bin with the most counts
2. Calculate the weighted average (binned mean)
3. Calculate the unbinned mean

They all calculate different things. As long as it's clear from the function name and comments/documentation what you're calculating, it's a question of physics, not code review.
created_at: 2025-03-31T07:06:53Z
author_association: NONE
diff_hunk: @@ -542,6 +542,39 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
 
+double computeMean(const std::vector<double>& values, double bin_width)
path: analysis/tools/sndSciFiTools.cxx
user: login: siilieva
body: > I'm going to update the computeMean function so that it calculates the mean position directly from the unbinned data. Do I understand correctly that this is what we need ? , @siilieva

yes, that is correct.
The binning is only needed in the end/during analysis, which in the case we discussed was when visualizing the result on a 2d plot.
created_at: 2025-03-31T07:31:09Z
author_association: NONE
diff_hunk: @@ -541,3 +542,66 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
 
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
+
+double computeMean(const std::vector<double>& values)
+{
+    if ( values.empty() ) 
+    {
+      return 0.0;
+    }
+
+    double sum = 0.0;
+    for ( double value : values ) 
+    {
+      sum += value;
+    }
+
+    return sum / values.size();
+}
+
+std::pair<double, double>
+snd::analysis_tools::find_centre_of_gravity_per_station(const TClonesArray* digiHits, int station)
+{
+    if (!digiHits) 
+    {
+         LOG(ERROR) << "Error: digiHits is null in find_centre_of_gravity_per_station";
+         return {0.0, 0.0};
+    }
+
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+
+    for ( auto* obj : *digiHits )
+    {
+        auto* hit = dynamic_cast<sndScifiHit*>(obj);
+
+        if (!hit || !hit->isValid()) 
+        {
+            continue;
+        }
+
+        if (hit->GetStation() != station) 
+        {
+            continue;
+        }
+
+        TVector3 A, B;
path: analysis/tools/sndSciFiTools.cxx
user: login: siilieva
body: the A, B declaration should  be moved outside the loop over digis (l574) to save time on mem allocation.
In shower events, one might have thousands of hits and this small speed loss might become noticeable. 
created_at: 2025-05-02T12:59:05Z
author_association: NONE
diff_hunk: @@ -8,6 +8,7 @@
 #include "TClonesArray.h"
 #include "sndScifiHit.h"
 #include "ROOT/TSeq.hxx"
+#include <numeric> // for std::accumulate
path: analysis/tools/sndSciFiTools.cxx
user: login: siilieva
body: does one need this lib in the end? Could it be that it is a leftover from previous versions?
created_at: 2025-05-02T13:00:56Z
author_association: NONE
diff_hunk: @@ -542,53 +543,35 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
 
-double computeMean(const std::vector<double>& values, double bin_width)
+double computeMean(const std::vector<double>& values)
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: I'd probably do something like instead, which is probably faster and more succinct:
```C++
double sum = std::accumulate(v.begin(), v.end(), 0.0);
double mean = sum / v.size();
```
created_at: 2025-05-05T05:40:47Z
author_association: NONE
diff_hunk: @@ -542,53 +543,35 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
 
-double computeMean(const std::vector<double>& values, double bin_width)
+double computeMean(const std::vector<double>& values)
 {
-    if (values.empty()) {
-        return 0.0;
-    }
-
-    double min_val = *std::min_element(values.begin(), values.end());
-    double max_val = *std::max_element(values.begin(), values.end());
-
-    double start = static_cast<double>(static_cast<int>(min_val - 1));
-    double end   = static_cast<double>(static_cast<int>(max_val + 1));
-
-    int nbins = static_cast<int>(std::ceil((end - start) / bin_width));
-    if (nbins <= 0) {
-        return 0.0;
+    if ( values.empty() ) 
+    {
+      return 0.0;
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: When the vector is empty, returning zero might be confusing, as the mean of an empty set doesn't really make sense. Might be better to raise an error in the calling code if there are no hits.
created_at: 2025-05-05T05:42:34Z
author_association: NONE
diff_hunk: @@ -8,6 +8,7 @@
 #include "TClonesArray.h"
 #include "sndScifiHit.h"
 #include "ROOT/TSeq.hxx"
+#include <numeric> // for std::accumulate
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: You mean the header?
I'd probably prefer using it over the current implementation, see my comment below.
created_at: 2025-05-05T05:43:30Z
author_association: NONE
diff_hunk: @@ -541,3 +542,66 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
 
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
+
+double computeMean(const std::vector<double>& values)
+{
+    if ( values.empty() ) 
+    {
+      return 0.0;
+    }
+
+    double sum = 0.0;
+    for ( double value : values ) 
+    {
+      sum += value;
+    }
+
+    return sum / values.size();
+}
+
+std::pair<double, double>
+snd::analysis_tools::find_centre_of_gravity_per_station(const TClonesArray* digiHits, int station)
+{
+    if (!digiHits) 
+    {
+         LOG(ERROR) << "Error: digiHits is null in find_centre_of_gravity_per_station";
+         return {0.0, 0.0};
+    }
+
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+
+    for ( auto* obj : *digiHits )
+    {
+        auto* hit = dynamic_cast<sndScifiHit*>(obj);
+
+        if (!hit || !hit->isValid()) 
+        {
+            continue;
+        }
+
+        if (hit->GetStation() != station) 
+        {
+            continue;
+        }
+
+        TVector3 A, B;
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: I'd expect this to be completely negligible compared to the calculations happening in GetSiPMPosition, which involve many expensive operations.
created_at: 2025-05-05T05:46:58Z
author_association: NONE
diff_hunk: @@ -541,3 +542,66 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
 
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
+
+double computeMean(const std::vector<double>& values)
+{
+    if ( values.empty() ) 
+    {
+      return 0.0;
+    }
+
+    double sum = 0.0;
+    for ( double value : values ) 
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: ```suggestion
    for (double value : values) 
```
created_at: 2025-05-05T05:47:43Z
author_association: NONE
diff_hunk: @@ -541,3 +542,66 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
 
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
+
+double computeMean(const std::vector<double>& values)
+{
+    if ( values.empty() ) 
+    {
+      return 0.0;
+    }
+
+    double sum = 0.0;
+    for ( double value : values ) 
+    {
+      sum += value;
+    }
+
+    return sum / values.size();
+}
+
+std::pair<double, double>
+snd::analysis_tools::find_centre_of_gravity_per_station(const TClonesArray* digiHits, int station)
+{
+    if (!digiHits) 
+    {
+         LOG(ERROR) << "Error: digiHits is null in find_centre_of_gravity_per_station";
+         return {0.0, 0.0};
+    }
+
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+
+    for ( auto* obj : *digiHits )
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: ```suggestion
    for (auto* obj : *digiHits)
```
created_at: 2025-05-05T05:48:10Z
author_association: NONE
diff_hunk: @@ -541,3 +542,66 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
 
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
+
+double computeMean(const std::vector<double>& values)
+{
+    if ( values.empty() ) 
+    {
+      return 0.0;
+    }
+
+    double sum = 0.0;
+    for ( double value : values ) 
+    {
+      sum += value;
+    }
+
+    return sum / values.size();
+}
+
+std::pair<double, double>
+snd::analysis_tools::find_centre_of_gravity_per_station(const TClonesArray* digiHits, int station)
+{
+    if (!digiHits) 
+    {
+         LOG(ERROR) << "Error: digiHits is null in find_centre_of_gravity_per_station";
+         return {0.0, 0.0};
+    }
+
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+
+    for ( auto* obj : *digiHits )
+    {
+        auto* hit = dynamic_cast<sndScifiHit*>(obj);
+
+        if (!hit || !hit->isValid()) 
+        {
+            continue;
+        }
+
+        if (hit->GetStation() != station) 
+        {
+            continue;
+        }
+
+        TVector3 A, B;
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: (the compiler might also do this automatically, as the vectors are never initialised and not used before being set in GetSiPMPosition)
created_at: 2025-05-05T05:49:18Z
author_association: NONE
diff_hunk: @@ -541,3 +542,66 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
 
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
+
+double computeMean(const std::vector<double>& values)
+{
+    if ( values.empty() ) 
+    {
+      return 0.0;
+    }
+
+    double sum = 0.0;
+    for ( double value : values ) 
+    {
+      sum += value;
+    }
+
+    return sum / values.size();
+}
+
+std::pair<double, double>
+snd::analysis_tools::find_centre_of_gravity_per_station(const TClonesArray* digiHits, int station)
+{
+    if (!digiHits) 
+    {
+         LOG(ERROR) << "Error: digiHits is null in find_centre_of_gravity_per_station";
+         return {0.0, 0.0};
+    }
+
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+
+    for ( auto* obj : *digiHits )
+    {
+        auto* hit = dynamic_cast<sndScifiHit*>(obj);
+
+        if (!hit || !hit->isValid()) 
+        {
+            continue;
+        }
+
+        if (hit->GetStation() != station) 
+        {
+            continue;
+        }
+
+        TVector3 A, B;
path: analysis/tools/sndSciFiTools.cxx
user: login: siilieva
body: it is still better style in my opinion to be declared outside
For the timing, with showers it is better to be conservative minimizing operations. Although compilers are smart, but again _style_
created_at: 2025-05-05T07:27:37Z
author_association: NONE
diff_hunk: @@ -541,3 +542,66 @@ int snd::analysis_tools::showerInteractionWall(const TClonesArray &digiHits, int
 
    return showerInteractionWall(digiHits, selection_parameters, method, setup);
 }
+
+double computeMean(const std::vector<double>& values)
+{
+    if ( values.empty() ) 
+    {
+      return 0.0;
+    }
+
+    double sum = 0.0;
+    for ( double value : values ) 
+    {
+      sum += value;
+    }
+
+    return sum / values.size();
+}
+
+std::pair<double, double>
+snd::analysis_tools::find_centre_of_gravity_per_station(const TClonesArray* digiHits, int station)
+{
+    if (!digiHits) 
+    {
+         LOG(ERROR) << "Error: digiHits is null in find_centre_of_gravity_per_station";
+         return {0.0, 0.0};
+    }
+
+    std::vector<double> x_positions;
+    std::vector<double> y_positions;
+
+    for ( auto* obj : *digiHits )
+    {
+        auto* hit = dynamic_cast<sndScifiHit*>(obj);
+
+        if (!hit || !hit->isValid()) 
+        {
+            continue;
+        }
+
+        if (hit->GetStation() != station) 
+        {
+            continue;
+        }
+
+        TVector3 A, B;
path: analysis/tools/sndSciFiTools.cxx
user: login: olantwin
body: I don't object to moving it out of the loop in this case (and you definitely have the last word in any case), *but* if we are seriously concerned about performance here, we should measure.

Because compilers are so smart (and sometimes *too smart*) gut feelings about what is faster can sometimes be quite wrong, and it's also possible to prevent the compiler from making certain optimisations when optimising code manually.
created_at: 2025-05-05T08:07:00Z
author_association: NONE
diff_hunk: @@ -8,6 +8,7 @@
 #include "TClonesArray.h"
 #include "sndScifiHit.h"
 #include "ROOT/TSeq.hxx"
+#include <numeric> // for std::accumulate
path: analysis/tools/sndSciFiTools.cxx
user: login: schuetha
body: @olantwin, @siilieva Actually this header already exists once above. So I just removed this duplicated one. 
created_at: 2025-05-08T06:15:39Z
author_association: NONE

PR comments:
user: login: olantwin
created_at: 2025-03-17T10:02:27Z
body: The static_cast conversions are fine. Maybe more explicit than necessary, but they don't cause any excess instructions to be emitted by the compiler.
author_association: NONE
user: login: olantwin
created_at: 2025-03-17T10:54:16Z
body: > @olantwin Hi Oliver, I’ve updated the code based on your suggestions in the comments. I’m now curious about how I can test my code to check if it works correctly. I’m not very experienced with C++, so any guidance would be really helpful. Thanks you in advance!

The basic tests would of course be whether it compiles and runs.

Then, it should be straightforward to test whether the function calculates what you expect it to calculate by comparing to the event displays for a few events.

As this function can't really affect anything else, there is probably no need to perform more general testing.
author_association: NONE
user: login: schuetha
created_at: 2025-04-28T13:48:41Z
body: Hi @siilieva and @olantwin , I’ve updated the code to calculate the mean position of hits at each station using the un-binned hits position (x and y coordinates) for each event. Could you help reviewing when you have a minute?

Thank you in advance !
author_association: NONE
user: login: olantwin
created_at: 2025-04-28T16:31:23Z
body: > Hi @siilieva and @olantwin , I’ve updated the code to calculate the mean position of hits at each station using the un-binned hits position (x and y coordinates) for each event. Could you help reviewing when you have a minute?
> 
> Thank you in advance !

Thanks, I'm on holiday until the 5th, I'll review the code when I'm back.
author_association: NONE
user: login: olantwin
created_at: 2025-05-07T10:11:11Z
body: Looks good, but the error checking should not happen in the function that calculates the mean but outside in the caller.

Please check that you really don't need to include the header.
author_association: NONE
user: login: olantwin
created_at: 2025-05-15T12:27:39Z
body: @schuetha Thanks for your patience and implementing all the feedback.
author_association: NONE
user: login: schuetha
created_at: 2025-05-16T07:26:34Z
body: > @schuetha Thanks for your patience and implementing all the feedback.
@olantwin, Actually I have to thank you for your patience on my poor coding skill and your guidance all the way up to now.

author_association: NONE
user: login: siilieva
created_at: 2025-05-19T09:47:02Z
body: ok, ok,  glad you're both happy with this PR!
now, I'll squash all commits to one and it is done
author_association: NONE