|
8 | 8 | import time |
9 | 9 |
|
10 | 10 | import asdf |
| 11 | +import numpy as np |
11 | 12 | import jwst |
12 | 13 | from jwst.datamodels import ModelContainer |
13 | 14 | from jwst.resample import ResampleStep |
| 15 | +from jwst.resample.resample import compute_image_pixel_area |
| 16 | +from stcal.outlier_detection.utils import gwcs_blot |
14 | 17 |
|
15 | 18 | # FIXME For newer JWST versions, import ModelLibrary |
16 | 19 | try: |
@@ -66,6 +69,10 @@ def __init__( |
66 | 69 | bgr_background_name="off", |
67 | 70 | process_bgr_like_science=False, |
68 | 71 | jwst_parameters=None, |
| 72 | + do_drizzle=None, |
| 73 | + do_blot=False, |
| 74 | + blot_ref_index=0, |
| 75 | + blot_fillval=np.nan, |
69 | 76 | overwrite=False, |
70 | 77 | ): |
71 | 78 | """Wrapper around the level 3 JWST pipeline |
@@ -113,6 +120,22 @@ def __init__( |
113 | 120 | jwst_parameters: Parameter dictionary to pass to |
114 | 121 | the level 2 pipeline. Defaults to None, |
115 | 122 | which will run the observatory defaults |
| 123 | + do_drizzle: If True, drizzle individual frames |
| 124 | + to the i2d mosaic WCS after the main |
| 125 | + pipeline run. Note: creates a lot of files. |
| 126 | + If not set (None) and do_blot is True, |
| 127 | + do_drizzle is automatically enabled. If |
| 128 | + explicitly set to False while do_blot is |
| 129 | + True, a ValueError is raised. Defaults to |
| 130 | + None. |
| 131 | + do_blot: If True, blot each resampled exposure |
| 132 | + back to a reference detector frame (requires |
| 133 | + drizzling). Defaults to False. |
| 134 | + blot_ref_index: Index into the alphabetically sorted CRF file list of |
| 135 | + the reference exposure whose detector frame |
| 136 | + is used for blotting. Defaults to 0. |
| 137 | + blot_fillval: Fill value for pixels outside the |
| 138 | + blotted footprint. Defaults to np.nan |
116 | 139 | overwrite: Whether to overwrite or not. Defaults |
117 | 140 | to False |
118 | 141 | """ |
@@ -152,6 +175,18 @@ def __init__( |
152 | 175 | self.bgr_background_name = bgr_background_name |
153 | 176 | self.process_bgr_like_science = process_bgr_like_science |
154 | 177 | self.jwst_parameters = jwst_parameters |
| 178 | + if do_drizzle is False and do_blot: |
| 179 | + raise ValueError( |
| 180 | + "do_drizzle was explicitly set to False, but do_blot " |
| 181 | + "was set to True. Either set (do_drizzle, do_blot) to " |
| 182 | + "(True, True) or (True, False), or (None, True)." |
| 183 | + ) |
| 184 | + if do_drizzle is None: |
| 185 | + do_drizzle = bool(do_blot) |
| 186 | + self.do_drizzle = do_drizzle |
| 187 | + self.do_blot = do_blot |
| 188 | + self.blot_ref_index = blot_ref_index |
| 189 | + self.blot_fillval = blot_fillval |
155 | 190 | self.overwrite = overwrite |
156 | 191 |
|
157 | 192 | self.band_type = get_band_type(self.band) |
@@ -608,60 +643,143 @@ def run_step( |
608 | 643 | gc.collect() |
609 | 644 |
|
610 | 645 | # Drizzle individual frames to the common mosaic WCS |
611 | | - i2d_files = glob.glob(os.path.join(self.out_dir, "*_i2d.fits")) |
612 | | - crf_files = sorted(glob.glob(os.path.join(self.out_dir, "*_crf.fits"))) |
613 | | - |
614 | | - if i2d_files and crf_files: |
615 | | - ref_wcs_file = os.path.join(self.out_dir, "resample_refwcs.asdf") |
616 | | - with datamodels.open(i2d_files[0]) as i2d_model: |
617 | | - asdf.AsdfFile({ |
618 | | - "wcs": i2d_model.meta.wcs, |
619 | | - "array_shape": i2d_model.data.shape, |
620 | | - }).write_to(ref_wcs_file) |
621 | | - |
622 | | - resample_single = ResampleStep() |
623 | | - resample_single.single = True |
624 | | - resample_single.output_use_model = True |
625 | | - resample_single.save_results = True |
626 | | - resample_single.output_dir = self.out_dir |
627 | | - resample_single.output_wcs = ref_wcs_file |
628 | | - |
629 | | - try: |
630 | | - resample_params = self.jwst_parameters["resample"] |
631 | | - except KeyError: |
632 | | - resample_params = {} |
633 | | - |
634 | | - wcs_keys = { |
635 | | - "output_wcs", "crval", "crpix", "rotation", |
636 | | - "pixel_scale", "pixel_scale_ratio", "output_shape", |
637 | | - } |
638 | | - for resample_key in resample_params: |
639 | | - if resample_key in wcs_keys: |
640 | | - continue |
641 | | - value = parse_parameter_dict( |
642 | | - parameters=resample_params, |
643 | | - key=resample_key, |
644 | | - band=self.band, |
645 | | - target=self.target, |
646 | | - ) |
647 | | - if value == "VAL_NOT_FOUND": |
648 | | - continue |
649 | | - recursive_setattr(resample_single, resample_key, value) |
| 646 | + if self.do_drizzle: |
| 647 | + i2d_files = glob.glob(os.path.join(self.out_dir, "*_i2d.fits")) |
| 648 | + crf_files = sorted(glob.glob(os.path.join(self.out_dir, "*_crf.fits"))) |
| 649 | + |
| 650 | + if i2d_files and crf_files: |
| 651 | + ref_wcs_file = os.path.join(self.out_dir, "resample_refwcs.asdf") |
| 652 | + with datamodels.open(i2d_files[0]) as i2d_model: |
| 653 | + asdf.AsdfFile({ |
| 654 | + "wcs": i2d_model.meta.wcs, |
| 655 | + "array_shape": i2d_model.data.shape, |
| 656 | + }).write_to(ref_wcs_file) |
| 657 | + |
| 658 | + resample_single = ResampleStep() |
| 659 | + resample_single.single = True |
| 660 | + resample_single.output_use_model = True |
| 661 | + resample_single.save_results = True |
| 662 | + resample_single.output_dir = self.out_dir |
| 663 | + resample_single.output_wcs = ref_wcs_file |
| 664 | + |
| 665 | + try: |
| 666 | + resample_params = self.jwst_parameters["resample"] |
| 667 | + except KeyError: |
| 668 | + resample_params = {} |
| 669 | + |
| 670 | + wcs_keys = { |
| 671 | + "output_wcs", "crval", "crpix", "rotation", |
| 672 | + "pixel_scale", "pixel_scale_ratio", "output_shape", |
| 673 | + } |
| 674 | + for resample_key in resample_params: |
| 675 | + if resample_key in wcs_keys: |
| 676 | + continue |
| 677 | + value = parse_parameter_dict( |
| 678 | + parameters=resample_params, |
| 679 | + key=resample_key, |
| 680 | + band=self.band, |
| 681 | + target=self.target, |
| 682 | + ) |
| 683 | + if value == "VAL_NOT_FOUND": |
| 684 | + continue |
| 685 | + recursive_setattr(resample_single, resample_key, value) |
| 686 | + |
| 687 | + resample_single.run(crf_files) |
| 688 | + |
| 689 | + del resample_single |
| 690 | + gc.collect() |
| 691 | + |
| 692 | + # Notes: |
| 693 | + # - ResampleImage.resample_many_to_many() adds _outlier_resamplestep.fits |
| 694 | + # to the file names, but it's not an outlier image, so let's rename it. |
| 695 | + # - We'd have to edit ResampleStep for a proper fix. |
| 696 | + # - Can't have suffix ending in _i2d.fits, messes with anchoring pattern matching. |
| 697 | + for f in glob.glob(os.path.join(self.out_dir, "*_outlier_resamplestep.fits")): |
| 698 | + os.rename(f, f.replace("_outlier_resamplestep.fits", "_i2d_single.fits")) |
| 699 | + |
| 700 | + if self.do_blot: |
| 701 | + self._blot_to_detector_frame( |
| 702 | + crf_files=crf_files, |
| 703 | + ref_index=self.blot_ref_index, |
| 704 | + ) |
| 705 | + else: |
| 706 | + log.warning("do_drizzle is set but no i2d/crf files found") |
650 | 707 |
|
651 | | - resample_single.run(crf_files) |
| 708 | + return True |
652 | 709 |
|
653 | | - del resample_single |
654 | | - gc.collect() |
| 710 | + def _blot_to_detector_frame( |
| 711 | + self, |
| 712 | + crf_files, |
| 713 | + ref_index=0, |
| 714 | + ): |
| 715 | + """Blot resampled single-exposure images back to a detector frame. |
655 | 716 |
|
656 | | - # Notes: |
657 | | - # - ResampleImage.resample_many_to_many() adds _outlier_resamplestep.fits |
658 | | - # to the file names, but it's not an outlier image, so let's rename it. |
659 | | - # - We'd have to edit ResampleStep for a proper fix. |
660 | | - # - Can't have suffix ending in _i2d.fits, messes with anchoring pattern matching. |
661 | | - for f in glob.glob(os.path.join(self.out_dir, "*_outlier_resamplestep.fits")): |
662 | | - os.rename(f, f.replace("_outlier_resamplestep.fits", "_i2d_single.fits")) |
| 717 | + Each ``*_i2d_single.fits`` file (one per exposure, on the common |
| 718 | + i2d WCS grid) is blotted onto the detector frame of the chosen |
| 719 | + reference exposure. The output files are written as |
| 720 | + ``*_blot_det.fits`` alongside the other lv3 products. |
663 | 721 |
|
664 | | - return True |
| 722 | + Args: |
| 723 | + crf_files: Sorted list of CRF file paths (one per exposure). |
| 724 | + Used to obtain the detector-frame GWCS and shape. |
| 725 | + ref_index: Index into *crf_files* of the reference exposure |
| 726 | + whose detector frame will be adopted. Defaults to 0 |
| 727 | + (the first exposure). |
| 728 | + """ |
| 729 | + |
| 730 | + single_files = sorted( |
| 731 | + glob.glob(os.path.join(self.out_dir, "*_i2d_single.fits")) |
| 732 | + ) |
| 733 | + if not single_files: |
| 734 | + log.warning("_blot_to_detector_frame: no *_i2d_single.fits files found") |
| 735 | + return |
| 736 | + |
| 737 | + if ref_index >= len(crf_files): |
| 738 | + log.warning( |
| 739 | + "_blot_to_detector_frame: ref_index %d out of range (%d exposures)", |
| 740 | + ref_index, len(crf_files), |
| 741 | + ) |
| 742 | + return |
| 743 | + |
| 744 | + log.info( |
| 745 | + "Blotting %d resampled exposures to detector frame of %s", |
| 746 | + len(single_files), os.path.basename(crf_files[ref_index]), |
| 747 | + ) |
| 748 | + |
| 749 | + with datamodels.open(crf_files[ref_index]) as ref_model: |
| 750 | + blot_wcs = ref_model.meta.wcs |
| 751 | + blot_shape = ref_model.data.shape |
| 752 | + |
| 753 | + ref_pixflux_area = ref_model.meta.photometry.pixelarea_steradians |
| 754 | + blot_wcs.array_shape = blot_shape |
| 755 | + ref_pixel_area = compute_image_pixel_area(blot_wcs) |
| 756 | + pix_ratio = np.sqrt(ref_pixflux_area / ref_pixel_area) |
| 757 | + |
| 758 | + for single_file in single_files: |
| 759 | + with datamodels.open(single_file) as single_model: |
| 760 | + blotted = gwcs_blot( |
| 761 | + median_data=single_model.data.astype(np.float32), |
| 762 | + median_wcs=single_model.meta.wcs, |
| 763 | + blot_shape=blot_shape, |
| 764 | + blot_wcs=blot_wcs, |
| 765 | + pix_ratio=pix_ratio, |
| 766 | + fillval=self.blot_fillval, |
| 767 | + ) |
| 768 | + |
| 769 | + out_name = single_file.replace("_i2d_single.fits", "_blot_det.fits") |
| 770 | + blot_model = datamodels.ImageModel(data=blotted) |
| 771 | + blot_model.update(ref_model) |
| 772 | + blot_model.meta.wcs = copy.deepcopy(blot_wcs) |
| 773 | + save_file(blot_model, out_name=out_name, dr_version=self.dr_version) |
| 774 | + blot_model.close() |
| 775 | + |
| 776 | + log.info( |
| 777 | + " %s -> %s", |
| 778 | + os.path.basename(single_file), |
| 779 | + os.path.basename(out_name), |
| 780 | + ) |
| 781 | + |
| 782 | + gc.collect() |
665 | 783 |
|
666 | 784 | def propagate_metadata(self, |
667 | 785 | files): |
|
0 commit comments