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

Implementing Calibrated Contrast Measurements #91

Merged
merged 33 commits into from
Dec 21, 2023
Merged

Implementing Calibrated Contrast Measurements #91

merged 33 commits into from
Dec 21, 2023

Conversation

AarynnCarter
Copy link
Collaborator

@AarynnCarter AarynnCarter commented Aug 11, 2023

Putting together all the code necessary to perform calibrated contrast measurements.

  • Implement masking for MIRI FQPM's so that raw contrasts can be computed
  • Restructure pyklippipeline.py so that pyKLIP datasets can be created more easily
  • Implement companion injection into pyKLIP datasets using existing pyKLIP functions
  • Implement calibrated contrast calculation

Resolves #79, Resolves #85

Started by implementing the MIRI FQPM masking, this is improved from original spaceKLIP and uses rectangles instead of pizza slices.

image

@AarynnCarter
Copy link
Collaborator Author

AarynnCarter commented Aug 11, 2023

The SpaceTelescope(Data) class that was in pyklippipeline.py has now been removed. Instead, I have ported this over to pyKLIP itself under a new jwst_new branch: https://bitbucket.org/pyKLIP/pyklip/src/jwst_new/pyklip/instruments/JWST.py.

Important: We shouldn't merge this spaceKLIP branch until we are happy with the pyKLIP file / have merged things into pyKLIP's jwst branch.

EDIT: Don't worry about failing checks at the moment, they are related to the code wanting to use the older version of pyKLIP.

@AarynnCarter
Copy link
Collaborator Author

Companion injection and recovery is now coded up, based on fitting 2D Gaussians to estimate the peak flux of injected sources after KLIP subtraction. For MIRI this is probably okay, but perhaps we should do something smarter for NIRCam in the future.

@AarynnCarter
Copy link
Collaborator Author

Okay calibrated contrast curves can now be calculated. I won't merge in just yet, as I think further thought needs to go into retrieved throughputs >1. Plus, it would be great if a few people could test this first. @mperrin, @kammerje, I know you are on vacation right now, but perhaps when you get back you could review what I've done.

image

image

@AarynnCarter
Copy link
Collaborator Author

Added some minor bug fixes.

Resolves #88, Resolves #92

@mperrin
Copy link
Collaborator

mperrin commented Aug 28, 2023

@AarynnCarter FYI I'm back from vacation, still playing catchup on many things. I see & acknowledge your request for me to take a look at this, and will try to do so within the next ~2 weeks (i.e. before the conference in a couple weeks here).

Is this "ready to go" from your perspective? I see it's still a Draft PR. And can you remind me if there's a plan yet for timing re the related pyKLIP branch merges etc?

@AarynnCarter
Copy link
Collaborator Author

@mperrin This should be good for review, yes. I might add some minor bug fixes in if needed. This is a somewhat complicated PR as we'll need to make sure pyKLIP is updated first, so once you think it's good to go I will coordinate with Jason so that everything gets updated together.

@AarynnCarter AarynnCarter marked this pull request as ready for review August 28, 2023 20:08
@AarynnCarter AarynnCarter changed the base branch from main to develop August 29, 2023 17:32
@AarynnCarter
Copy link
Collaborator Author

@AarynnCarter Add logging for calibrated contrast steps

Copy link
Collaborator

@mperrin mperrin left a comment

Choose a reason for hiding this comment

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

Apologies for the delay in this -- I started this review many weeks ago but got interrupted and didn't get back to it until now.

The comments are mostly suggestions for clarification, possible improvement in parts. I don't see any show stoppers to merging this, but I think a round of revisions would be beneficial, so this review is for now just a "comment", neither an approve nor a formal request for changes.

@@ -4,5 +4,6 @@ jdaviz
jwst
matplotlib
numpy
pysynphot
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would urge you to reconsider this dependency, since pysynphot is deprecated and no longer actively developed. Instead synphot should be preferred as the current tool, with similar but slightly different API after a refactoring. synphot is actively developed and supported.

Copy link
Collaborator

Choose a reason for hiding this comment

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

And yes, this is an annoyance from upstream, but wanted to make sure you were aware.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Looks like this is tied to the psf.py file, which is built upon WebbPSF_ext. I 100% agree we should change, but don't want to bloat this PR further / I think this will need a careful hand to make sure we don't break anything. Instead, I've made a new issue so that we don't lose track.

@@ -121,6 +124,16 @@ def raw_contrast(self,
output_dir = os.path.join(self.database.output_dir, subdir)
if not os.path.exists(output_dir):
os.makedirs(output_dir)

# Copy the starfile that will be used into this directory
starfile_type = starfile.split('.')[-1]
Copy link
Collaborator

Choose a reason for hiding this comment

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

minor: use os.path.splitext for this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Changed.

Comment on lines 130 to 136
new_starfile_path = output_dir+'/user_starfile.'+starfile_type
header = '#'+starfile.split('/')[-1] + ' /// {}'.format(spectral_type)+'\n'
with open(new_starfile_path, 'w') as new_starfile:
with open(starfile, 'r') as orig_starfile:
new_starfile.write(header)
for line in orig_starfile.readlines():
new_starfile.write(line)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor: I see what you're doing here, I guess, but this seems kinda a low-level kludge, to copy a data file line by line just to append a comment text at the start. This is a minor point and I'm OK if you want to leave as-is, it just strikes me as an unnecessary complication, and from a code style perspective is an interruption that breaks up the conceptual flow, and breaks layers of abstraction. Consider encapsulating this sort of thing into utility functions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Moved function to utils.py, streamlined it a bit, and added an error flag if the file can't be found.

nanmask[nanmask<0.5] = 1

# Downsample, remove padding, and mask data
nanmask = nanmask[::samp,::samp]
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't follow the logic here about oversampling. You make the mask oversampled by 15x, but here you downsample to every 15th sample, without any averaging at all. So what is the point of the upsampling? The vast majority of the upsampled pixels (1 - (1/15)**2, so 99.1%!) are discarded without ever being used.

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 sampling is for the nanmask, so there isn't really any averaging to do. A pixel should either be == 1 (not masked) or == np.nan (masked). I found that by upsampling the array initially, the rectangles that I apply can be better centered in the image, perhaps I could adjust how the rectangles are being made to remove the need to upsample but it seems to work right now and it's relatively quick.

However, by looking at this I spotted that some pixels, despite being surrounded by NaNs, weren't being masked. I've added a new utility function to catch these and fix them.

@@ -269,6 +339,7 @@ def raw_contrast(self,
ax.plot(seps[k], cons[k], color=colors[k % mod], alpha=0.3)
ax.plot(seps[k], cons_mask[k], color=colors[k % mod], label=klmodes[k] + ' KL')
ax.set_yscale('log')
ax.set_ylim([None,1])
Copy link
Collaborator

Choose a reason for hiding this comment

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

A general comment on the raw_contrast plots (not this particular line of code): When running in a notebook, it would be nice if the PDF plots are displayed in the notebook, as well as saved to disk. At minimum, the text in the function that writes out like Contrast results saved to [filename etc] should also state that PDF plots were saved into that directory.

The point here is to help make it more discoverable and obvious to users that these files now exist, and where to find them...

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'm not familiar enough with notebooks to understand why things aren't being plotted there, maybe that's best left for a future improvement? I did add a quick adjustment to the print statement that mentions plots have been saved.

plt.savefig(save_string + '_calcon.pdf',
bbox_inches='tight', dpi=300)

pass
Copy link
Collaborator

Choose a reason for hiding this comment

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

pass is unnecessary here

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

raw_dataset : pyKLIP dataset
A pyKLIP dataset which companions will be injected into and KLIP
will be performed on.
true_companions :
Copy link
Collaborator

Choose a reason for hiding this comment

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

doc string missing info for true_companions

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed


Returns
-------
TBD
Copy link
Collaborator

Choose a reason for hiding this comment

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

???

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added

head_pri['XOFFSET'] = xoffset
head_pri['YOFFSET'] = yoffset
head_pri['XOFFSET'] = xoffset / 1e3 #Should be saved in arcseconds for FITS file
head_pri['YOFFSET'] = yoffset / 1e3 #Should be saved in arcseconds for FITS file
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ideally this ought to include a FITS header comment giving the units!

head_pri['XOFFSET'] = (xoffset, '[arcsec] X offset in arcseconds')
head_pri['YOFFSET'] = (yoffset, '[arcsec] Y offset in arcseconds')

(See FITS standard, section 4.3.2)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

/ 1e3 was conflicted with the develop branch and has been removed (hence the reason I was confused why it was there). Totally agree on FITS header comments, but this is an issue that extends beyond the XOFFSET and YOFFSET values - we haven't done the best job at including these. I'll make a separate issue to address this code wide.

@@ -1988,9 +1991,10 @@ def align_frames(self,
f = plt.figure(figsize=(6.4, 4.8))
ax = plt.gca()
for index, j in enumerate(ww_sci):
ax.scatter(shifts_all[index][:, 0] * self.database.obs[key]['PIXSCALE'][j], shifts_all[index][:, 1] * self.database.obs[key]['PIXSCALE'][j], s=5, color=colors[index], marker='o', label='PA = %.0f deg' % self.database.obs[key]['ROLL_REF'][j])
ax.scatter(shifts_all[index][:, 0] * self.database.obs[key]['PIXSCALE'][j], shifts_all[index][:, 1] * self.database.obs[key]['PIXSCALE'][j], s=5, color=colors[index%10], marker='o', label='PA = %.0f deg' % self.database.obs[key]['ROLL_REF'][j])
Copy link
Collaborator

Choose a reason for hiding this comment

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

(code style) This line is really dense and hard to read... Consider reformatting over a couple of lines

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

@AarynnCarter
Copy link
Collaborator Author

Latest commit Resolves #119

Copy link
Collaborator

@mperrin mperrin left a comment

Choose a reason for hiding this comment

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

Reviewed, tested extensively, some debugging and additional enhancements to plots added. Discussed in Slack with @AarynnCarter and @kammerje.

Working; looks good to me. 👍

@AarynnCarter AarynnCarter merged commit 2847c9a into develop Dec 21, 2023
2 of 4 checks passed
@mperrin mperrin deleted the calcon branch February 6, 2024 12:10
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.

contrast measurement code doesn't work for MIRI? Add injection-recovery tests
2 participants