def test_model_set_raises_value_error(expr, result): """Check that creating model sets with components whose _n_models are different raise a value error """ with pytest.raises(ValueError): s = expr(Const1D((2, 2), n_models=2), Const1D(3, n_models=1))
def extract_grism_objects(input_model, grism_objects=None, reference_files=None, extract_orders=None, mmag_extract=None, compute_wavelength=True, wfss_extract_half_height=None, nbright=None): """ Extract 2d boxes around each objects spectra for each order. Parameters ---------- input_model : `~jwst.datamodels.ImageModel` An instance of an ImageModel, this is the grism image grism_objects : list(GrismObject) A list of GrismObjects reference_files : dict Needs to include the name of the wavelengthrange reference file extract_orders : int Spectral orders to extract mmag_extract : float Sources with magnitudes fainter than this minimum magnitude extraction cutoff will not be extracted compute_wavelength : bool Compute a wavelength array for the datamodel. wfss_extract_half_height : int, (optional) Cross-dispersion extraction half height in pixels, WFSS mode. Overwrites the computed extraction height. nbright : int Number of brightest objects to extract for WFSS mode. Returns ------- output_model : `~jwst.datamodels.MultiSlitModel` Notes ----- This method supports NRC_WFSS and NIS_WFSS only GrismObject is a named tuple which contains distilled information about each catalog object. It can be created by calling jwst.assign_wcs.util.create_grism_bbox() which will return a list of GrismObjects that contain the bounding boxes that will be used to define the 2d extraction area. For each spectral order, the configuration file contains a magnitude-cutoff value. The total list of objects to extract is limited by both MMAG_EXTRACT and NBRIGHT. Sources with magnitudes fainter than the extraction cutoff (MMAG_EXTRACT) will not be extracted, but are accounted for when computing the spectral contamination and background estimates. The default value is 99 right now. NBRIGHT further limits the list to the NBRIGHT brightest objects. The default value is 999 right now. The sensitivity information from the original aXe style configuration file needs to be modified by the passband of the filter used for the direct image to get the min and max wavelengths which correspond to t=0 and t=1, this currently has been done by the team and the min and max wavelengths to use to calculate t are stored in the grism reference file as wavelengthrange. Step 1: Convert the source catalog from the reference frame of the uberimage to that of the dispersed image. For the Vanilla Pipeline we assume that the pointing information in the file headers is sufficient. This will be strictly true if all images were obtained in a single visit (same guide stars). Step 2: Record source information for each object in the catalog: position (RA and Dec), shape (A_IMAGE, B_IMAGE, THETA_IMAGE), and all available magnitudes, and minimum bounding boxes Step 3: Compute the trace and wavelength solutions for each object in the catalog and for each spectral order. Record this information. Step 4: Compute the WIDTH of each spectral subwindow, which may be fixed or variable. The cross-dispersion size is taken from the minimum bounding box. """ if reference_files is None or not reference_files: raise TypeError("Expected a dictionary for reference_files") if grism_objects is None: # get the wavelengthrange reference file from the input_model if ('wavelengthrange' not in reference_files or reference_files['wavelengthrange'] in ['N/A', '']): raise ValueError("Expected name of wavelengthrange reference file") else: grism_objects = util.create_grism_bbox( input_model, reference_files, extract_orders=extract_orders, mmag_extract=mmag_extract, wfss_extract_half_height=wfss_extract_half_height, nbright=nbright) log.info( "Grism object list created from source catalog: {0:s}".format( input_model.meta.source_catalog)) if not isinstance(grism_objects, list): raise TypeError("Expected input grism objects to be a list") if len(grism_objects) == 0: raise ValueError("No grism objects created from source catalog") log.info("Extracting %d grism objects", len(grism_objects)) output_model = datamodels.MultiSlitModel() output_model.update(input_model) # One WCS model can be used to govern all the extractions # and in fact the model transforms rely on the full frame # coordinates of the input pixel location. So the WCS # attached to the extraction is just a copy of the # input_model WCS with a shift transform to the corner # of the subarray. They also depend on the source object # center, this information will be saved to the meta of # the output model as source_[x/y]pos inwcs = input_model.meta.wcs # For easy reference here, GrismObjects has: # # xcenter,ycenter: in direct image pixels # order_bounding in grism_detector pixels # sky_centroid: SkyCoord of object center # sky_bbox_ :lower and upper bounding box in SkyCoord # sid: catalog ID of the object slits = [] for obj in grism_objects: for order in obj.order_bounding.keys(): # Add the shift to the lower corner to each subarray WCS object # The shift should just be the lower bounding box corner # also replace the object center location inputs to the GrismDispersion # model with the known object center and order information (in pixels of direct image) # This is changes the user input to the model from (x,y,x0,y0,order) -> (x,y) # # The bounding boxes here are also limited to the size of the detector # The check for boxes entirely off the detector is done in create_grism_bbox right now y, x = obj.order_bounding[order] # limit the boxes to the detector ymin = clamp(y[0], 0, input_model.meta.subarray.ysize) ymax = clamp(y[1], 0, input_model.meta.subarray.ysize) xmin = clamp(x[0], 0, input_model.meta.subarray.xsize) xmax = clamp(x[1], 0, input_model.meta.subarray.xsize) # don't extract anything that ended up with zero dimensions in one axis # this means that it was identified as a partial order but only on one # row or column of the detector if (((ymax - ymin) > 0) and ((xmax - xmin) > 0)): subwcs = copy.deepcopy(inwcs) log.info("Subarray extracted for obj: {} order: {}:".format( obj.sid, order)) log.info("Subarray extents are: " "(xmin:{}, xmax:{}), (ymin:{}, ymax:{})".format( xmin, xmax, ymin, ymax)) # only the first two numbers in the Mapping are used # the order and source position are put directly into # the new wcs for the subarray for the forward transform xcenter_model = Const1D(obj.xcentroid) xcenter_model.inverse = Const1D(obj.xcentroid) ycenter_model = Const1D(obj.ycentroid) ycenter_model.inverse = Const1D(obj.ycentroid) order_model = Const1D(order) order_model.inverse = Const1D(order) tr = inwcs.get_transform('grism_detector', 'detector') tr = Mapping((0, 1, 0, 0, 0)) | (Shift(xmin) & Shift(ymin) & xcenter_model & ycenter_model & order_model) | tr ext_data = input_model.data[ymin:ymax + 1, xmin:xmax + 1].copy() ext_err = input_model.err[ymin:ymax + 1, xmin:xmax + 1].copy() ext_dq = input_model.dq[ymin:ymax + 1, xmin:xmax + 1].copy() if input_model.var_poisson is not None and np.size( input_model.var_poisson) > 0: var_poisson = input_model.var_poisson[ymin:ymax + 1, xmin:xmax + 1].copy() else: var_poisson = None if input_model.var_rnoise is not None and np.size( input_model.var_rnoise) > 0: var_rnoise = input_model.var_rnoise[ymin:ymax + 1, xmin:xmax + 1].copy() else: var_rnoise = None if input_model.var_flat is not None and np.size( input_model.var_flat) > 0: var_flat = input_model.var_flat[ymin:ymax + 1, xmin:xmax + 1].copy() else: var_flat = None tr.bounding_box = util.transform_bbox_from_shape( ext_data.shape) subwcs.set_transform('grism_detector', 'detector', tr) new_slit = datamodels.SlitModel(data=ext_data, err=ext_err, dq=ext_dq, var_poisson=var_poisson, var_rnoise=var_rnoise, var_flat=var_flat) new_slit.meta.wcsinfo.spectral_order = order new_slit.meta.wcsinfo.dispersion_direction = \ input_model.meta.wcsinfo.dispersion_direction new_slit.meta.wcsinfo.specsys = input_model.meta.wcsinfo.specsys new_slit.meta.coordinates = input_model.meta.coordinates new_slit.meta.wcs = subwcs if compute_wavelength: log.debug("Computing wavelengths") new_slit.wavelength = compute_wavelength_array(new_slit) # set x/ystart values relative to the image (screen) frame. # The overall subarray offset is recorded in model.meta.subarray. # nslit = obj.sid - 1 # catalog id starts at zero new_slit.name = "{0}".format(obj.sid) new_slit.is_extended = obj.is_extended new_slit.xstart = xmin + 1 # fits pixels new_slit.xsize = ext_data.shape[1] new_slit.ystart = ymin + 1 # fits pixels new_slit.ysize = ext_data.shape[0] new_slit.source_xpos = float(obj.xcentroid) new_slit.source_ypos = float(obj.ycentroid) new_slit.source_id = obj.sid new_slit.bunit_data = input_model.meta.bunit_data new_slit.bunit_err = input_model.meta.bunit_err slits.append(new_slit) output_model.slits.extend(slits) # In the case that there are no spectra to extract deleting the variables # will fail so add the try block. try: del subwcs except UnboundLocalError: pass try: del new_slit except UnboundLocalError: pass # del subwcs # del new_slit log.info("Finished extractions") return output_model
def test_model_set(expr, result): s = expr(Const1D((2, 2), n_models=2), Const1D((3, 3), n_models=2)) out = s(0, model_set_axis=False) assert_array_equal(out, result)
def create_spectral_wcs(ra, dec, wavelength): """Assign a WCS for sky coordinates and a table of wavelengths Parameters ---------- ra: float The right ascension (in degrees) at the nominal location of the entrance aperture (slit). dec: float The declination (in degrees) at the nominal location of the entrance aperture. wavelength: ndarray The wavelength in microns at each pixel of the extracted spectrum. Returns: -------- wcs: a gwcs.wcs.WCS object This takes a float or sequence of float and returns a tuple of the right ascension, declination, and wavelength (or sequence of wavelengths) at the pixel(s) specified by the input argument. """ input_frame = cf.CoordinateFrame(naxes=1, axes_type=("SPATIAL", ), axes_order=(0, ), unit=(u.pix, ), name="pixel_frame") sky = cf.CelestialFrame(name='sky', axes_order=(0, 1), reference_frame=coord.ICRS()) spec = cf.SpectralFrame(name='spectral', axes_order=(2, ), unit=(u.micron, ), axes_names=('wavelength', )) world = cf.CompositeFrame([sky, spec], name='world') pixel = np.arange(len(wavelength), dtype=float) tab = Mapping((0, 0, 0)) | \ Const1D(ra) & Const1D(dec) & Tabular1D(points=pixel, lookup_table=wavelength, bounds_error=False, fill_value=None) tab.name = "pixel_to_world" if all(np.diff(wavelength) > 0): tab.inverse = Mapping((2, )) | Tabular1D( points=wavelength, lookup_table=pixel, bounds_error=False, ) elif all(np.diff(wavelength) < 0): tab.inverse = Mapping((2, )) | Tabular1D( points=wavelength[::-1], lookup_table=pixel[::-1], bounds_error=False, ) else: log.warning( "Wavelengths are not strictly monotonic, inverse transform is not set" ) pipeline = [(input_frame, tab), (world, None)] return WCS(pipeline)
def extract_tso_object(input_model, reference_files=None, tsgrism_extract_height=None, extract_orders=None, compute_wavelength=True): """ Extract the spectrum for a NIRCam TSO grism observation. Parameters ---------- input_model : `~jwst.datamodels.CubeModel` or `~jwst.datamodels.ImageModel` The input TSO data is an instance of a CubeModel (3D) or ImageModel (2D) reference_files : dict Needs to include the name of the wavelengthrange reference file tsgrism_extract_height : int The extraction height, in total, for the spectrum in the cross-dispersion direction. If this is other than None, it will override the team default of 64 pixels. The team wants the source centered near row 34, so the extraction height is not the same on either size of the central row. extract_orders : list[ints] This is an optional parameter that will override the orders specified for extraction in the wavelengthrange reference file. compute_wavelength : bool Compute a wavelength array for the datamodel. Returns ------- output_model : `~jwst.datamodels.SlitModel` Notes ----- This method supports NRC_TSGRISM only, where only one bright object is considered in the field, so there's no catalog of sources and the object is assumed to have been observed at the aperture reference position. The aperture reference location is read during level-1b (uncal) product creation by the "set_telescope_pointing" script from the SIAF entries XSciRef and YSciRef (reference location in the science frame) and saved as "meta.wcsinfo.siaf_xref_sci" and "meta.wcsinfo.siaf_yref_sci" (FITS header keywords XREF_SCI and YREF_SCI). Because this mode has a single known source location, the utilities used in the WFSS modes are overkill. Instead, similar structures are created during the extract2d process and then directly used. https://jwst-docs.stsci.edu/near-infrared-camera/nircam-observing-modes/nircam-time-series-observations/nircam-grism-time-series """ # Check for reference files if not isinstance(reference_files, dict): raise TypeError("Expected a dictionary for reference_files") # Check for wavelengthrange reference file if 'wavelengthrange' not in reference_files: raise KeyError("No wavelengthrange reference file specified") # If an extraction height is not supplied, default to entire # cross-dispersion size of the data array if tsgrism_extract_height is None: tsgrism_extract_height = input_model.meta.subarray.ysize log.info("Setting extraction height to {}".format(tsgrism_extract_height)) # Get the disperser parameters that have the wave limits with WavelengthrangeModel(reference_files['wavelengthrange']) as f: if (f.meta.instrument.name != 'NIRCAM' or f.meta.exposure.type != 'NRC_TSGRISM'): raise ValueError( "Wavelengthrange reference file is not for NIRCAM TSGRISM mode!" ) wavelengthrange = f.wavelengthrange ref_extract_orders = f.extract_orders # If user-supplied spectral orders are not provided, # default to extracting only the 1st order if extract_orders is None: log.info("Using default order extraction from reference file") extract_orders = ref_extract_orders available_orders = [ x[1] for x in extract_orders if x[0] == input_model.meta.instrument.filter ].pop() else: if (not isinstance(extract_orders, list) or not all(isinstance(item, int) for item in extract_orders)): raise TypeError( "Expected extract_orders to be a list of integers.") available_orders = extract_orders if len(available_orders) > 1: raise NotImplementedError("Multiple order extraction for TSO is " "not currently implemented.") # Check for the existence of the aperture reference location meta data if (input_model.meta.wcsinfo.siaf_xref_sci is None or input_model.meta.wcsinfo.siaf_yref_sci is None): raise ValueError('XREF_SCI and YREF_SCI are required for TSO mode.') # Create the extracted output as a SlitModel log.info("Extracting order: {}".format(available_orders)) output_model = datamodels.SlitModel() output_model.update(input_model) subwcs = copy.deepcopy(input_model.meta.wcs) # Loop over spectral orders for order in available_orders: range_select = [ (x[2], x[3]) for x in wavelengthrange if (x[0] == order and x[1] == input_model.meta.instrument.filter) ] # Use the filter that was in front of the grism for translation lmin, lmax = range_select.pop() # Create the order bounding box source_xpos = input_model.meta.wcsinfo.siaf_xref_sci - 1 # remove FITS 1-indexed offset source_ypos = input_model.meta.wcsinfo.siaf_yref_sci - 1 # remove FITS 1-indexed offset transform = input_model.meta.wcs.get_transform('direct_image', 'grism_detector') xmin, ymin, _ = transform(source_xpos, source_ypos, lmin, order) xmax, ymax, _ = transform(source_xpos, source_ypos, lmax, order) # Add the shift to the lower corner to the subarray WCS object. # The shift should just be the lower bounding box corner. # Also replace the object center location inputs to the GrismDispersion # model with the known object center and order information (in pixels of direct image) # This changes the user input to the model from (x,y,x0,y0,order) -> (x,y) # # The bounding box is limited to the size of the detector in the dispersion direction # and 64 pixels in the cross-dispersion direction (at request of instrument team). # # The team wants the object to fall near row 34 for all cutouts, but the default cutout # height is 64 pixels (32 on either side). So bump the extraction ycenter, when necessary, # so that the height is 30 above and 34 below (in full frame) the object center. bump = source_ypos - 34 extract_y_center = source_ypos - bump splitheight = int(tsgrism_extract_height / 2) below = extract_y_center - splitheight if below == 34: extract_y_min = 0 extract_y_max = extract_y_center + splitheight elif below < 0: extract_y_min = 0 extract_y_max = tsgrism_extract_height - 1 else: extract_y_min = extract_y_center - 34 # always return source at row 34 in cutout extract_y_max = extract_y_center + tsgrism_extract_height - 34 - 1 # Check for bad results if extract_y_min > extract_y_max: raise ValueError( "Something bad happened calculating extraction y-size") # Limit the bounding box to the detector edges ymin, ymax = (max(extract_y_min, 0), min(extract_y_max, input_model.meta.subarray.ysize)) xmin, xmax = (max(xmin, 0), min(xmax, input_model.meta.subarray.xsize)) # The order and source position are put directly into the new WCS of the subarray # for the forward transform. # # NOTE NOTE NOTE 2020-02-14 # We would normally use x-axis (along dispersion) extraction limits calculated # above based on the min/max wavelength range and the source position to do the # subarray extraction and set the subarray WCS accordingly. HOWEVER, the NIRCam # team has asked for all data along the dispersion direction to be included in # subarray cutout, so here we override the xmin/xmax values calculated above and # instead hardwire the extraction limits for the x (dispersion) direction to # cover the entire range of the data and use this new minimum x value in the # subarray WCS transform. If the team ever decides to change the extraction limits, # the following two constants must be modified accordingly. xmin_ext = 0 # hardwire min x for extraction to zero xmax_ext = input_model.data.shape[ -1] - 1 # hardwire max x for extraction to size of data order_model = Const1D(order) order_model.inverse = Const1D(order) tr = input_model.meta.wcs.get_transform('grism_detector', 'direct_image') tr = Mapping( (0, 1, 0)) | Shift(xmin_ext) & Shift(ymin) & order_model | tr subwcs.set_transform('grism_detector', 'direct_image', tr) xmin = int(xmin) xmax = int(xmax) ymin = int(ymin) ymax = int(ymax) log.info("WCS made explicit for order: {}".format(order)) log.info("Spectral trace extents: (xmin: {}, ymin: {}), " "(xmax: {}, ymax: {})".format(xmin, ymin, xmax, ymax)) log.info("Extraction limits: (xmin: {}, ymin: {}), " "(xmax: {}, ymax: {})".format(xmin_ext, ymin, xmax_ext, ymax)) # Cut out the subarray from the input data arrays ext_data = input_model.data[..., ymin:ymax + 1, xmin_ext:xmax_ext + 1].copy() ext_err = input_model.err[..., ymin:ymax + 1, xmin_ext:xmax_ext + 1].copy() ext_dq = input_model.dq[..., ymin:ymax + 1, xmin_ext:xmax_ext + 1].copy() if input_model.var_poisson is not None and np.size( input_model.var_poisson) > 0: var_poisson = input_model.var_poisson[..., ymin:ymax + 1, xmin_ext:xmax_ext + 1].copy() else: var_poisson = None if input_model.var_rnoise is not None and np.size( input_model.var_rnoise) > 0: var_rnoise = input_model.var_rnoise[..., ymin:ymax + 1, xmin_ext:xmax_ext + 1].copy() else: var_rnoise = None if input_model.var_flat is not None and np.size( input_model.var_flat) > 0: var_flat = input_model.var_flat[..., ymin:ymax + 1, xmin_ext:xmax_ext + 1].copy() else: var_flat = None # Finish populating the output model and meta data if output_model.meta.model_type == "SlitModel": output_model.data = ext_data output_model.err = ext_err output_model.dq = ext_dq output_model.var_poisson = var_poisson output_model.var_rnoise = var_rnoise output_model.var_flat = var_flat output_model.meta.wcs = subwcs output_model.meta.wcs.bounding_box = util.wcs_bbox_from_shape( ext_data.shape) output_model.meta.wcsinfo.siaf_yref_sci = 34 # update for the move, vals are the same output_model.meta.wcsinfo.siaf_xref_sci = input_model.meta.wcsinfo.siaf_xref_sci output_model.meta.wcsinfo.spectral_order = order output_model.meta.wcsinfo.dispersion_direction = \ input_model.meta.wcsinfo.dispersion_direction if compute_wavelength: log.debug("Computing wavelengths (this takes a while ...)") output_model.wavelength = compute_wavelength_array( output_model) output_model.name = '1' output_model.source_type = input_model.meta.target.source_type output_model.source_name = input_model.meta.target.catalog_name output_model.source_alias = input_model.meta.target.proposer_name output_model.xstart = 1 # FITS pixels are 1-indexed output_model.xsize = ext_data.shape[-1] output_model.ystart = ymin + 1 # FITS pixels are 1-indexed output_model.ysize = ext_data.shape[-2] output_model.source_xpos = source_xpos output_model.source_ypos = 34 output_model.source_id = 1 output_model.bunit_data = input_model.meta.bunit_data output_model.bunit_err = input_model.meta.bunit_err if hasattr(input_model, 'int_times'): output_model.int_times = input_model.int_times.copy() del subwcs log.info("Finished extraction") return output_model
def gwa_to_ifuslit(slits, input_model, disperser, reference_files): """ GWA to SLIT transform. Parameters ---------- slits : list A list of slit IDs for all IFU slits 0-29. disperser : dict A corrected disperser ASDF object. filter : str The filter used. grating : str The grating used in the observation. reference_files: dict Dictionary with reference files returned by CRDS. Returns ------- model : `~jwst.transforms.Gwa2Slit` model. Transform from GWA frame to SLIT frame. """ ymin = -.55 ymax = .55 wrange = (input_model.meta.wcsinfo.waverange_start, input_model.meta.wcsinfo.waverange_end), order = input_model.meta.wcsinfo.spectral_order agreq = angle_from_disperser(disperser, input_model) lgreq = wavelength_from_disperser(disperser, input_model) # The wavelength units up to this point are # meters as required by the pipeline but the desired output wavelength units is microns. # So we are going to Scale the spectral units by 1e6 (meters -> microns) if input_model.meta.instrument.filter == 'OPAQUE': lgreq = lgreq | Scale(1e6) collimator2gwa = collimator_to_gwa(reference_files, disperser) mask = mask_slit(ymin, ymax) ifuslicer = AsdfFile.open(reference_files['ifuslicer']) ifupost = AsdfFile.open(reference_files['ifupost']) slit_models = [] ifuslicer_model = ifuslicer.tree['model'] for slit in slits: slitdata = ifuslicer.tree['data'][slit] slitdata_model = get_slit_location_model(slitdata) ifuslicer_transform = (slitdata_model | ifuslicer_model) ifupost_transform = ifupost.tree[slit]['model'] msa2gwa = ifuslicer_transform | ifupost_transform | collimator2gwa gwa2msa = gwa_to_ymsa(msa2gwa) # TODO: Use model sets here bgwa2msa = Mapping((0, 1, 0, 1), n_inputs=3) | \ Const1D(0) * Identity(1) & Const1D(-1) * Identity(1) & Identity(2) | \ Identity(1) & gwa2msa & Identity(2) | \ Mapping((0, 1, 0, 1, 2, 3)) | Identity(2) & msa2gwa & Identity(2) | \ Mapping((0, 1, 2, 3, 5), n_inputs=7) | Identity(2) & lgreq | mask # msa to before_gwa msa2bgwa = msa2gwa & Identity(1) | Mapping((3, 0, 1, 2)) | agreq bgwa2msa.inverse = msa2bgwa slit_models.append(bgwa2msa) ifuslicer.close() ifupost.close() return Gwa2Slit(slits, slit_models)
def gwa_to_slit(open_slits, input_model, disperser, reference_files): """ GWA to SLIT transform. Parameters ---------- open_slits : list A list of slit IDs for all open shutters/slitlets. disperser : dict A corrected disperser ASDF object. filter : str The filter used. grating : str The grating used in the observation. reference_files: dict Dictionary with reference files returned by CRDS. Returns ------- model : `~jwst.transforms.Gwa2Slit` model. Transform from GWA frame to SLIT frame. """ wrange = (input_model.meta.wcsinfo.waverange_start, input_model.meta.wcsinfo.waverange_end), order = input_model.meta.wcsinfo.spectral_order agreq = angle_from_disperser(disperser, input_model) collimator2gwa = collimator_to_gwa(reference_files, disperser) lgreq = wavelength_from_disperser(disperser, input_model) # The wavelength units up to this point are # meters as required by the pipeline but the desired output wavelength units is microns. # So we are going to Scale the spectral units by 1e6 (meters -> microns) if input_model.meta.instrument.filter == 'OPAQUE': lgreq = lgreq | Scale(1e6) msa = AsdfFile.open(reference_files['msa']) slit_models = [] for quadrant in range(1, 6): slits_in_quadrant = [s for s in open_slits if s.quadrant == quadrant] log.info("There are {0} open slits in quadrant {1}".format( len(slits_in_quadrant), quadrant)) if any(slits_in_quadrant): msa_model = msa.tree[quadrant]['model'] log.info( "Getting slits location for quadrant {0}".format(quadrant)) msa_data = msa.tree[quadrant]['data'] for slit in slits_in_quadrant: mask = mask_slit(slit.ymin, slit.ymax) slit_id = slit.shutter_id slitdata = msa_data[slit_id] slitdata_model = get_slit_location_model(slitdata) msa_transform = slitdata_model | msa_model msa2gwa = (msa_transform | collimator2gwa) gwa2msa = gwa_to_ymsa(msa2gwa) # TODO: Use model sets here bgwa2msa = Mapping((0, 1, 0, 1), n_inputs=3) | \ Const1D(0) * Identity(1) & Const1D(-1) * Identity(1) & Identity(2) | \ Identity(1) & gwa2msa & Identity(2) | \ Mapping((0, 1, 0, 1, 2, 3)) | Identity(2) & msa2gwa & Identity(2) | \ Mapping((0, 1, 2, 3, 5), n_inputs=7) | Identity(2) & lgreq | mask #Mapping((0, 1, 2, 5), n_inputs=7) | Identity(2) & lgreq | mask # and modify lgreq to accept alpha_in, beta_in, alpha_out # msa to before_gwa msa2bgwa = msa2gwa & Identity(1) | Mapping( (3, 0, 1, 2)) | agreq bgwa2msa.inverse = msa2bgwa slit_models.append(bgwa2msa) msa.close() return Gwa2Slit(open_slits, slit_models)
def _make_gwcs_wcs(fits_hdr): hdr = fits.Header.fromfile(get_pkg_data_filename(fits_hdr)) fw = fitswcs.WCS(hdr) a_order = hdr['A_ORDER'] a_coeff = {} for i in range(a_order + 1): for j in range(a_order + 1 - i): key = 'A_{:d}_{:d}'.format(i, j) if key in hdr: a_coeff[key] = hdr[key] b_order = hdr['B_ORDER'] b_coeff = {} for i in range(b_order + 1): for j in range(b_order + 1 - i): key = 'B_{:d}_{:d}'.format(i, j) if key in hdr: b_coeff[key] = hdr[key] distortion = polynomial.SIP( fw.wcs.crpix, fw.sip.a_order, fw.sip.b_order, a_coeff, b_coeff ) + Identity(2) unit_conv = Scale(1.0 / 3600.0, name='arcsec_to_deg_1D') unit_conv = unit_conv & unit_conv unit_conv.name = 'arcsec_to_deg_2D' unit_conv_inv = Scale(3600.0, name='deg_to_arcsec_1D') unit_conv_inv = unit_conv_inv & unit_conv_inv unit_conv_inv.name = 'deg_to_arcsec_2D' c2s = CartesianToSpherical(name='c2s', wrap_lon_at=180) s2c = SphericalToCartesian(name='s2c', wrap_lon_at=180) c2tan = ((Mapping((0, 1, 2), name='xyz') / Mapping((0, 0, 0), n_inputs=3, name='xxx')) | Mapping((1, 2), name='xtyt')) c2tan.name = 'Cartesian 3D to TAN' tan2c = (Mapping((0, 0, 1), n_inputs=2, name='xtyt2xyz') | (Const1D(1, name='one') & Identity(2, name='I(2D)'))) tan2c.name = 'TAN to cartesian 3D' tan2c.inverse = c2tan c2tan.inverse = tan2c aff = AffineTransformation2D(matrix=fw.wcs.cd) offx = Shift(-fw.wcs.crpix[0]) offy = Shift(-fw.wcs.crpix[1]) s = 5e-6 scale = Scale(s) & Scale(s) distortion |= (offx & offy) | scale | tan2c | c2s | unit_conv_inv taninv = s2c | c2tan tan = Pix2Sky_TAN() n2c = RotateNative2Celestial(fw.wcs.crval[0], fw.wcs.crval[1], 180) wcslin = unit_conv | taninv | scale.inverse | aff | tan | n2c sky_frm = cf.CelestialFrame(reference_frame=coord.ICRS()) det_frm = cf.Frame2D(name='detector') v2v3_frm = cf.Frame2D( name="v2v3", unit=(u.arcsec, u.arcsec), axes_names=('x', 'y'), axes_order=(0, 1) ) pipeline = [(det_frm, distortion), (v2v3_frm, wcslin), (sky_frm, None)] gw = gwcs.WCS(input_frame=det_frm, output_frame=sky_frm, forward_transform=pipeline) gw.crpix = fw.wcs.crpix gw.crval = fw.wcs.crval # sanity check: for _ in range(100): x = np.random.randint(1, fw.pixel_shape[0]) y = np.random.randint(1, fw.pixel_shape[0]) assert np.allclose(gw(x, y), fw.all_pix2world(x, y, 1), rtol=0, atol=1e-11) return gw
def imaging(input_model, reference_files): """ Imaging pipeline frames : detector, gwa, msa, sky """ # Get the corrected disperser model disperser = get_disperser(input_model, reference_files['disperser']) # DMS to SCA transform dms2detector = dms_to_sca(input_model) # DETECTOR to GWA transform det2gwa = detector_to_gwa(reference_files, input_model.meta.instrument.detector, disperser) gwa_through = Const1D(-1) * Identity(1) & Const1D(-1) * Identity( 1) & Identity(1) angles = [ disperser['theta_x'], disperser['theta_y'], disperser['theta_z'], disperser['tilt_y'] ] rotation = Rotation3DToGWA(angles, axes_order="xyzy", name='rotaton').inverse dircos2unitless = DirCos2Unitless(name='directional_cosines2unitless') col = AsdfFile.open(reference_files['collimator']).tree['model'] # Get the default spectral order and wavelength range and record them in the model. sporder, wrange = get_spectral_order_wrange( input_model, reference_files['wavelengthrange']) input_model.meta.wcsinfo.waverange_start = wrange[0] input_model.meta.wcsinfo.waverange_end = wrange[1] input_model.meta.wcsinfo.spectral_order = sporder lam = wrange[0] + (wrange[1] - wrange[0]) * .5 lam_model = Mapping((0, 1, 1)) | Identity(2) & Const1D(lam) gwa2msa = gwa_through | rotation | dircos2unitless | col | lam_model gwa2msa.inverse = col.inverse | dircos2unitless.inverse | rotation.inverse | gwa_through # Create coordinate frames in the NIRSPEC WCS pipeline # "detector", "gwa", "msa", "oteip", "v2v3", "world" det, sca, gwa, msa_frame, oteip, v2v3, world = create_imaging_frames() if input_model.meta.instrument.filter != 'OPAQUE': # MSA to OTEIP transform msa2ote = msa_to_oteip(reference_files) msa2oteip = msa2ote | Mapping((0, 1), n_inputs=3) msa2oteip.inverse = Mapping((0, 1, 0, 1)) | msa2ote.inverse | Mapping( (0, 1), n_inputs=3) # OTEIP to V2,V3 transform with AsdfFile.open(reference_files['ote']) as f: oteip2v23 = f.tree['model'].copy() # V2, V3 to world (RA, DEC) transform tel2sky = pointing.v23tosky(input_model) imaging_pipeline = [(det, dms2detector), (sca, det2gwa), (gwa, gwa2msa), (msa_frame, msa2oteip), (oteip, oteip2v23), (v2v3, tel2sky), (world, None)] else: # convert to microns if the pipeline ends earlier gwa2msa = (gwa2msa | Identity(2) & Scale(10**6)).rename('gwa2msa') imaging_pipeline = [(det, dms2detector), (sca, det2gwa), (gwa, gwa2msa), (msa_frame, None)] return imaging_pipeline
def evaluate(self, x, y, x0, y0, order): """Return the valid pixel(s) and wavelengths given x0, y0, x, y, order Parameters ---------- x0: int,float,list Source object x-center y0: int,float,list Source object y-center x : int,float,list Input x location y : int,float,list Input y location order : int Spectral order to use Returns ------- x0, y0, lambda, order in the direct image for the pixel that was specified as input using the wavelength l and spectral order Notes ----- I kept the possibility of having a rotation like NIRISS, although I don't know if there is a use case for it for WFC3. The two `flatten` lines may need to be uncommented if we want to use this for array input. """ try: iorder = self._order_mapping[int(order.flatten()[0])] except AttributeError: iorder = self._order_mapping[order] except KeyError: raise ValueError("Specified order is not available") # The next two lines are to get around the fact that # modeling.standard_broadcasting=False does not work. #x00 = x0.flatten()[0] #y00 = y0.flatten()[0] t = np.linspace(0, 1, 10) #sample t xmodel = self.xmodels[iorder] ymodel = self.ymodels[iorder] lmodel = self.lmodels[iorder] dx = xmodel.evaluate(x0, y0, t) dy = ymodel.evaluate(x0, y0, t) if self.theta != 0.0: rotate = Rotation2D(self.theta) dx, dy = rotate(dx, dy) so = np.argsort(dx) tab = Tabular1D(dx[so], t[so], bounds_error=False, fill_value=None) dxr = astmath.SubtractUfunc() wavelength = dxr | tab | lmodel model = Mapping( (2, 3, 0, 2, 4)) | Const1D(x0) & Const1D(y0) & wavelength & Const1D(order) return model(x, y, x0, y0, order)
def centroid_1dg(data, error=None, mask=None): """ Calculate the centroid of a 2D array by fitting 1D Gaussians to the marginal ``x`` and ``y`` distributions of the array. Invalid values (e.g. NaNs or infs) in the ``data`` or ``error`` arrays are automatically masked. The mask for invalid values represents the combination of the invalid-value masks for the ``data`` and ``error`` arrays. Parameters ---------- data : array_like The 2D data array. error : array_like, optional The 2D array of the 1-sigma errors of the input ``data``. mask : array_like (bool), optional A boolean mask, with the same shape as ``data``, where a `True` value indicates the corresponding element of ``data`` is masked. Returns ------- centroid : `~numpy.ndarray` The ``x, y`` coordinates of the centroid. """ data = np.ma.asanyarray(data) if mask is not None and mask is not np.ma.nomask: mask = np.asanyarray(mask) if data.shape != mask.shape: raise ValueError('data and mask must have the same shape.') data.mask |= mask if np.any(~np.isfinite(data)): data = np.ma.masked_invalid(data) warnings.warn( 'Input data contains input values (e.g. NaNs or infs), ' 'which were automatically masked.', AstropyUserWarning) if error is not None: error = np.ma.masked_invalid(error) if data.shape != error.shape: raise ValueError('data and error must have the same shape.') data.mask |= error.mask error.mask = data.mask xy_error = [np.sqrt(np.ma.sum(error**2, axis=i)) for i in [0, 1]] xy_weights = [(1.0 / xy_error[i].clip(min=1.e-30)) for i in [0, 1]] else: xy_weights = [np.ones(data.shape[i]) for i in [1, 0]] # assign zero weight where an entire row or column is masked if np.any(data.mask): bad_idx = [np.all(data.mask, axis=i) for i in [0, 1]] for i in [0, 1]: xy_weights[i][bad_idx[i]] = 0. xy_data = [np.ma.sum(data, axis=i).data for i in [0, 1]] constant_init = np.ma.min(data) centroid = [] for (data_i, weights_i) in zip(xy_data, xy_weights): params_init = gaussian1d_moments(data_i) g_init = Const1D(constant_init) + Gaussian1D(*params_init) fitter = LevMarLSQFitter() x = np.arange(data_i.size) g_fit = fitter(g_init, x, data_i, weights=weights_i) centroid.append(g_fit.mean_1.value) return np.array(centroid)
def build_nirspec_output_wcs(self, refmodel=None): """ Create a spatial/spectral WCS covering footprint of the input """ if not refmodel: refmodel = self.input_models[0] refwcs = refmodel.meta.wcs bb = refwcs.bounding_box ref_det2slit = refwcs.get_transform('detector', 'slit_frame') ref_slit2world = refwcs.get_transform('slit_frame', 'world') grid = x, y = wcstools.grid_from_bounding_box(bb, step=(1, 1)) Grid = namedtuple('Grid', refwcs.slit_frame.axes_names) grid_slit = Grid(*ref_det2slit(*grid)) # Compute spatial transform from detector to slit ref_wavelength = np.nanmean(grid_slit.wavelength) # find the number of pixels sampled by a single shutter fid = np.array([[0., 0.], [-.5, .5], np.repeat(ref_wavelength, 2)]) slit_extents = np.array(ref_det2slit.inverse(*fid)).T pix_per_shutter = np.linalg.norm(slit_extents[0] - slit_extents[1]) # Get min and max of slit in pixel units ymin = np.nanmin(grid_slit.y_slit) * pix_per_shutter ymax = np.nanmax(grid_slit.y_slit) * pix_per_shutter slit_height_pix = int(abs(ymax - ymin + 0.5)) # Compute grid of wavelengths and make tabular model w/inverse lookup_table = np.nanmean(grid_slit.wavelength, axis=0) wavelength_transform = Tabular1D(lookup_table=lookup_table, bounds_error=False, fill_value=np.nan) wavelength_transform.inverse = Tabular1D( points=lookup_table, lookup_table=np.arange(grid_slit.wavelength.shape[1]), bounds_error=False, fill_value=np.nan) # Define detector to slit transforms yslit_transform = Scale(-1 / pix_per_shutter) | Shift( ymax / pix_per_shutter) xslit_transform = Const1D(-0.) xslit_transform.inverse = Const1D(0.) # Construct the final transform coord_mapping = Mapping((0, 1, 0)) coord_mapping.inverse = Mapping((2, 1)) the_transform = xslit_transform & yslit_transform & wavelength_transform out_det2slit = coord_mapping | the_transform # Create coordinate frames det = cf.Frame2D(name='detector', axes_order=(0, 1)) slit_spatial = cf.Frame2D(name='slit_spatial', axes_order=(0, 1), unit=("", ""), axes_names=('x_slit', 'y_slit')) spec = cf.SpectralFrame(name='spectral', axes_order=(2, ), unit=(u.micron, ), axes_names=('wavelength', )) slit_frame = cf.CompositeFrame([slit_spatial, spec], name='slit_frame') sky = cf.CelestialFrame(name='sky', axes_order=(0, 1), reference_frame=coord.ICRS()) world = cf.CompositeFrame([sky, spec], name='world') pipeline = [(det, out_det2slit), (slit_frame, ref_slit2world), (world, None)] output_wcs = WCS(pipeline) self.data_size = (slit_height_pix, len(lookup_table)) bounding_box = resample_utils.bounding_box_from_shape(self.data_size) output_wcs.bounding_box = bounding_box return output_wcs
def tsgrism(input_model, reference_files): """Create WCS pipeline for a NIRCAM Time Series Grism observation. Parameters ---------- input_model : `~jwst.datamodels.ImagingModel` The input datamodel, derived from datamodels reference_files : dict Dictionary of reference file names {reftype: reference file name}. Returns ------- pipeline : list The pipeline list that is returned is suitable for input into gwcs.wcs.WCS to create a GWCS object. Notes ----- The TSGRISM mode should function effectively like the grism mode except that subarrays will be allowed. Since the transform models depend on the original full frame coordinates of the observation, the regular grism transforms will need to be shifted to the full frame coordinates around the trace transform. TSGRISM is only slated to work with GRISMR and Mod A """ # The input is the grism image if not isinstance(input_model, CubeModel): raise TypeError('The input data model must be a CubeModel.') # make sure this is a grism image if "NRC_TSGRISM" != input_model.meta.exposure.type: raise ValueError( 'The input exposure is not a NIRCAM time series grism') if input_model.meta.instrument.module != "A": raise ValueError('NRC_TSGRISM mode only supports module A') if input_model.meta.instrument.pupil != "GRISMR": raise ValueError('NRC_TSGRIM mode only supports GRISMR') gdetector = cf.Frame2D(name='grism_detector', axes_order=(0, 1), unit=(u.pix, u.pix)) detector = cf.Frame2D(name='full_detector', axes_order=(0, 1), unit=(u.pix, u.pix)) v2v3 = cf.Frame2D(name='v2v3', axes_order=(0, 1), unit=(u.deg, u.deg)) world = cf.CelestialFrame(reference_frame=coord.ICRS(), name='world') # translate the x,y detector-in to x,y detector out coordinates # Get the disperser parameters which are defined as a model for each # spectral order with NIRCAMGrismModel(reference_files['specwcs']) as f: displ = f.displ dispx = f.dispx dispy = f.dispy invdispx = f.invdispx invdispl = f.invdispl orders = f.orders # now create the appropriate model for the grismr det2det = NIRCAMForwardRowGrismDispersion(orders, lmodels=displ, xmodels=invdispx, ymodels=dispy) det2det.inverse = NIRCAMBackwardGrismDispersion(orders, lmodels=invdispl, xmodels=dispx, ymodels=dispy) # Add in the wavelength shift from the velocity dispersion try: velosys = input_model.meta.wcsinfo.velosys except AttributeError: pass if velosys is not None: velocity_corr = velocity_correction(input_model.meta.wcsinfo.velosys) log.info("Added Barycentric velocity correction: {}".format( velocity_corr[1].amplitude.value)) det2det = det2det | Mapping( (0, 1, 2, 3)) | Identity(2) & velocity_corr & Identity(1) # input into the forward transform is x,y,x0,y0,order # where x,y is the pixel location in the grism image # and x0,y0 is the source location in the "direct" image # For this mode, the source is always at crpix1,crpis2 # discussion with nadia that wcsinfo might not be available # here but crpix info could be in wcs.source_location or similar # TSGRISM mode places the sources at crpix, and all subarrays # begin at 0,0, so no need to translate the crpix to full frame # because they already are in full frame coordinates. xc, yc = (input_model.meta.wcsinfo.crpix1, input_model.meta.wcsinfo.crpix2) xcenter = Const1D(xc) xcenter.inverse = Const1D(xc) ycenter = Const1D(yc) ycenter.inverse = Const1D(yc) setra = Const1D(input_model.meta.wcsinfo.crval1) setra.inverse = Const1D(input_model.meta.wcsinfo.crval1) setdec = Const1D(input_model.meta.wcsinfo.crval2) setdec.inverse = Const1D(input_model.meta.wcsinfo.crval2) # x, y, order in goes to transform to full array location and order # get the shift to full frame coordinates sub_trans = subarray_transform(input_model) if sub_trans is not None: sub2direct = (sub_trans & Identity(1) | Mapping( (0, 1, 0, 1, 2)) | (Identity(2) & xcenter & ycenter & Identity(1)) | det2det) else: sub2direct = (Mapping( (0, 1, 0, 1, 2)) | (Identity(2) & xcenter & ycenter & Identity(1)) | det2det) # take us from full frame detector to v2v3 distortion = imaging_distortion(input_model, reference_files) & Identity(2) # v2v3 to the sky # remap the tel2sky inverse as well since we can feed it the values of # crval1, crval2 which correspond to crpix1, crpix2. This leaves # us with a calling structure: # (x, y, order) <-> (wavelength, order) tel2sky = pointing.v23tosky(input_model) & Identity(2) t2skyinverse = tel2sky.inverse newinverse = Mapping( (0, 1, 0, 1)) | setra & setdec & Identity(2) | t2skyinverse tel2sky.inverse = newinverse pipeline = [(gdetector, sub2direct), (detector, distortion), (v2v3, tel2sky), (world, None)] return pipeline
def niriss_soss(input_model, reference_files): """ The NIRISS SOSS WCS pipeline. Parameters ---------- input_model : `~jwst.datamodel.DataModel` Input datamodel for processing reference_files : dict The dictionary of reference file names and their associated files {reftype: reference file name}. Returns ------- pipeline : list The pipeline list that is returned is suitable for input into gwcs.wcs.WCS to create a GWCS object. Notes ----- It includes tWO coordinate frames - "detector" and "world". It uses the "specwcs" reference file. """ # Get the target RA and DEC, they will be used for setting the WCS RA # and DEC based on a conversation with Kevin Volk. try: target_ra = float(input_model.meta.target.ra) target_dec = float(input_model.meta.target.dec) except TypeError: # There was an error getting the target RA and DEC, so we are not going to continue. raise ValueError('Problem getting the TARG_RA or TARG_DEC from input model {}'.format(input_model)) # Define the frames detector = cf.Frame2D(name='detector', axes_order=(0, 1), unit=(u.pix, u.pix)) spec = cf.SpectralFrame(name='spectral', axes_order=(2,), unit=(u.micron,), axes_names=('wavelength',)) sky = cf.CelestialFrame(reference_frame=coord.ICRS(), axes_names=('ra', 'dec'), axes_order=(0, 1), unit=(u.deg, u.deg), name='sky') world = cf.CompositeFrame([sky, spec], name='world') try: # We'd like to open this file as a DataModel, so we can consolidate # the S3 URI handling to one place. The S3-related code here can # be removed once that has been changed. if s3_utils.is_s3_uri(reference_files['specwcs']): wl = asdf.open(s3_utils.get_object(reference_files['specwcs'])) else: wl = asdf.open(reference_files['specwcs']) with wl: wl1 = wl.tree[1].copy() wl2 = wl.tree[2].copy() wl3 = wl.tree[3].copy() except Exception: raise IOError('Error reading wavelength correction from {}'.format(reference_files['specwcs'])) try: velosys = input_model.meta.wcsinfo.velosys except AttributeError: pass else: if velosys is not None: velocity_corr = velocity_correction(input_model.meta.wcsinfo.velosys) wl1 = wl1 | velocity_corr wl2 = wl2 | velocity_corr wl2 = wl3 | velocity_corr log.info("Applied Barycentric velocity correction: {}".format(velocity_corr[1].amplitude.value)) # Reverse the order of inputs passed to Tabular because it's in python order in modeling. # Consider changing it in modelng ? cm_order1 = (Mapping((0, 1, 1, 0)) | (Const1D(target_ra) & Const1D(target_dec) & wl1) ).rename('Order1') cm_order2 = (Mapping((0, 1, 1, 0)) | (Const1D(target_ra) & Const1D(target_dec) & wl2) ).rename('Order2') cm_order3 = (Mapping((0, 1, 1, 0)) | (Const1D(target_ra) & Const1D(target_dec) & wl3) ).rename('Order3') subarray2full = subarray_transform(input_model) if subarray2full is not None: cm_order1 = subarray2full | cm_order1 cm_order2 = subarray2full | cm_order2 cm_order3 = subarray2full | cm_order3 bbox = ((-0.5, input_model.meta.subarray.ysize - 0.5), (-0.5, input_model.meta.subarray.xsize - 0.5)) cm_order1.bounding_box = bbox cm_order2.bounding_box = bbox cm_order3.bounding_box = bbox # Define the transforms, they should accept (x,y) and return (ra, dec, lambda) soss_model = NirissSOSSModel([1, 2, 3], [cm_order1, cm_order2, cm_order3] ).rename('3-order SOSS Model') # Define the pipeline based on the frames and models above. pipeline = [(detector, soss_model), (world, None) ] return pipeline
def extract_tso_object(input_model, reference_files=None, extract_height=None, extract_orders=None): """ Extract the spectrum for a NIRCAM TSO observation. Parameters ---------- input_model : `~jwst.datamodels.CubeModel` The input TSO image as an instance of a CubeModel reference_files : dict Needs to include the name of the wavelengthrange reference file extract_height : int The extraction height, in total, for the spectra in the cross-dispersion direction. If this is other than None, it will override the team default of 64 pixels. The team wants the source centered near row 34, so the extraction height is not the same on either size of the central pixel. extract_orders : list[ints] This is an optional parameter that will override the orders specified for extraction in the wavelengthrange reference file. Returns ------- output_model : `~jwst.datamodels.SlitModel` Notes ----- This method supports NRC_TSGRISM only, where only one bright object is considered in the field, so there's no catalog of sources and the object is assumed to have been observed at the aperture location crpix1/2. CRPIX1/2 are read from the SIAF and saved as "meta.wcsinfo.siaf_xref_sci" and "meta.wcsinfo.siaf_yref_sci". GrismObject is a named tuple which contains distilled information about each catalog object and the bounding boxes that will be used to define the 2d extraction area. Since this mode has a single known source location the utilities used in the WFSS modes are overkill, instead, similar structures are created during the extrac 2d process and then directly used. https://jwst-docs.stsci.edu/display/JTI/NIRCam+Grism+Time+Series """ if not isinstance(reference_files, dict): raise TypeError("Expected a dictionary for reference_files") if 'wavelengthrange' not in reference_files.keys(): raise KeyError("No wavelengthrange reference file specified") if not isinstance(input_model, CubeModel): raise TypeError('The input data model is not a CubeModel.') if extract_height is None: extract_height = input_model.meta.subarray.ysize log.info("Setting extraction height to {}".format(extract_height)) # Get the disperser parameters which have the wave limits with WavelengthrangeModel(reference_files['wavelengthrange']) as f: if (f.meta.instrument.name != 'NIRCAM'): raise ValueError("Wavelengthrange reference file not for NIRCAM!") if (f.meta.exposure.type != 'NRC_TSGRISM'): raise ValueError("Wavelengthrange reference file not for TSGRISM") wavelengthrange = f.wavelengthrange ref_extract_orders = f.extract_orders # this supports the user override and overriding the WFSS order extraction # when called from the pipeline only the first order is extracted if extract_orders is None: log.info("Using default order extraction from reference file") extract_orders = ref_extract_orders available_orders = [x[1] for x in extract_orders if x[0] == input_model.meta.instrument.filter].pop() else: if isinstance(extract_orders, list): if not all(isinstance(item, int) for item in extract_orders): raise TypeError("Expected extract_orders to be a list of ints") else: raise TypeError("Expected extract_orders to be a list of ints") available_orders = extract_orders if len(available_orders) > 1: raise NotImplementedError("Multiple order extraction for TSO not currently implemented") log.info("Extracting order: {}".format(available_orders)) output_model = datamodels.SlitModel() output_model.update(input_model) subwcs = copy.deepcopy(input_model.meta.wcs) for order in available_orders: range_select = [(x[2], x[3]) for x in wavelengthrange if (x[0] == order and x[1] == input_model.meta.instrument.filter)] # All objects in the catalog will use the same filter for translation # that filter is the one that was used in front of the grism lmin, lmax = range_select.pop() # create the order bounding box source_xpos = input_model.meta.wcsinfo.siaf_xref_sci - 1 # remove fits source_ypos = input_model.meta.wcsinfo.siaf_yref_sci - 1 # remove fits transform = input_model.meta.wcs.get_transform('full_detector', 'grism_detector') xmin, ymin, _ = transform(source_xpos, source_ypos, lmin, order) xmax, ymax, _ = transform(source_xpos, source_ypos, lmax, order) # Add the shift to the lower corner to each subarray WCS object # The shift should just be the lower bounding box corner # also replace the object center location inputs to the GrismDispersion # model with the known object center and order information (in pixels of direct image) # This is changes the user input to the model from (x,y,x0,y0,order) -> (x,y) # # The bounding boxes here are also limited to the size of the detector in the # dispersion direction and 64 pixels in the cross-dispersion # The check for boxes entirely off the detector is done in create_grism_bbox right now # The team wants the object to fall near row 34 for all cutouts, # but the default cutout height is 64pixel (32 on either side) # so use crpix2 when it equals 34, but bump the ycenter by 2 pixel # in the case that it's 32 so that the height is 30 above and 34 below (in full frame) bump = source_ypos - 34 extract_y_center = source_ypos - bump splitheight = int(extract_height / 2) below = extract_y_center - splitheight if below == 34: extract_y_min = 0 extract_y_max = extract_y_center + splitheight elif below < 0: extract_y_min = 0 extract_y_max = extract_height - 1 else: extract_y_min = extract_y_center - 34 # always return source at pixel 34 in cutout. extract_y_max = extract_y_center + extract_height - 34 - 1 if (extract_y_min > extract_y_max): raise ValueError("Something bad happened calculating extraction y-size") # limit the bounding box to the detector ymin, ymax = (max(extract_y_min, 0), min(extract_y_max, input_model.meta.subarray.ysize)) xmin, xmax = (max(xmin, 0), min(xmax, input_model.meta.subarray.xsize)) log.info("xmin, xmax: {} {} ymin, ymax: {} {}".format(xmin, xmax, ymin, ymax)) # the order and source position are put directly into # the new wcs for the subarray for the forward transform order_model = Const1D(order) order_model.inverse = Const1D(order) tr = input_model.meta.wcs.get_transform('grism_detector', 'full_detector') tr = Mapping((0, 1, 0)) | Shift(xmin) & Shift(ymin) & order_model | tr subwcs.set_transform('grism_detector', 'full_detector', tr) xmin = int(xmin) xmax = int(xmax) ymin = int(ymin) ymax = int(ymax) # cut it out, keep the entire row though ext_data = input_model.data[:, ymin: ymax + 1, :].copy() ext_err = input_model.err[:, ymin: ymax + 1, :].copy() ext_dq = input_model.dq[:, ymin: ymax + 1, :].copy() log.info("WCS made explicit for order: {}".format(order)) log.info("Trace extents are: (xmin:{}, ymin:{}), (xmax:{}, ymax:{})".format(xmin, ymin, xmax, ymax)) if output_model.meta.model_type == "SlitModel": output_model.data = ext_data output_model.err = ext_err output_model.dq = ext_dq output_model.meta.wcs = subwcs output_model.meta.wcs.bounding_box = util.wcs_bbox_from_shape(ext_data.shape) output_model.meta.wcsinfo.siaf_yref_sci = 34 # update for the move, vals are the same output_model.meta.wcsinfo.siaf_xref_sci = input_model.meta.wcsinfo.siaf_xref_sci output_model.meta.wcsinfo.spectral_order = order output_model.name = str('TSO object') output_model.xstart = 1 # fits pixels output_model.xsize = ext_data.shape[2] output_model.ystart = 1 # fits pixels output_model.ysize = ext_data.shape[1] output_model.source_xpos = source_xpos output_model.source_ypos = 34 output_model.source_id = 1 output_model.bunit_data = input_model.meta.bunit_data output_model.bunit_err = input_model.meta.bunit_err del subwcs log.info("Finished extractions") return output_model
def niriss_soss(input_model, reference_files): """ The NIRISS SOSS pipeline includes 3 coordinate frames - detector, focal plane and sky reference_files={'specwcs': 'soss_wavelengths_configuration.asdf'} """ # Get the target RA and DEC, they will be used for setting the WCS RA and DEC based on a conversation # with Kevin Volk. try: target_ra = float(input_model['meta.target.ra']) target_dec = float(input_model['meta.target.dec']) except: # There was an error getting the target RA and DEC, so we are not going to continue. raise ValueError( 'Problem getting the TARG_RA or TARG_DEC from input model {}'. format(input_model)) # Define the frames detector = cf.Frame2D(name='detector', axes_order=(0, 1), unit=(u.pix, u.pix)) spec = cf.SpectralFrame(name='spectral', axes_order=(2, ), unit=(u.micron, ), axes_names=('wavelength', )) sky = cf.CelestialFrame(reference_frame=coord.ICRS(), axes_names=('ra', 'dec'), axes_order=(0, 1), unit=(u.deg, u.deg), name='sky') world = cf.CompositeFrame([sky, spec], name='world') try: with AsdfFile.open(reference_files['specwcs']) as wl: wl1 = wl.tree[1].copy() wl2 = wl.tree[2].copy() wl3 = wl.tree[3].copy() except Exception as e: raise IOError('Error reading wavelength correction from {}'.format( reference_files['specwcs'])) subarray2full = subarray_transform(input_model) # Reverse the order of inputs passed to Tabular because it's in python order in modeling. # Consider changing it in modelng ? cm_order1 = subarray2full | (Mapping((0, 1, 1, 0)) | \ (Const1D(target_ra) & Const1D(target_dec) & wl1) ).rename('Order1') cm_order2 = subarray2full | (Mapping((0, 1, 1, 0)) | \ (Const1D(target_ra) & Const1D(target_dec) & wl2) ).rename('Order2') cm_order3 = subarray2full | (Mapping((0, 1, 1, 0)) | \ (Const1D(target_ra) & Const1D(target_dec) & wl3) ).rename('Order3') # Define the transforms, they should accept (x,y) and return (ra, dec, lambda) soss_model = NirissSOSSModel( [1, 2, 3], [cm_order1, cm_order2, cm_order3]).rename('3-order SOSS Model') # Define the pipeline based on the frames and models above. pipeline = [(detector, soss_model), (world, None)] return pipeline