Skip to content

Fix the normalization for muon analysis to account for different PixelShapes.#2845

Closed
burmist-git wants to merge 21 commits into
mainfrom
muon_fit_chord_length_with_phi0
Closed

Fix the normalization for muon analysis to account for different PixelShapes.#2845
burmist-git wants to merge 21 commits into
mainfrom
muon_fit_chord_length_with_phi0

Conversation

@burmist-git

@burmist-git burmist-git commented Sep 25, 2025

Copy link
Copy Markdown
Member

issue : #2844 (comment)
Parent issue : #2815

The goal is to improve the existing intensity fitter and prepare it for adding another method to measure the impact point and optical efficiency.

This pull request contains two main tasks:

  • Include phi0 as a parameter in chord_length(radius, rho, phi) in image.muon.intensity_fitter:
  • Fix the normalization to account for different PixelShapes. Add a function to calculate the trivial solution for the number of photons (from a muon) incident on the telescope mirror, and include it in the tests.

@burmist-git burmist-git marked this pull request as draft September 25, 2025 12:19
@burmist-git burmist-git marked this pull request as ready for review September 26, 2025 15:19

@mexanick mexanick left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@burmist-git thank you for this PR. I've posted a couple of quick comments above. In addition, please add the changelog statement and fix the issues identified by sonar.

@vectorize(
[double(double, double, double, double)], cache=not CTAPIPE_DISABLE_NUMBA_CACHE
)
def chord_length(radius, rho, phi0, phi):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What's the reason to break the order of mandatory arguments? Can't we get away with

def chord_length(radius, rho, phi, phi0):

?

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.

Not just the order, also the meaning.

Rho is now absolute instead of a fraction of the radius. Why? That seems like completely needless breaking change, just moving one division or multiplication around.

@burmist-git burmist-git Sep 29, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, the rho and phi0 were changed. And indeed, we can reduce some of the changes.
However, we are modifying this function for a good reason: to make it easier to read and to reuse the same function in the new method.

Old version arguments: (scalar) mirror radius, (scalar) impact parameter relative to the mirror, omitting phi0 and using only one vector phi.

New version arguments: (scalar) mirror radius, (scalar) impact parameter, (scalar) phi0, and one vector phi.

The latest implementation is more natural for three reasons:

  • It includes all the required parameters in a single function.
  • If Rmirror is zero, we need to check this before performing the division by Rmirror, since it enters the MINUIT minimizer. It is more natural to handle this inside the function itself, making it more self-contained. For example:
  • if Rmirror (e.g. for a hole) is 0, the function simply provides the correct answer (zero).

The argument ordering is clearer: (scalar), (scalar), (scalar), (vector).

This function is used only in this module, so I don’t really understand your concerns.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@maxnoe @mexanick please give feed back on this point. However, the current code already incorporates your suggestions, but my comments are still valid. I still believe we need to make this function more straightforward.

Comment thread src/ctapipe/image/muon/intensity_fitter.py Outdated
phi = phi - phi0

phi_modulo = (phi + np.pi) % (2 * np.pi) - np.pi
if phi < 0:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

use math.abs() or np.abs()

@burmist-git burmist-git Sep 29, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

If you are commenting about this line:
phi_modulo *= -1
then it is correct. Since modulo returns sign less value and we need to return the minus sign back.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

looking closer at your code, I don't understand why do you even need this. phi_modulo is used in

  • np.sin(phi_modulo) ** 2
  • np.cos(phi_modulo)
  • np.abs(phi_modulo)

The result of the operations above doesn't depend on the sign of phi_modulo.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ok i see. Let me run the tests.

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.

Isn't reflecting at the origin just wrong here? Shouldn't you add 2π instead?

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.

Consider e.g. a small negative value (you'd change -π/4 to +π/4 for example).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It is not needed there at all, the line above returns phi_modulo correctly normalized within [-π, π].

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is unnecessary
phi_modulo *= -1

Here is the test without it

image

Comment thread src/ctapipe/image/muon/intensity_fitter.py
from ctapipe.image.muon.intensity_fitter import chord_length

length = chord_length(radius, rho, phi.to_value(u.rad))
length = chord_length(radius, rho, phi0.to_value(u.rad), phi.to_value(u.rad))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks like the order of parameters is wrong now (phi <-> phi0)



def intersect_circle(mirror_radius, r, angle, hole_radius=0):
def intersect_circle(mirror_radius, r, phi0, angle, hole_radius=0):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

phi0 is not used in the function, so there's no need (at least at the moment) to add it.
Also, when adding, considering adding it as a last-order keyword argument to preserve the legacy calls.

@burmist-git burmist-git Sep 29, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This has been corrected. It was the reason this wrapper was written.

@burmist-git

Copy link
Copy Markdown
Member Author

The code is ready for review. Sonar reports incorrect code coverage for functions using vectorization.

@burmist-git

Copy link
Copy Markdown
Member Author

The wrapper did not change the time.

image

On my laptop, the log-likelihood method takes on average 7 minutes and 40 seconds to run for 9,809 muon fits.
With the new method, the runtime is measured to be about 3 minutes at most.

@mexanick mexanick self-requested a review September 30, 2025 08:49
mexanick
mexanick previously approved these changes Sep 30, 2025

@mexanick mexanick left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@burmist-git

Copy link
Copy Markdown
Member Author

@maxnoe Any feed back on this MR ?

@burmist-git burmist-git requested a review from kosack October 6, 2025 07:26

phi_modulo = (phi + np.pi) % (2 * np.pi) - np.pi

rho_r = np.abs(rho) / radius

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.

What is the motivation for this redefinition? It seems like an arbitrary choice, so why change it now?

Please revert.

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.

Also, calling abs internally on a quantity that you expect to be always positive might result in very hard debug issues as it just hides wrong input.

@burmist-git burmist-git Oct 6, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@maxnoe concerning this : #2845 (comment)
here is the reason why :
image

So, in other words, we can minimize the symmetric function with respect to rho. And of course, the same logic is applied to phi.

https://root.cern.ch/download/minuit.pdf

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@maxnoe concerning this : #2845 (comment)

Here I put this as explanation :
#2845 (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.

Then I'd expect this transformation as part of the likelihood definition, not in the low-level function.

I think here it is misleading.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

What should we do if this value is negative? Raise an error?

If the value is negative, it still returns a result. This is non-physical and incorrect, so we cannot output it without handling it properly.

@maxnoe maxnoe Oct 20, 2025

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.

Yes, raising a ValueError(f"r must be > 0, got {r}") seems appropriate here.

@@ -317,7 +344,12 @@ def image_prediction_no_units(
# diameter. In any case, since in the end we do a data-MC comparison of the muon
# ring analysis outputs, it is not critical that this value is exact.

@maxnoe maxnoe Oct 6, 2025

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 comment here provided by @moralejo is no longer correct, since you introduce the correction for the pixel shape.

Please update it.

Please also have a look here @moralejo.

@burmist-git burmist-git Oct 6, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@moralejo Is this is ok to replace. In the last two sentences… :

# Actually, for the large rings (relative to pixel
# size) we are concerned with, a good enough approximation is the ratio between a
# circle's area and that of the square whose side is equal to the circle's
# diameter or flat-to-flat distance for hexagonal pixel.

@maxnoe maxnoe 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.

Please better explain why the change of API of these functions is needed or an improvement (both adding phi0 and changing the definition of rho).

If we decide that the absolute is better, then the parameter should also be renamed, since rho is used in the current definition by the linked references.

I.e. rho should only be used for the relative quantity.

In general, please separate bug fixes like the modulo check from improvements like the pixel type scaling from API changes.

@burmist-git

Copy link
Copy Markdown
Member Author

Please better explain why the change of API of these functions is needed or an improvement (both adding phi0 and changing the definition of rho).

Adding phi0 and changing rho (to be renamed to impact_parameter) need to be done for at least three reasons:

  1. The function becomes clearer — there is no confusion between the mirror radius and the impact parameter from the mirror’s center, as both are now expressed in the same units.

  2. phi0 is explicitly defined, consistent with one of the reference publications we are using for the muon analysis code.

  3. These changes are preparatory steps for integrating the new intensity fitter (the one you’ve already seen). This time, however, we’ll proceed step by step in separate MRs.

If we decide that the absolute is better, then the parameter should also be renamed, since rho is used in the current definition by the linked references.

Sure - we can rename it to impact_parameter.

I.e. rho should only be used for the relative quantity.

Ok.

In general, please separate bug fixes like the modulo check from improvements like the pixel type scaling from API changes.

Ok.

radius: float or ndarray
radius of circle
rho: float or ndarray
impact_point: float or ndarray

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.

point implies 2d, but we pass radial coordinates r, phi0. So I'd call it impact_distance (as also the docsting already uses)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

ok

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Dear @maxnoe, this is a friendly reminder about this MR — let’s merge it :-)

@ctao-sonarqube

ctao-sonarqube Bot commented Oct 9, 2025

Copy link
Copy Markdown

Quality Gate failed Quality Gate failed

Failed conditions
69.8% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube

@burmist-git burmist-git requested a review from maxnoe October 20, 2025 08:55
pixels_on_circle = int(circumference / pixel_diameter)

ang = phi + linspace_two_pi(pixels_on_circle * oversampling)
ang = linspace_two_pi(pixels_on_circle * oversampling) - phi

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.

Please explain this change. Why do you change the order of this array?

@burmist-git burmist-git Oct 20, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

https://github.com/cta-observatory/ctapipe/pull/2845/files/397903ad39453ed0227fb8928d124e4b95c26e92#r2444300605

These two changes are linked. The reason is that phi0 should be subtracted from phi. In this way, the fitted value of phi0 gives us the azimuth of the impact point, whereas before it was giving us the complementary angle of the impact point. This was leading to inconsistencies between the reconstructed and true xcore and ycore values. In addition to the coordinate frame transformation.

image

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.

But phi0 is subtracted from phi. Now you again subtract phi from the array of all angles.

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: if you are changing the coordinate system of the muon fit, don't you think this is a major change that needs to be mentioned both in the motivation for the pull request and in the changelog?

Please, explain what you are doing so that people can actually review and understand.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Please note that phi0 is set to 0. As we agree do not change the current fitter.

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.

Even more misleading.

Please... do not mix bug fixes with such changes.

I would propose to close this merge request.

Please re-open the many changes you do here in a way that they are separate, their impact can be properly explained, assessed and understood.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Well reuse this one for Fix the normalization for muon analysis to account for different PixelShapes.
I will rename the MR.

ang = np.arctan2(dy, dx)
# Add muon rotation angle
ang += phi_rad
ang -= phi_rad

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.

Why do you change here + to -?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@burmist-git burmist-git changed the title Muon fit chord length with phi0 Fix the normalization for muon analysis to account for different PixelShapes. Oct 20, 2025
@burmist-git burmist-git deleted the muon_fit_chord_length_with_phi0 branch October 20, 2025 13:16
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