Skip to content

3D cluster association algorithm - #280

Open
Mandri97 wants to merge 23 commits into
PandoraPFAOrg:masterfrom
Mandri97:feature/ThreeDAssociationAlgorithm
Open

3D cluster association algorithm#280
Mandri97 wants to merge 23 commits into
PandoraPFAOrg:masterfrom
Mandri97:feature/ThreeDAssociationAlgorithm

Conversation

@Mandri97

Copy link
Copy Markdown

An algorithm to associate clusters in 3D. It is a simple approach inspired by the (2D) longitudinal association algorithm, but it removes the reliance and constraints on pseudo-layer to better support 3D reconstruction

@CrossR

CrossR commented Apr 29, 2026

Copy link
Copy Markdown
Member

Not checked over yet in depth...but this looks very nicely self contained. Could it be split into a LArContent ClusterHelper update and LArRecoND new algorithm?

};

typedef std::unordered_map<const pandora::Cluster*, ClusterAttr> ClusterAttrMap;
mutable ClusterAttrMap clusterAttrMap;

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.

Add m_ prefix for class member variables

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed

for (ClusterList::const_iterator iter = pClusterList->begin(), iterEnd = pClusterList->end(); iter != iterEnd; ++iter)
{
const Cluster *const pCluster = *iter;
float clusterLength = LArClusterHelper::GetLength(pCluster);

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.

Can be const?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed

Comment on lines +91 to +95
CartesianVector currentClusterInnerCoordinate{ clusterAttrMap.at(pCurrentCluster).innerCoordinate };
CartesianVector currentClusterOuterCoordinate{ clusterAttrMap.at(pCurrentCluster).outerCoordinate };

CartesianVector testClusterInnerCoordinate{ clusterAttrMap.at(pTestCluster).innerCoordinate };
CartesianVector testClusterOuterCoordinate{ clusterAttrMap.at(pTestCluster).outerCoordinate };

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.

All const?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And make them reference variables

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed both

@lhwhitehead lhwhitehead left a comment

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.

Only minor things from my side. Once you are done with everything, run clang format to polish up things into the style that we use.

@AndyChappell AndyChappell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hi Manoa, Thanks for the PR. I think this is generally looking pretty good. I have a few, mostly minor requests and a couple of queries.

On some more general points, as Leigh noted, we'll ultimately pass this through clang format to get the Pandora style. You can skip that step if you want, as for LArContent PRs, I will apply the clang format is a normal matter of constructing release branches. However if you do choose to apply it yourself, can you ensure that it's isolated to a single commit and make it the last one in response to reviews (this makes it easier to distinguish substantive changes from formatting changes when reviewing updates. Thanks!

A secondary comment - and not anything that I think needs addressing for this PR, but I wonder if some of the sliding fit algorithms might provide some more flexibility versus PCA fits for 3D association?


//------------------------------------------------------------------------------------------------------------------------------------------


Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Remove excess whitespace.

Comment on lines +781 to +782
CartesianVector lhsOuterCoordinate(0.f, 0.f, 0.f);
CartesianVector rhsOuterCoordinate(0.f, 0.f, 0.f);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

To avoid the extra instantiation, given you don't actually care about these values you could just define

CartesianVector unused(0.f, 0.f, 0.f);

and pass that as the second parameter to both of the extremal coordinate functions below.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good idea. It's fixed

Comment on lines +60 to +65
float length;
pandora::CartesianVector innerCoordinate = pandora::CartesianVector(0.f, 0.f, 0.f);
pandora::CartesianVector outerCoordinate = pandora::CartesianVector(0.f, 0.f, 0.f);
pandora::CartesianVector centroid = pandora::CartesianVector(0.f, 0.f, 0.f);
LArPcaHelper::EigenValues eigenValues = LArPcaHelper::EigenValues(0.f, 0.f, 0.f);
LArPcaHelper::EigenVectors eigenVectors;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Coding style in Pandora is to add a prefix m_ to member variables.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed

Comment on lines +40 to +41
const Cluster *const pCluster = *iter;
float clusterLength = LArClusterHelper::GetLength(pCluster);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In general (here and elsewhere), when initialising variables, prefer

type varname{value}; over type varname = value;

unless there's a specific reason not to. This has some benefits in terms of avoiding unintended type narrowing, etc. The main exception here would be if you are explicitly calling an object constructor that might otherwise be confused for an initialisation list in the case of {}. The classic here would be a std::vector

std::vector<int> v{10, 20}; Gets you a vector containing the elements 10 and 20
std::vector<int> v(10, 20); Gets you a vector containing 10 elements, all of which are 20

if( clusterLength < m_minClusterLength )
continue;

this->PCAFit(pCluster);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wonder if this would be better named PopulatePCAAttributes, or PopulateFitAttributes?

@Mandri97 Mandri97 May 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Renamed PCAFit to PopulateFitAttributes

constexpr float shortClusterLength = 2.50f;

// Bypass opening angle cut if a short cluster is the extension of a longer one
if( (closestDistance > 0.5f) ||

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should 0.5f be a configurable parameter?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

In ND-LAr, 0.5f corresponds to a 4 mm pixel pitch with a 1 mm tolerance. Updated to

const double channelPitch{ 1.3 * LArGeometryHelper::GetWirePitch(this->GetPandora(), m_view) };

to make it detector agnostic.


constexpr float shortClusterLength = 2.50f;

// Bypass opening angle cut if a short cluster is the extension of a longer one

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would this not also bypass if the long cluster is an extension of the short one? Is that what you want?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fair. Updated the comment for clarity

// Bypass opening angle check if a short cluster extends a long one (or vice versa)

Comment on lines +164 to +165
const CartesianVector innerEndPrimaryAxis { centroid + primaryAxis * primaryAxis.GetDotProduct(innerClusterOuterCoordinate - centroid) };
const CartesianVector outerStartPrimaryAxis{ centroid + primaryAxis * primaryAxis.GetDotProduct(outerClusterInnerCoordinate - centroid) };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I can't quite get my head around what this is trying to do

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

These lines determine the coordinates of the inner cluster's end and the outer cluster's start along the longest cluster's primary axis, relative to the longest cluster's centroid.

bool isInnerLongest{ LArClusterHelper::SortByNHits(pInnerCluster, pOuterCluster) &&
(clusterAttrMap.at(pInnerCluster).length > clusterAttrMap.at(pOuterCluster).length) };

const CartesianVector centroid{ isInnerLongest ? innerCentroid : outerCentroid };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think all of these CartesianVectors can be reference variables.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed

Comment thread larpandoracontent/LArContent.cc Outdated
Comment on lines +292 to +293
d("LArThreeDLongitudinalTracks", ThreeViewLongitudinalTracksAlgorithm) \
d("LArThreeDAssociation", ThreeDAssociationAlgorithm) \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The header files will be reordered by clang format, so could you swap these two here? (clang format is not allowed to alter these preprocessor blocks).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed

@AndyChappell

AndyChappell commented May 1, 2026

Copy link
Copy Markdown
Member

Could it be split into a LArContent ClusterHelper update and LArRecoND new algorithm?

Just to comment on this latter point - a clusterhelper style refactoring may have some value here - I'll let @CrossR comment on that, but to the more general point of LArRecoND versus LArContent, modulo a couple of points where some hard-coded values could do with becoming configurable, I think this is a case where the algorithm is sufficiently generic that it belongs in LArContent. It seems to be a general purpose association algorithm based on 3D hits, which, those hard-coded values aside, doesn't appear to have anything that is specific to the ND, and could equally apply to the built 3D clusters in wire/strip-based TPCS. Happy to hear thoughts on this.

@AndyChappell AndyChappell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A couple of minor additional requests, and then I think this should be good to go (though looks like there will be merge conflicts to resolve).

Comment on lines +97 to +98
if( m_clusterAttrMap.find(pCurrentCluster) == m_clusterAttrMap.end() )
LArClusterHelper::GetExtremalCoordinates(pCurrentCluster, currentClusterInnerCoordinate, currentClusterOuterCoordinate);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If any part of the conditional is multiline, enclose each branch in braces, even if that branch is only a single line


if( m_clusterAttrMap.find(pCurrentCluster) == m_clusterAttrMap.end() )
LArClusterHelper::GetExtremalCoordinates(pCurrentCluster, currentClusterInnerCoordinate, currentClusterOuterCoordinate);
else{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No need to address this here, as the pre-release clang format run will resolve it, but for future reference, Pandora style is to have the opening brace on its own line.

Comment on lines +104 to +105
if ( m_clusterAttrMap.find(pTestCluster) == m_clusterAttrMap.end() )
LArClusterHelper::GetExtremalCoordinates(pTestCluster, testClusterInnerCoordinate, testClusterOuterCoordinate);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As above re brace enclosure

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed them all

const CartesianVector outerCentroid{ clusterAttrMap.at(pOuterCluster).centroid };

const float openingAngle{ innerPrimaryAxis.GetCosOpeningAngle(outerPrimaryAxis) };
const double channelPitch{ 1.3 * LArGeometryHelper::GetWirePitch(this->GetPandora(), m_view) };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should 1.3 be a configurable parameter here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes! Fixed.

@CrossR

CrossR commented Jul 6, 2026

Copy link
Copy Markdown
Member

I've got two more general / high-level comments (which may be explained if I knew the XML or some of the more underlying code):

  • Does the m_clusterAttrMap need to be mutable / a class var? It seemed at least that it might be cleaner to just pass it around in the few places that need it, rather than having a mutable var in const methods.

  • Similarly, I'm not 100% sure what the mutable pandora::HitType m_view; is for? My naive understanding is that as a 3D focused algorithm it would only ever had the 3D views, but is that wrong and the view is going to change a bunch? Regardless it didn't look like it was used very much so could that also just be passed around?

@Mandri97

Mandri97 commented Aug 4, 2026

Copy link
Copy Markdown
Author

Hi @CrossR. I'm not really sure where/how to reply to your comment, so I'm writing here 😅.

Does the m_clusterAttrMap need to be mutable / a class var? It seemed at least that it might be cleaner to just pass it around in the few places that need it, rather than having a mutable var in const methods.

m_clusterAttrMap was introduced to cache cluster attributes since computing them repeatedly is quite expensive. This map is used in both AreClustersAssociated and IsExtremalCluster. The former is called within this class, but IsExtremalCluster is called from the parent class (ClusterAssociationAlgorithm) that this class inherits from. And because of that, there isn't a practical way to pass m_clusterAttrMap to IsExtremalCluster, so it needs to be a member variable. For the mutable thing, I moved all the code that fills m_clusterAttrMap into PopulateFitAttributes and made that method non-const, so that we can remove the mutable keyword

Similarly, I'm not 100% sure what the mutable pandora::HitType m_view; is for? My naive understanding is that as a 3D focused algorithm it would only ever had the 3D views, but is that wrong and the view is going to change a bunch? Regardless it didn't look like it was used very much so could that also just be passed around?

Good catch! It's only used in one place, so it can be a local variable instead. It's only used to retrieve the wire pitch not to do any reco logic.

@Mandri97

Copy link
Copy Markdown
Author

Edit to my previous comment.

Ugh, I forgot that PopulateFitAttributes (renamed to PopulateClusterAttributes) is called from GetListOfCleanClusters, which is const, so PopulateClusterAttributes had to become const too, which meant m_clusterAttrMap had to become mutable

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.

4 participants