Skip to content

Added crystalID field to caloHitMC and changed LY for LYSO#1756

Merged
oksuzian merged 4 commits intoMu2e:mainfrom
bechenard:CaloLysoID
Mar 12, 2026
Merged

Added crystalID field to caloHitMC and changed LY for LYSO#1756
oksuzian merged 4 commits intoMu2e:mainfrom
bechenard:CaloLysoID

Conversation

@bechenard
Copy link
Contributor

No description provided.

@FNALbuild
Copy link
Collaborator

Hi @bechenard,
You have proposed changes to files in these packages:

  • MCDataProducts
  • CaloReco
  • CaloMC

which require these tests: build.

@Mu2e/fnalbuild-users, @Mu2e/write have access to CI actions on main.

⌛ The following tests have been triggered for fa4d74c: build (Build queue - API unavailable)

About FNALbuild. Code review on Mu2e/Offline.

@FNALbuild
Copy link
Collaborator

☀️ The build tests passed at fa4d74c.

Test Result Details
test with Command did not list any other PRs to include
merge Merged fa4d74c at 9ffc000
build (prof) Log file. Build time: 04 min 12 sec
ceSimReco Log file.
g4test_03MT Log file.
transportOnly Log file.
POT Log file.
g4study Log file.
cosmicSimReco Log file.
cosmicOffSpill Log file.
ceSteps Log file.
ceDigi Log file.
muDauSteps Log file.
ceMix Log file.
rootOverlaps Log file.
g4surfaceCheck Log file.
trigger Log file.
check_cmake Log file.
FIXME, TODO TODO (0) FIXME (0) in 5 files
clang-tidy ➡️ 6 errors 114 warnings
whitespace check no whitespace errors found

N.B. These results were obtained from a build of this Pull Request at fa4d74c after being merged into the base branch at 9ffc000.

For more information, please check the job page here.
Build artifacts are deleted after 5 days. If this is not desired, select Keep this build forever on the job page.

Copy link
Contributor

@michaelmackenzie michaelmackenzie left a comment

Choose a reason for hiding this comment

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

Looks good to me, thank you! I just made one small comment, but it's not necessary to resolve in order to merge

Comment on lines +283 to +285
bool isCaphri = std::find(CaloConst::_caphriId.begin(),CaloConst::_caphriId.end(),
crystalID) != CaloConst::_caphriId.end();
float pePerMeV = isCaphri ? pePerMeVLyso_ : pePerMeV_;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be moved outside the for loop, instead of evaluated in each loop?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, same comment as above

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Obviously, I was tired....

@oksuzian
Copy link
Collaborator

oksuzian commented Mar 7, 2026

Let me review this pull request by examining its details and the code changes.Here's my review of PR #1756. This PR adds a crystalID field to CaloHitMC and introduces a separate light yield (LY) parameter for LYSO crystals (CaPHRI detectors), distinguishing them from CsI crystals. I found several issues:


🔴 Bugs / Correctness Issues

1. Breaking change to CaloHitMC constructor — other callers may be broken

// Old:
CaloHitMC(const std::vector<CaloEDepMC>& edeps) : edeps_(edeps) {};
// New:
CaloHitMC(const std::vector<CaloEDepMC>& edeps, int crystalId) :
  edeps_(edeps), crystalId_(crystalId) {};

The constructor signature changed from 1 parameter to 2. Any existing code in the codebase that constructs a CaloHitMC with only an edeps vector will now fail to compile. Only CaloHitTruthMatch_module.cc is updated in this PR — there may be other call sites that were missed.

2. CaloHitMC move constructor lost — potential unnecessary copies

caloHitMCs.emplace_back(CaloHitMC(std::move(edeps),hit.crystalID()));

The old constructor accepted std::move(edeps) into a const std::vector<CaloEDepMC>& parameter, which means the move is silently ignored — the vector is always copied. The new constructor has the same problem. The parameter should be std::vector<CaloEDepMC>&& edeps (or accept by value) to actually benefit from std::move.

3. Merge conflict — PR is not mergeable

The PR shows mergeable: false and mergeable_state: "dirty". This needs to be rebased or have conflicts resolved against main before it can be merged.


🟡 Performance Issues

4. isCaphri lookup inside a loop (already flagged by @michaelmackenzie)

for (int i=0; i<nROs; ++i)
{
    bool isCaphri = std::find(CaloConst::_caphriId.begin(),CaloConst::_caphriId.end(),
                              crystalID) != CaloConst::_caphriId.end();
    float pePerMeV = isCaphri ? pePerMeVLyso_ : pePerMeV_;

The isCaphri determination and pePerMeV selection depend only on crystalID, which does not change across loop iterations (it's per-crystal, not per-RO). These should be hoisted before the for loop. This is a linear search (std::find on a vector) repeated nROs times unnecessarily.

5. Same pattern in CaloDigiMaker_module.cc — linear search could use std::set or sorted + binary_search

bool isCaphri = std::find(CaloConst::_caphriId.begin(),CaloConst::_caphriId.end(),
                          crystalID) != CaloConst::_caphriId.end();

If CaloConst::_caphriId is not tiny, using std::find (O(n) linear scan) for every hit is suboptimal. Consider using a std::unordered_set or ensuring the array is sorted and using std::binary_search.


🟡 Design / Consistency Issues

6. Type inconsistency: double in CaloDigiMaker vs float in CaloShowerROMaker

fhicl::Atom<double> pePerMeVLyso  { Name("readoutPEPerMeVLyso"), ... };
double pePerMeVLyso_;
fhicl::Atom<float> pePerMeVLyso  { Name("readoutPEPerMeVLyso"), ... };
float pePerMeVLyso_;

CaloDigiMaker uses double while CaloShowerROMaker uses float for the same conceptual parameter. This is inconsistent and could lead to subtle precision differences. (This pre-existing inconsistency for pePerMeV_ is carried over to the new LYSO parameter.)

7. auto scaleFactor(...) uses "most vexing parse"-adjacent syntax

auto scaleFactor(MeVToADC_/pePerMeV);

While this compiles, auto scaleFactor = MeVToADC_ / pePerMeV; is clearer and more idiomatic modern C++. The parenthesized initialization with auto can confuse readers who might think it's a function declaration.

8. Naming inconsistency: crystalId_ (member) vs crystalID() (accessor)

int crystalID() const { return crystalId_; }  // accessor: "ID" (uppercase)
int crystalId_;                                 // member: "Id" (mixed case)

The member uses crystalId_ but the public accessor is crystalID(). While minor, consistency (e.g., crystalID_ to match the accessor) would improve readability.


🟢 Bugfix Acknowledgment

9. Good fix in CaloHitTruthMatch_module.cc

// Old (buggy): put caloHitMCTruth twice, caloShowerMCTruth never
-  event.put(std::move(caloHitMCTruth));
   event.put(std::move(caloHitMCs));
-  if (fillDetailedMC_) event.put(std::move(caloHitMCTruth));
// New (fixed):
   event.put(std::move(caloHitMCs));
+  event.put(std::move(caloHitMCTruth));
+  if (fillDetailedMC_) event.put(std::move(caloShowerMCTruth));

This is a genuine bug fix — the old code called event.put(std::move(caloHitMCTruth)) twice (the second call would be on an already-moved-from object, causing undefined behavior) and never put caloShowerMCTruth. Good catch.


Summary

Severity Issue File
🔴 Bug Breaking constructor change — check all CaloHitMC call sites CaloHitMC.hh
🔴 Bug std::move into const& param is a no-op (copy instead of move) CaloHitMC.hh
🔴 Merge PR has merge conflicts with main
🟡 Perf isCaphri lookup should be hoisted out of loop CaloShowerROMaker_module.cc
🟡 Perf Linear search for CaPHRI check on every hit CaloDigiMaker_module.cc
🟡 Design double vs float type inconsistency for same parameter Multiple
🟡 Style auto var(expr) vs auto var = expr CaloDigiMaker_module.cc
🟡 Style crystalId_ vs crystalID() naming mismatch CaloHitMC.hh
🟢 Fix Correctly puts caloShowerMCTruth instead of double-moving caloHitMCTruth CaloHitTruthMatch_module.cc

@FNALbuild
Copy link
Collaborator

📝 The HEAD of main has changed to dc50b36. Tests are now out of date.

Comment on lines +236 to +238
int crystalID = CaloSiPMId(iRO).crystal().id();
bool isCaphri = std::find(CaloConst::_caphriId.begin(),CaloConst::_caphriId.end(),
crystalID) != CaloConst::_caphriId.end();
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can (should?) use CaloSiPMId(iR0).crystal().isCaphri()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, didn't know about this one, will modify

Comment on lines +283 to +285
bool isCaphri = std::find(CaloConst::_caphriId.begin(),CaloConst::_caphriId.end(),
crystalID) != CaloConst::_caphriId.end();
float pePerMeV = isCaphri ? pePerMeVLyso_ : pePerMeV_;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, same comment as above

bool isCaphri = std::find(CaloConst::_caphriId.begin(),CaloConst::_caphriId.end(),
crystalID) != CaloConst::_caphriId.end();
auto pePerMeV = isCaphri ? pePerMeVLyso_ : pePerMeV_;
auto scaleFactor(MeVToADC_/pePerMeV);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this could be prone to future mistakes by swapping pePerMeV with pePerMeV_.
Perhaps better to swap pePerMeV_ with pePerMeVCsI_?
No strong opinion however.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed, I really start to hate these Caphri crystals..... We will adopt a better solution when we port these values into the database too.

P.S: I think we use the same SiPMs for the LYSO crystals, so the noise should be identical to that of the CsI crystals and I have left it this way. IF we use different readouts, then I'll make modifications when I refactor the noise generation

@bechenard
Copy link
Contributor Author

@FNALbuild run build test

@FNALbuild
Copy link
Collaborator

⌛ The following tests have been triggered for bbce729: build (Build queue - API unavailable)

@FNALbuild
Copy link
Collaborator

☀️ The build tests passed at bbce729.

Test Result Details
test with Command did not list any other PRs to include
merge Merged bbce729 at dc50b36
build (prof) Log file. Build time: 04 min 10 sec
ceSimReco Log file.
g4test_03MT Log file.
transportOnly Log file.
POT Log file.
g4study Log file.
cosmicSimReco Log file.
cosmicOffSpill Log file.
ceSteps Log file.
ceDigi Log file.
muDauSteps Log file.
ceMix Log file.
rootOverlaps Log file.
g4surfaceCheck Log file.
trigger Log file. Return Code 1.
check_cmake Log file.
FIXME, TODO TODO (0) FIXME (0) in 5 files
clang-tidy ➡️ 6 errors 115 warnings
whitespace check no whitespace errors found

N.B. These results were obtained from a build of this Pull Request at bbce729 after being merged into the base branch at dc50b36.

For more information, please check the job page here.
Build artifacts are deleted after 5 days. If this is not desired, select Keep this build forever on the job page.

@oksuzian
Copy link
Collaborator

I'll review this pull request by examining its details and the code changes.Now let me look at the existing review comments and also examine the CaloSiPMId and CaloConst classes to understand the CAPHRI detection logic better.Here is my review of PR #1756 with the critical issues I've identified:


PR Summary

This PR adds a crystalID field to CaloHitMC, differentiates the light yield (PE/MeV) for CsI vs LYSO (CAPHRI) crystals, and changes several member variable types from double to float in CaloDigiMaker.


Critical Issues

1. 🔴 Missing std::move in rvalue-reference constructor (CaloHitMC.hh)

CaloHitMC(const std::vector<CaloEDepMC>&& edeps, int crystalId) :
 edeps_(edeps),       // ← BUG: copies instead of moving!
 crystalID_(crystalId)
{};

The rvalue-reference overload accepts const std::vector<CaloEDepMC>&& but then initializes edeps_ by copy because edeps is not std::move'd, and even if it were, const&& cannot be moved from. This constructor should be:

CaloHitMC(std::vector<CaloEDepMC>&& edeps, int crystalId) :
 edeps_(std::move(edeps)),
 crystalID_(crystalId)
{};

The const on the rvalue reference defeats the purpose of having a move constructor entirely—it silently falls back to copying. This is a performance bug and a semantic correctness issue.

2. 🔴 CaloNoiseSimGenerator still references the old readoutPEPerMeV parameter

fhicl::Atom<double>      pePerMeV       { Name("readoutPEPerMeV"),Comment("Number of pe / MeV for Readout") };

The noise generator config still declares a single readoutPEPerMeV parameter. In CaloMC/fcl/prolog.fcl, the noise generator block (NoiseGenerator) is configured with:

readoutPEPerMeV         : @local::readoutPEPerMeVCsI

While this avoids an immediate runtime crash (the fhicl key name matches), the noise simulation always uses the CsI value and never differentiates for LYSO crystals. The author acknowledges this in a comment, stating the SiPMs are currently assumed identical, but this is a latent correctness issue if that assumption changes.

3. 🟠 Inconsistent CAPHRI detection between the two modules

The two C++ modules use different methods to determine if a crystal is CAPHRI:

File Method
CaloDigiMaker_module.cc CaloSiPMId(iRO).crystal().isCaphri()
CaloShowerROMaker_module.cc std::find(CaloConst::_caphriId.begin(), ...) (manual linear search)

While both should give the same result, using two different code paths for the same logic is fragile and error-prone. CaloShowerROMaker should also use the CrystalId::isCaphri() method for consistency. The author was told about isCaphri() in the review comments and updated CaloDigiMaker but forgot to update CaloShowerROMaker.

4. 🟠 doublefloat type narrowing is unnecessary and reduces precision

In CaloDigiMaker_module.cc, multiple member variables and fhicl parameters are changed from double to float:

float                   digitizationStart_;
float                   digitizationEnd_;
float                   timeFromProtonsToDRMarker_;
float                   digiSampling_;
float                   startTimeBuffer_;
float                   MeVToADC_;

This reduces precision from ~15 significant digits to ~7. For timing values (ns-scale with sub-ns precision requirements in HEP), and for MeVToADC which is used in division (MeVToADC_/pePerMeV), the loss of precision could introduce subtle simulation artifacts. The CaloShowerROMaker already used float for some of these, but CaloDigiMaker previously used double presumably for a reason. This change is bundled silently with the LYSO feature and may not receive the scrutiny it deserves.

5. 🟡 Order of event.put() changed in CaloHitTruthMatch

- event.put(std::move(caloHitMCTruth));
  event.put(std::move(caloHitMCs));
+ event.put(std::move(caloHitMCTruth));

The order of event.put() calls was swapped. In the art framework, the order of event.put() for different product types typically doesn't matter, but if any downstream consumer relies on product insertion order for same-type products, this could cause a subtle issue. This should at minimum have a comment explaining why the reorder was needed.


Summary

Severity Issue File
🔴 Critical const&& prevents move; silent copy CaloHitMC.hh
🔴 Critical Noise generator not updated for dual PE/MeV CaloNoiseSimGenerator.hh
🟠 Medium Inconsistent CAPHRI detection methods CaloShowerROMaker_module.cc
🟠 Medium Unnecessary doublefloat narrowing CaloDigiMaker_module.cc
🟡 Low event.put() order changed without explanation CaloHitTruthMatch_module.cc

@bechenard
Copy link
Contributor Author

@FNALbuild run build test

@FNALbuild
Copy link
Collaborator

⌛ The following tests have been triggered for 64bab70: build (Build queue - API unavailable)

@FNALbuild
Copy link
Collaborator

☀️ The build tests passed at 64bab70.

Test Result Details
test with Command did not list any other PRs to include
merge Merged 64bab70 at dc50b36
build (prof) Log file. Build time: 04 min 06 sec
ceSimReco Log file.
g4test_03MT Log file.
transportOnly Log file.
POT Log file.
g4study Log file.
cosmicSimReco Log file.
cosmicOffSpill Log file.
ceSteps Log file.
ceDigi Log file.
muDauSteps Log file.
ceMix Log file.
rootOverlaps Log file.
g4surfaceCheck Log file.
trigger Log file. Return Code 1.
check_cmake Log file.
FIXME, TODO TODO (0) FIXME (0) in 5 files
clang-tidy ➡️ 6 errors 115 warnings
whitespace check no whitespace errors found

N.B. These results were obtained from a build of this Pull Request at 64bab70 after being merged into the base branch at dc50b36.

For more information, please check the job page here.
Build artifacts are deleted after 5 days. If this is not desired, select Keep this build forever on the job page.

@michaelmackenzie
Copy link
Contributor

@bechenard are changes in the trigger test expected here? It looks like there are changes in the calo paths that weren't seen in the initial CI test

@bechenard
Copy link
Contributor Author

Nope, this is weird. Will have a look at it

@bechenard
Copy link
Contributor Author

The trigger ran correctly, but there are small changes in the trigger rates since I changes the light yield of the LYSO crystals. Looks reasonable to me

@oksuzian oksuzian merged commit eda787f into Mu2e:main Mar 12, 2026
14 checks passed
@michaelmackenzie
Copy link
Contributor

@bechenard why would the LYSO light yield change trigger reco? Wouldn't this only change the CAPHRI crystals which (I assume) aren't included in the clustering for the trigger paths that changed?

@bechenard
Copy link
Contributor Author

bechenard commented Mar 12, 2026 via email

@michaelmackenzie
Copy link
Contributor

michaelmackenzie commented Mar 12, 2026

I assumed they weren't since the CaloHitMakerFast produces a separate hit collection for CAPHRI vs. non-CAPHRI hits:
https://github.com/Mu2e/Offline/blob/main/CaloReco/src/CaloHitMakerFast_module.cc#L163-#L171

@giro94
Copy link
Collaborator

giro94 commented Mar 12, 2026

They are treated the same in offline, separate in online

@michaelmackenzie
Copy link
Contributor

@bechenard could you please check running mu2e-trig-config/ci/check_trigger_results.sh with the original LYSO light yield you restore the trigger results? Then I can open an issue to understand why the LYSO light yield impacts Online reco results...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants