Skip to content

Maia/theta energy - #41

Open
Jullianwatts wants to merge 12 commits into
PandoraPFAOrg:masterfrom
Jullianwatts:maia/theta-energy
Open

Maia/theta energy#41
Jullianwatts wants to merge 12 commits into
PandoraPFAOrg:masterfrom
Jullianwatts:maia/theta-energy

Conversation

@Jullianwatts

Copy link
Copy Markdown

These commits are from @trholmes. I am submitting a PR because downstream
work depends on them.

Adds two energy correction plugins that look up a correction factor in a 2D
(theta, energy) table instead of applying one flat constant per subdetector:

  • PhotonEMNonLinearity, electromagnetic
  • HadronicThetaEnergyBinned, hadronic

The correction is applied inside Pandora, after clustering and before PFOs are
built, so cluster energies, PFO energies, and jets all come out consistent and
nothing has to be applied by hand downstream.

Also adds a three-argument RegisterNonLinearityEnergyCorrection overload taking
the bin edges and the table. The existing one-argument overload is unchanged,
and with no table supplied the plugins fall back to it, which is the identity.
So this is a no-op for anyone who does not configure a table.

These plugins have been in use in muon collider reconstruction and are being
upstreamed so they are available generally. key4hep/k4GaudiPandora#51 uses this
overload and cannot compile until this lands, and
MuonColliderSoft/MAIAConfig#90 sits on top of that.
This is a draft for now. This needs a rebase onto current master before it can be merged. Opening it so the dependency chain is visible.

@Jullianwatts
Jullianwatts marked this pull request as ready for review September 1, 2026 17:50
@madbaron

madbaron commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Thanks for upstreaming these!
I've confirmed the no-op claim holds: all four new XML flags default to false, and with no table registered the lookup returns the input energy unchanged.

I tested this in ghcr.io/muoncollidersoft/mucoll-sim-ubuntu24:post_3_1_test, against PandoraSDK 3.4.2.
This is ready from a mechanical standpoint.

I have just a couple of comments on the track-consistency comparison and about the lookup's global state:

  1. In both merging algorithms, chi and chi0 are computed through different correction chains. chi0 uses GetCorrectedHadronicEnergy(parent), which runs the full plugin chain while chi uses the raw cluster sum with only the theta factor applied. The chi^2 - chi0^2 difference the cut is made on therefore absorbs the non-theta part of the correction chain rather than the daughter's contribution. In ConeBasedMergingAlgorithm this has a failure mode that inverts the intent of the flag (I'll comment inline).
  2. The three new sites use GetCorrectedHadronicEnergy, but every other track-vs-cluster energy comparison in LCContent uses GetTrackComparisonEnergy. The SDK documents GetTrackComparisonEnergy as "the best energy estimate to use when comparing cluster energy to associated track momentum" corrected EM energy for EM showers, corrected hadronic otherwise. I think we should probably pick it up.


mergedHadronicEnergy = LCEnergyCorrectionPlugins::GetThetaEnergyCorrectedEnergy(
pandora::HADRONIC, parentDirection, mergedHadronicEnergy);
addedHadronicEnergy = std::max(0.f, mergedHadronicEnergy - parentHadronicEnergy);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

At this point parentHadronicEnergy is the fully corrected parent energy, while mergedHadronicEnergy is the raw sum with only the theta factor applied. The subtraction mixes two different correction chains.

When the theta factor is below 1, the corrected parent can exceed the theta-corrected
raw sum. addedHadronicEnergy then clamps to 0, and on line 144 0 > 1 GeV is false, so the chi cut is skipped entirely and the merge proceeds unchecked.

I'd suggest computing both sides through the same path, and gating on the daughter's own energy as master does rather than on a difference of differently-corrected quantities.


if (m_useCorrectedHadronicEnergyForTrackComparison) {
parentHadronicEnergy = pBestParentCluster->GetCorrectedHadronicEnergy(this->GetPandora());
mergedHadronicEnergy = pBestParentCluster->GetHadronicEnergy() + pDaughterCluster->GetHadronicEnergy();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is overwritten on line 137, and it already equals the value assigned in the initialiser on line 125. Safe to delete.

float clusterEnergySum(daughterHadronicEnergy + parentHadronicEnergy);

if (m_useCorrectedHadronicEnergyForTrackComparison) {
parentTrackComparisonEnergy = pParentCluster->GetCorrectedHadronicEnergy(this->GetPandora());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same chain asymmetry as in ConeBasedMergingAlgorithm: this is the fully corrected parent, while clusterEnergySum on line 133 is the theta-corrected raw sum. The chi^2 - chi0^2 cut is dominated by the correction-chain difference rather than by the daughter.

I think we should apply the same manual theta correction to the parent alone so both sides go through identical treatment.

const float energyDifference(std::fabs(pCluster->GetHadronicEnergy() - pTrack->GetEnergyAtDca()));
const float clusterHadronicEnergy(pCluster->GetHadronicEnergy());
const float trackComparisonEnergy(m_useCorrectedHadronicEnergyForTrackComparison
? pCluster->GetCorrectedHadronicEnergy(this->GetPandora())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggest to use GetTrackComparisonEnergy(this->GetPandora()) here. It resolves to the corrected hadronic energy for hadronic clusters, so behaviour is unchanged for those, but it uses the corrected EM energy for the rest.


typedef std::map<ThetaEnergyCorrectionKey, ThetaEnergyCorrectionTable> ThetaEnergyCorrectionTableMap;

ThetaEnergyCorrectionTableMap& GetThetaEnergyCorrectionTableMap() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This function-local static is keyed on (name, EnergyCorrectionType) only. Two Pandora instances in one process share a single table, and the second registration silently overwrites the first. There's no way for an algorithm to ask for its Pandora's table.

Worth noting this is currently the only global mutable state in LCContent.
Suggest keying on const pandora::Pandora * as well, with the algorithms passing &this->GetPandora().

const float cosTheta(std::max(-1.f, std::min(1.f, direction.GetCosOpeningAngle(CartesianVector(0.f, 0.f, 1.f)))));
const float theta(std::acos(cosTheta));

for (const ThetaEnergyCorrectionTableMap::value_type& mapEntry : GetThetaEnergyCorrectionTableMap()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The loop skips entries whose type doesn't match, then unconditionally returns on the first survivor. If two HADRONIC tables are ever registered under different names, which one you get depends on std::map's ordering of the name strings.

We could the plugin name as a parameter, plumbed from each algorithm's XML block alongside the existing flag. This would also let a single job use different tables in different algorithms.

Related: pandora::HADRONIC is hard-coded at both merging call sites, so a table registered under any other correction type is silently ignored.

return true;
}

int FindBin(const FloatVector& edges, const float value) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

value >= edges.back() returns -1, and GetCorrection then returns 1.0. So a cluster one MeV above the top energy edge jumps discontinuously from (say) 0.85 to 1.00. The same applies at both theta ends, but it's less damaging if a map is coded up to >pi.

I think we should clamp to the edge bin, making the last bin inclusive?

Comment thread src/LCPlugins/LCEnergyCorrectionPlugins.cc Outdated
Comment thread src/LCPlugins/LCEnergyCorrectionPlugins.cc
Comment thread include/LCPlugins/LCEnergyCorrectionPlugins.h Outdated
Comment thread include/LCPlugins/LCEnergyCorrectionPlugins.h
throw StatusCodeException(STATUS_CODE_FAILURE);

const float eOverP(electromagneticEnergy / momentumAtDca);
const float eOverPEnergy(m_useCorrectedElectromagneticEnergyForEOverP

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

eOverPEnergy doesn't depend on the loop iterator, so it can be moved above the for over associatedTrackList.

@madbaron

madbaron commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

I'll add a couple of tests for the new correction lookup and will take care of the deduplication of GetCorrection.

I'm happy to do more if you need help!

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