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

Scattered light model #1704

Merged
merged 80 commits into from
Nov 24, 2023
Merged

Scattered light model #1704

merged 80 commits into from
Nov 24, 2023

Conversation

rcooke-ast
Copy link
Collaborator

This PR introduces a "proper" scattered light model using the inter-slit pixels (with some padding to reject light near the slit edges). This has been primarily tested with KCWI data (some tweaks are still being implemented), but this PR contains the main base to model the scattered light.

@rcooke-ast rcooke-ast changed the title Scattlight model Scattered light model Oct 15, 2023
@rcooke-ast
Copy link
Collaborator Author

Partially addresses issue #1684

@codecov-commenter
Copy link

codecov-commenter commented Oct 15, 2023

Codecov Report

Attention: 284 lines in your changes are missing coverage. Please review.

Comparison is base (70cfb02) 40.97% compared to head (5d88464) 40.90%.

Files Patch % Lines
pypeit/core/scattlight.py 9.34% 97 Missing ⚠️
pypeit/images/rawimage.py 5.71% 66 Missing ⚠️
pypeit/calibrations.py 33.33% 28 Missing ⚠️
pypeit/scripts/chk_scattlight.py 28.57% 25 Missing ⚠️
pypeit/scattlight.py 52.17% 22 Missing ⚠️
pypeit/display/display.py 7.14% 13 Missing ⚠️
pypeit/spectrographs/keck_kcwi.py 35.00% 13 Missing ⚠️
pypeit/spectrographs/keck_esi.py 12.50% 7 Missing ⚠️
pypeit/spectrographs/spectrograph.py 14.28% 6 Missing ⚠️
pypeit/utils.py 16.66% 5 Missing ⚠️
... and 1 more

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1704      +/-   ##
===========================================
- Coverage    40.97%   40.90%   -0.08%     
===========================================
  Files          190      193       +3     
  Lines        43914    44300     +386     
===========================================
+ Hits         17994    18119     +125     
- Misses       25920    26181     +261     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Collaborator

@kbwestfall kbwestfall left a comment

Choose a reason for hiding this comment

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

Thanks, @rcooke-ast ! I've got lots of comments for you. The most significant ones (I think) are:

  • Update the docs, and more specifically KCWI docs with a description of the new approach
  • Add some tests (will there be an associated dev-suite PR?)
  • I'm confused about the control flow
  • It feels like we can make the scattered-light modeling code more general

I'm fine if you want to push off the latter two points to future PRs.

pypeit/calibrations.py Outdated Show resolved Hide resolved
pypeit/calibrations.py Outdated Show resolved Hide resolved
pypeit/calibrations.py Outdated Show resolved Hide resolved
pypeit/core/framematch.py Outdated Show resolved Hide resolved
pypeit/images/rawimage.py Outdated Show resolved Hide resolved
spl = interpolate.RectBivariateSpline(specvec, spatvec, scale_img, kx=1, ky=1)
return spl(zoom * (specvec + shft_spec), zoom * (spatvec + shft_spat))

def scattered_light(self, frame, offslitmask, tbl, detpad=300):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Similar comment: I would generalize and move much/most of this to scattlight.py.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

See comment above. Perhaps we can discuss this further on Slack, but the reason I've implemented it like this is because I suspect the scattered light model requires spectrograph specific code.

pypeit/spectrographs/keck_kcwi.py Outdated Show resolved Hide resolved
pypeit/spectrographs/keck_kcwi.py Outdated Show resolved Hide resolved
pypeit/spectrographs/spectrograph.py Outdated Show resolved Hide resolved
if not success:
msgs.warn("Scattered light model failed - using predefined model parameters")
# Use predefined model parameters
scatt_img = self.spectrograph.scattered_light_model(scattlight.scattlight_param, self.image[ii, ...])
Copy link
Collaborator

Choose a reason for hiding this comment

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

My understanding of the current flow is:

  • If scattered-light frames are provided, fit a scattered light model to them. If they are not, scattered light is never subtracted.
  • For the science frames, the code will try to independently model the scattered light in them (only if scattered light frames are provided). If that fails, use the scattered light model from the scattered-light frames.

Is this right? If not, can you describe the flow?

If so, can we enable the scattered-light model to be subtracted from the science data regardless of whether or not you provide scattered-light frames?

Also, if I understand the source of the scattered light for KCWI, at least, (i.e., its intensity is proportional to the light flux through the system), then is it appropriate to subtract the model from the science frames given the significant difference in flux?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Your understanding of the control flow is correct. But, I'm still deciding what is the best approach. I think it's best to general the model parameters and always use those, unless the user explicitly wants to calculate the scattered light model from each individual frame. I will change the flow such that it's like that, and document it here so the flow is clearer.

If so, can we enable the scattered-light model to be subtracted from the science data regardless of whether or not you provide scattered-light frames?

Yes, that is possible, and I think that may even be better in some cases... still testing...

Also, if I understand the source of the scattered light for KCWI, at least, (i.e., its intensity is proportional to the light flux through the system), then is it appropriate to subtract the model from the science frames given the significant difference in flux?

That is correct. This is precisely what is currently being done. For each setup there's a set of model parameters that:
(1) take the light distribution of the current frame (e.g. a science frame)
(2) based on this light distribution on the detector (and the pre-defined model parameters), calculate the scattered light image
(3) subtract this scattered light image from the current frame.

@profxj
Copy link
Collaborator

profxj commented Oct 16, 2023

Head's up that I am going to branch off this and try it on ESI.

We can then compare to the old IDL code (by having @jhennawi run it)

model : `numpy.ndarray`_
Model of the scattered light for the input
"""
# Extract the parameters into more conveniently named variables
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 reason why you don't just use the simpler:

sigmx_g, sigmy_g, sigmx_l, sigmy_l, shft_spec, shft_spat, zoom_spec, zoom_spat, constant, kern_angle, kern_scale, polyterms_spat, polyterms_spec = param

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not really, I just didn't want a very long line of code, so I put these into sensible groups. Also, polyterms_spat is two parameters, while polyterms_spec can have any number of free parameters, so they have to be separated.

Raw 2D data frame to be used to compute the scattered light.
bpm : `numpy.ndarray`_
2D boolean array indicating the bad pixels (True=bad)
offslitmask : `numpy.ndarray`_
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you specify image shapes for frame and then state that bpm and offslitmask are the same size.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good idea - done!

else: # Everything else in between
indx = (spat[None, :] > centrace[:, ii-1, None]) \
& (spat[None, :] < centrace[:, ii, None])
# Exclude these pixels
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since offslitmask is boolean, I think the logic would be clearer if you did something like

good_mask = offslitmask & np.logical_not(indx)

I would also name indx to indicate what it means, i.e. bad_pixels or something like that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good idea - I've changed the logic to make it clearer, as you suggest, and changed the name.

model_med = ndimage.median_filter(model, size=(50, 1)) # Median filter to get rid of CRs
scatt_light_fine = ndimage.gaussian_filter(model_med, sigma=10) # Gaussian filter to smooth median filter
if debug:
embed()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remove the embed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removed


Parameters
----------
frame : `numpy.ndarray`_
Copy link
Collaborator

Choose a reason for hiding this comment

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

Reminder, image shapes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep, good call 👍

# Use predefined model parameters
scatt_img = scattlight.scattered_light_model(this_modpar, _img)
if debug:
embed()
Copy link
Collaborator

Choose a reason for hiding this comment

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

remove embed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removed

scatt_img = scattlight.scattered_light_model(this_modpar, _img)
if debug:
embed()
# import astropy.io.fits as fits
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remove?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removed

f" {tmp[10]}, # Relative kernel scale (>1 means the kernel is more Gaussian, >0 but <1 makes the profile more lorentzian)\n" + \
f" {tmp[11]}, {tmp[12]}, # Polynomial terms (coefficients of \"spat\" and \"spat*spec\")\n" + \
f" {tmp[13]}, {tmp[14]}, {tmp[15]}]) # Polynomial terms (coefficients of spec**index)\n"
print(strprint)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This printing should be via msgs right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is in the debug statement. It's only used for developers that want a pretty printout of the best-fit parameters to include in the spectrograph file. I've added a more detailed comment now to make this a little more obvious!

defaults['finecorr_mask'] = None
dtypes['finecorr_mask'] = [int, list]
descr['finecorr_mask'] = 'A list containing the inter-slit regions that the user wishes to mask during ' \
'the fine correction to the scattered light. Each integer corresponds to an ' \
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just mention that the defaults mean no additional masking.

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.

Copy link
Collaborator

@jhennawi jhennawi left a comment

Choose a reason for hiding this comment

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

Hi @rcooke-ast excellent work on this PR. The code looks great. I have minor comments throughout that I hope you can look at, but I'm approving anyway. Only one substantive question which is why do you opt to hard-code the parameters in the spectrograph file versus fitting them from flats every time? Is it that the fitting is too time consuming? Also, I'm not sure I followed how things actually work. Do you fit for a template and then determine its amplitude from flats to account for the fact that the scattered light depends on the total amount of light? Sorry if these questions are naive.

Copy link
Collaborator Author

@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 @jhennawi for the comments! Let me respond to your other queries:

Only one substantive question which is why do you opt to hard-code the parameters in the spectrograph file versus fitting them from flats every time? Is it that the fitting is too time consuming?

The hard-coded parameters in the spectrograph files just provide the initial guesses for the fitting. The scattered light model parameters are always derived using the user's data (unless they request to use the archived model parameters). It's a little time-consuming, so starting somewhere near the expected solution speeds things up and makes sure the solution doesn't get stuck in model space far from the expected "best" solution.

Do you fit for a template and then determine its amplitude from flats to account for the fact that the scattered light depends on the total amount of light?

The model currently assumes that the scattered light is a reprocessed version of the detector image (this seems to work well for both ESI and KCWI). So, with this approach, the model parameters are assumed to be the same for all frames, the only thing that changes is the image of the detector (i.e. the detector image + any fixed [pre-calculated] model parameters determine the total scattered light). This way, we can derive the model parameters using a ScatteredLight calibration frame, and then apply these model parameters (without any adjustment) to any other frame.

I've also updated the calibrations/sacttlight.rst docs to include all of these details, and hopefully that makes things a little clearer.

I hope that makes sense!

model : `numpy.ndarray`_
Model of the scattered light for the input
"""
# Extract the parameters into more conveniently named variables
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not really, I just didn't want a very long line of code, so I put these into sensible groups. Also, polyterms_spat is two parameters, while polyterms_spec can have any number of free parameters, so they have to be separated.

Raw 2D data frame to be used to compute the scattered light.
bpm : `numpy.ndarray`_
2D boolean array indicating the bad pixels (True=bad)
offslitmask : `numpy.ndarray`_
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good idea - done!

else: # Everything else in between
indx = (spat[None, :] > centrace[:, ii-1, None]) \
& (spat[None, :] < centrace[:, ii, None])
# Exclude these pixels
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good idea - I've changed the logic to make it clearer, as you suggest, and changed the name.


Parameters
----------
frame : `numpy.ndarray`_
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep, good call 👍

model_med = ndimage.median_filter(model, size=(50, 1)) # Median filter to get rid of CRs
scatt_light_fine = ndimage.gaussian_filter(model_med, sigma=10) # Gaussian filter to smooth median filter
if debug:
embed()
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removed

# Use predefined model parameters
scatt_img = scattlight.scattered_light_model(this_modpar, _img)
if debug:
embed()
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removed

scatt_img = scattlight.scattered_light_model(this_modpar, _img)
if debug:
embed()
# import astropy.io.fits as fits
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removed

f" {tmp[10]}, # Relative kernel scale (>1 means the kernel is more Gaussian, >0 but <1 makes the profile more lorentzian)\n" + \
f" {tmp[11]}, {tmp[12]}, # Polynomial terms (coefficients of \"spat\" and \"spat*spec\")\n" + \
f" {tmp[13]}, {tmp[14]}, {tmp[15]}]) # Polynomial terms (coefficients of spec**index)\n"
print(strprint)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is in the debug statement. It's only used for developers that want a pretty printout of the best-fit parameters to include in the spectrograph file. I've added a more detailed comment now to make this a little more obvious!

defaults['finecorr_mask'] = None
dtypes['finecorr_mask'] = [int, list]
descr['finecorr_mask'] = 'A list containing the inter-slit regions that the user wishes to mask during ' \
'the fine correction to the scattered light. Each integer corresponds to an ' \
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.

@rcooke-ast
Copy link
Collaborator Author

All comments addressed. Running the dev suite again to make sure nothing is broken 🤞

@rcooke-ast
Copy link
Collaborator Author

Tests pass... merging
image

@rcooke-ast rcooke-ast merged commit 9d8bf9e into develop Nov 24, 2023
23 checks passed
@rcooke-ast rcooke-ast deleted the scattlight_model branch November 24, 2023 06:52
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.

5 participants