Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvement to spatial flexure #1870

Merged
merged 32 commits into from
Dec 20, 2024
Merged

Improvement to spatial flexure #1870

merged 32 commits into from
Dec 20, 2024

Conversation

debora-pe
Copy link
Collaborator

@debora-pe debora-pe commented Nov 13, 2024

This work makes improvements to the spatial flexure calculation and add a QA plot to check the flexure.
Documentation was also added.

It addresses issue #1843

Dev-Suite PR pypeit/PypeIt-development-suite#339

@rcooke-ast rcooke-ast changed the base branch from develop to dev2rel November 16, 2024 21:05
Base automatically changed from dev2rel to develop November 18, 2024 16:28
Copy link
Collaborator

@rcooke-ast rcooke-ast left a comment

Choose a reason for hiding this comment

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

Thanks @debora-pe - Overall this looks good, but it will be good to make sure the dev suite passes. Also, it would be good to check what happens when the slit edges are quite diagonal. I also have a few general questions:

(1) Could you mention the reason why this new algorithm is required?
(2) My main concern is that the spectral smashing might smear out the slit edges. Is there are reason that the ross-correlation needs to be done in 1D instead of 2D (along one dimension)?
(3) I quite like that the cross-correlation is done on the sobel images with this algorithm. Would it also work to use the old algorithm, but instead use the sobel edges? This would avoid the smashing.

sci_sobel, _ = trace.detect_slit_edges(_sciimg, bpm=bpm)
slits_sobel, _ = trace.detect_slit_edges(slitmask, bpm=bpm)
# collapse both sobel images along the spectral direction
sci_smash, _, _ = sigma_clipped_stats(sci_sobel, axis=0, mask=bpm)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this work well in cases where the slit edges are significantly tilted wrt detector columns?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, it does seems to work well. I was worried too at first, but if the slits edges are tilted for the trace image they will be tilted also for the science image.

sci_smash, _, _ = sigma_clipped_stats(sci_sobel, axis=0, mask=bpm)
slits_smash, _, _ = sigma_clipped_stats(slits_sobel, axis=0, mask=bpm)
# remove nan values
sci_smash[np.isnan(sci_smash)] = 0
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it possible to remove the nan values from the 2D image, instead (before smashing)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The nan values are actually created by sigma_clipped_stats when encounters the masked values. This is why I added it here.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Right, OK... Since the default is cenfunc=median, could you instead call sigma_clipped_stats with cenfunc=np.nanmedian, as this will ignore the masked pixels instead of making them nans. Could this work?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm also very surprised that a median operation is still able to correctly determine the flexure when the slits are tilted. Intuitively, this doesn't make sense to me, especially given that a median operation will reject the significant edges that are enhanced by the sobel filter. Do you have any example QA plots to share showing the two 1D inputs (sci_smash and slits_smash), as well as the peak of the cross-correlation for your example (LRIS) as well as a tilted spectrograph (either ESI or HIRES)?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps one possible solution is to consider a row-by-row cross-correlation of the Sobel images (possibly with a median filter applied), and then do the cross-correlation along one axis. Something like this:

corr = signal.fftconvolve(sci, np.fliplr(slits), mode='same', axes=0)

(although, you'd need to check if it's a fliplr or flipud, and if the axes variable is correct... you'd want them both along the spatial direction).

sci_smash[np.isnan(sci_smash)] = 0
slits_smash[np.isnan(slits_smash)] = 0
# invert the negative values
sci_smash[sci_smash < 0] *= -1
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this needed? I think keeping the left edges and right edges as +ve/-ve might actually help the cross-correlation.

Copy link
Collaborator

Choose a reason for hiding this comment

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

good point

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I tested this a lot and it seems that inverting the negative value is still better than keeping them negative. In many cases it does not make much difference, but in some cases the cross-correlation is better with the inversion.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if the median operation is "cancelling" the positive and negative values for slits that are close together? This could explain why making both left and right edges positive helps determine the slit location.


"""
debug = True if outfile is None else False
# TODO: should we use initial or tweaked slits in this plot?
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 it's the tweaked slits that you're after. As you noted above on line 85, whatever is used here needs to be consistent with that choice. I think once the tweaked slits are available, they get used.

Copy link
Collaborator

Choose a reason for hiding this comment

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

yes, I believe that is correct.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Well in the code above I'm still using the initial slits (not the tweaked ones) since:
slitmask = slits.slit_img(initial=True, exclude_flag=slits.bitmask.exclude_for_flexure)
It seems to me that the initial ones should work better in the cross-correlation, and therefore this plot should be consistent with that. But I do not want users to get confused when seeing this plot and comparing with what is actually use for reduction, i.e., tweaked slits if tweak_slits=True

pypeit/core/flexure.py Show resolved Hide resolved
Copy link
Collaborator

@profxj profxj left a comment

Choose a reason for hiding this comment

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

a few minor comments from me.

am really happy to see this code advanced.

doc/figures/r230417_01033_DET01_spat_flex_corr.png Outdated Show resolved Hide resolved
pypeit/core/arc.py Show resolved Hide resolved
sci_smash[np.isnan(sci_smash)] = 0
slits_smash[np.isnan(slits_smash)] = 0
# invert the negative values
sci_smash[sci_smash < 0] *= -1
Copy link
Collaborator

Choose a reason for hiding this comment

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

good point

#tslits_shift = trace_slits.shift_slits(tslits_dict, lag_max)
# Now translate the tilts
# 2D plot
spat_flexure_qa(sciimg, slits, shift, gpm=np.logical_not(bpm), vrange=qa_vrange)
Copy link
Collaborator

Choose a reason for hiding this comment

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

remove this line? it is duplicated below.
or is the idea to generate 2 PNGs?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No, this one is used only in debug mode and plot in a matplot window the whole image. No PNG is created. I uses the same script, but it does slightly different things.


"""
debug = True if outfile is None else False
# TODO: should we use initial or tweaked slits in this plot?
Copy link
Collaborator

Choose a reason for hiding this comment

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

yes, I believe that is correct.

if np.sum(no_nan) > 0:
if np.any(np.absolute(np.diff(spat_flex[no_nan])) > 0.1):
msgs.warn(f'Spatial flexure is not consistent for all images being combined: {spat_flex}.')
comb_spat_flex = np.round(np.mean(spat_flex[no_nan]),3)
Copy link
Collaborator

Choose a reason for hiding this comment

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

is there a preference to rounding to the nearest pixel instead of staying a float ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It is only rounding to the 3rd decimal. It's still a float. But the mean calculation can make it to be a very long number.

pypeit/images/rawimage.py Show resolved Hide resolved
Copy link
Collaborator Author

@debora-pe debora-pe left a comment

Choose a reason for hiding this comment

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

@rcooke-ast @profxj I have addressed your comments.
I have extensively tested these changes with all the Keck LRIS data in the Dev Suite and all looks good. I will anyway re-run the whole Dev Suite.

@rcooke-ast Here are my answers to your questions:

(1) Could you mention the reason why this new algorithm is required?

The old algorithm was not working well. A peak in the cross-correlation was often not found and the spatial flexure was not corrected.

(2) My main concern is that the spectral smashing might smear out the slit edges. Is there are reason that the ross-correlation needs to be done in 1D instead of 2D (along one dimension)?

As I mentioned in another comment, I was little worried too, but after a lot of testing (also with more tilted slit edges) the correction looks reasonable. I thought about a 2D cross-correlation but I wasn't able to find a way to do it without adding a lot of runtime (it's probably me that I don't know how to deal with it)

(3) I quite like that the cross-correlation is done on the sobel images with this algorithm. Would it also work to use the old algorithm, but instead use the sobel edges? This would avoid the smashing.

I have also tested a lot using the sobel edges, and the correction is not consistently good.

sci_sobel, _ = trace.detect_slit_edges(_sciimg, bpm=bpm)
slits_sobel, _ = trace.detect_slit_edges(slitmask, bpm=bpm)
# collapse both sobel images along the spectral direction
sci_smash, _, _ = sigma_clipped_stats(sci_sobel, axis=0, mask=bpm)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, it does seems to work well. I was worried too at first, but if the slits edges are tilted for the trace image they will be tilted also for the science image.

sci_smash[np.isnan(sci_smash)] = 0
slits_smash[np.isnan(slits_smash)] = 0
# invert the negative values
sci_smash[sci_smash < 0] *= -1
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I tested this a lot and it seems that inverting the negative value is still better than keeping them negative. In many cases it does not make much difference, but in some cases the cross-correlation is better with the inversion.

#tslits_shift = trace_slits.shift_slits(tslits_dict, lag_max)
# Now translate the tilts
# 2D plot
spat_flexure_qa(sciimg, slits, shift, gpm=np.logical_not(bpm), vrange=qa_vrange)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No, this one is used only in debug mode and plot in a matplot window the whole image. No PNG is created. I uses the same script, but it does slightly different things.

pypeit/core/flexure.py Show resolved Hide resolved

"""
debug = True if outfile is None else False
# TODO: should we use initial or tweaked slits in this plot?
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Well in the code above I'm still using the initial slits (not the tweaked ones) since:
slitmask = slits.slit_img(initial=True, exclude_flag=slits.bitmask.exclude_for_flexure)
It seems to me that the initial ones should work better in the cross-correlation, and therefore this plot should be consistent with that. But I do not want users to get confused when seeing this plot and comparing with what is actually use for reduction, i.e., tweaked slits if tweak_slits=True

if np.sum(no_nan) > 0:
if np.any(np.absolute(np.diff(spat_flex[no_nan])) > 0.1):
msgs.warn(f'Spatial flexure is not consistent for all images being combined: {spat_flex}.')
comb_spat_flex = np.round(np.mean(spat_flex[no_nan]),3)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It is only rounding to the 3rd decimal. It's still a float. But the mean calculation can make it to be a very long number.

sci_smash, _, _ = sigma_clipped_stats(sci_sobel, axis=0, mask=bpm)
slits_smash, _, _ = sigma_clipped_stats(slits_sobel, axis=0, mask=bpm)
# remove nan values
sci_smash[np.isnan(sci_smash)] = 0
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The nan values are actually created by sigma_clipped_stats when encounters the masked values. This is why I added it here.

Copy link
Collaborator

@rcooke-ast rcooke-ast left a comment

Choose a reason for hiding this comment

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

Thanks for the responses @debora-pe! I think it would be helpful to see some example plots of the two signals that you are correlating, for examples with tilted and non-tilted slits.

I always thought the flexure was done by correlating the slits and science frame along the spatial direction, but this seems to never have been the case. I can see that your algorithm is an improvement over what was previously in the code, but I've also proposed another possible solution (correlating 2D images along one direction). I think this would work best, particularly if we used the sobel filtering (without converting -ve numbers to +ve numbers). What do you think?

sci_smash, _, _ = sigma_clipped_stats(sci_sobel, axis=0, mask=bpm)
slits_smash, _, _ = sigma_clipped_stats(slits_sobel, axis=0, mask=bpm)
# remove nan values
sci_smash[np.isnan(sci_smash)] = 0
Copy link
Collaborator

Choose a reason for hiding this comment

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

Right, OK... Since the default is cenfunc=median, could you instead call sigma_clipped_stats with cenfunc=np.nanmedian, as this will ignore the masked pixels instead of making them nans. Could this work?

sci_smash, _, _ = sigma_clipped_stats(sci_sobel, axis=0, mask=bpm)
slits_smash, _, _ = sigma_clipped_stats(slits_sobel, axis=0, mask=bpm)
# remove nan values
sci_smash[np.isnan(sci_smash)] = 0
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm also very surprised that a median operation is still able to correctly determine the flexure when the slits are tilted. Intuitively, this doesn't make sense to me, especially given that a median operation will reject the significant edges that are enhanced by the sobel filter. Do you have any example QA plots to share showing the two 1D inputs (sci_smash and slits_smash), as well as the peak of the cross-correlation for your example (LRIS) as well as a tilted spectrograph (either ESI or HIRES)?

sci_smash[np.isnan(sci_smash)] = 0
slits_smash[np.isnan(slits_smash)] = 0
# invert the negative values
sci_smash[sci_smash < 0] *= -1
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if the median operation is "cancelling" the positive and negative values for slits that are close together? This could explain why making both left and right edges positive helps determine the slit location.

sci_smash, _, _ = sigma_clipped_stats(sci_sobel, axis=0, mask=bpm)
slits_smash, _, _ = sigma_clipped_stats(slits_sobel, axis=0, mask=bpm)
# remove nan values
sci_smash[np.isnan(sci_smash)] = 0
Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps one possible solution is to consider a row-by-row cross-correlation of the Sobel images (possibly with a median filter applied), and then do the cross-correlation along one axis. Something like this:

corr = signal.fftconvolve(sci, np.fliplr(slits), mode='same', axes=0)

(although, you'd need to check if it's a fliplr or flipud, and if the axes variable is correct... you'd want them both along the spatial direction).

@debora-pe
Copy link
Collaborator Author

Thanks for the responses @debora-pe! I think it would be helpful to see some example plots of the two signals that you are correlating, for examples with tilted and non-tilted slits.

I always thought the flexure was done by correlating the slits and science frame along the spatial direction, but this seems to never have been the case. I can see that your algorithm is an improvement over what was previously in the code, but I've also proposed another possible solution (correlating 2D images along one direction). I think this would work best, particularly if we used the sobel filtering (without converting -ve numbers to +ve numbers). What do you think?

Allright @rcooke-ast . Thank you for pushing back on this. I have extensively tests several methods and indeed using a row-by-row cross-correlation works better for very curved slits (like ESI). Bright objects can affect the cross-correlation a lot, so I have added a way to approximately mask the object. Also, I found that the cross-correlation works better with the edge image generated from the sobel image, so I am using that. Let me know what you think.
I have tested these changes with all the Dev-Suite data that we have for LRIS, DEIMOS, HIRES, ESI, SHANE and have added some more tests in the Dev-Suite PR.

@profxj , if you can, take a look as well, since I have made substantial changes.

I'll run the full Dev-Suite as well.

Copy link
Collaborator

@rcooke-ast rcooke-ast 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, thanks @debora-pe! Just two minor comments from me, and you have my 👍 to merge when the dev suite passes.

# mask (as much as possible) the objects on the slits to help the cross-correlation
# need to copy the bpm to avoid changing the input bpm
_bpm = np.zeros_like(_sciimg, dtype=int) if bpm is None else copy.deepcopy(bpm)
for i in range(slits.left_init.shape[1]):
Copy link
Collaborator

Choose a reason for hiding this comment

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

slits.nslits might be a little clearer

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

# need to copy the bpm to avoid changing the input bpm
_bpm = np.zeros_like(_sciimg, dtype=int) if bpm is None else copy.deepcopy(bpm)
for i in range(slits.left_init.shape[1]):
left_edge = slits.left_init[:, i].astype(int)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not a big deal, but should we do:
np.round(slits.left_init[:, i]).astype(int)
and likewise for right_edge?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

@debora-pe
Copy link
Collaborator Author

Finally I was able to run the Dev Suite (my computer was annoying!). All good. The only failure is the usual setup_gui unit test that my computer have problem with.

Test Summary
--------------------------------------------------------
--- PYTEST PYPEIT UNIT TESTS PASSED  257 passed, 63 warnings in 340.40s (0:05:40) ---
--- PYTEST UNIT TESTS FAILED  1 failed, 151 passed, 168 warnings in 2751.88s (0:45:51) ---
--- PYTEST VET TESTS PASSED  65 passed, 101 warnings in 5384.05s (1:29:44) ---
--- PYPEIT DEVELOPMENT SUITE PASSED 249/249 TESTS  ---
Testing Started at 2024-12-16T17:18:33.164290
Testing Completed at 2024-12-19T15:03:25.137859
Total Time: 2 days, 21:44:51.973569

@debora-pe debora-pe merged commit eb54214 into develop Dec 20, 2024
26 checks passed
@debora-pe debora-pe deleted the spatflex_improve branch December 20, 2024 04:28
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