def adc_shift(cmds): """Generates a list of x and y shifts from a tests_commands object""" para_angle = cmds["OBS_PARALLACTIC_ANGLE"] effectiveness = cmds["INST_ADC_PERFORMANCE"] / 100. ## get the angle shift for each slice zenith_distance = utils.airmass2zendist(cmds["ATMO_AIRMASS"]) angle_shift = [ scopesim.effects.shifts.atmospheric_refraction( lam, zenith_distance, cmds["ATMO_TEMPERATURE"], cmds["ATMO_REL_HUMIDITY"], cmds["ATMO_PRESSURE"], cmds["SCOPE_LATITUDE"], cmds["SCOPE_ALTITUDE"]) for lam in cmds.lam_bin_centers ] ## convert angle shift into number of pixels ## pixel shifts are defined with respect to last slice rel_shift = (angle_shift - angle_shift[-1]) if np.max(np.abs(rel_shift)) > 1000: raise ValueError("Pixel shifts too great (>1000), check units") ## Rotate by the paralytic angle x = -rel_shift * np.sin(np.deg2rad(para_angle)) * (1. - effectiveness) y = -rel_shift * np.cos(np.deg2rad(para_angle)) * (1. - effectiveness) ## return values are in [arcsec] return x, y
def test_airmass2zendist_undoes_exactly_what_zendist2airmass_does(self): zendist = 12.31334 assert np.allclose(airmass2zendist(zendist2airmass(zendist)), zendist)
def test_zendist2airmass_undoes_exactly_what_airmass2zendist_does(self): airmass = 1.78974234 assert np.allclose(zendist2airmass(airmass2zendist(airmass)), airmass)
def test_pass_for_known_quanities_AM_2_equals_ZD_sqrt2(self): assert np.allclose(airmass2zendist(np.sqrt(2)), 45)
def test_airmass2zendist_pass_for_known_quanities_AM_1_equals_ZD_0(self): assert np.allclose(airmass2zendist(1.0), 0)
def _get_lam_bin_edges(self, lam_min, lam_max): """ Generates an array with the bin edges of the layers in spectral space Parameters ---------- lam_min, lam_max : float [um] the minimum and maximum wavelengths of the filter range Notes ------- Atmospheric diffraction causes blurring in an image. To model this effect the spectra from a ``Source`` object are cut into bins based on how far the photons at different wavelength are diffracted from the image center. The threshold for defining a new layer based on the how far a certain bin will move is given by ``SIM_ADC_SHIFT_THRESHOLD``. The default value is 1 pixel. The PSF also causes blurring as it spreads out over a bandpass. This also needed to be taken into account """ if self.cmds["SIM_VERBOSE"] == "yes": print("Determining lam_bin_edges") effectiveness = self.cmds["INST_ADC_PERFORMANCE"] / 100. # This is redundant because also need to look at the PSF width # if effectiveness == 1.: # lam_bin_edges = np.array([lam_min, lam_max]) # return lam_bin_edges shift_threshold = self.cmds["SIM_ADC_SHIFT_THRESHOLD"] # get the angle shift for each slice lam = np.arange(lam_min, lam_max + 1E-7, 0.001) zenith_distance = airmass2zendist(self.cmds["ATMO_AIRMASS"]) angle_shift = atmospheric_refraction(lam, zenith_distance, self.cmds["ATMO_TEMPERATURE"], self.cmds["ATMO_REL_HUMIDITY"], self.cmds["ATMO_PRESSURE"], self.cmds["SCOPE_LATITUDE"], self.cmds["SCOPE_ALTITUDE"]) # convert angle shift into number of pixels # pixel shifts are defined with respect to last slice rel_shift = (angle_shift - angle_shift[-1]) / self.pix_res rel_shift *= (1. - effectiveness) if np.max(np.abs(rel_shift)) > 1000: raise ValueError("Pixel shifts too great (>1000), check units") # Rotate by the paralytic angle int_shift = np.array(rel_shift / shift_threshold, dtype=np.int) idx = [ np.where(int_shift == i)[0][0] for i in np.unique(int_shift)[::-1] ] lam_bin_edges_adc = np.array(lam[idx + [len(lam) - 1]]) # Now check to see if the PSF blurring is the controlling factor. If so, # take the lam_bin_edges for the PSF blurring diam = self.diameter d_ang = self.pix_res * shift_threshold # .. todo:: get rid of hard coded diameter of MICADO FOV diam_arcsec = 1.22 * 53 * 3600 lam_bin_edges_psf = [lam_min] ang0 = (lam_min * 1E-6) / diam * diam_arcsec i = 1 while lam_bin_edges_psf[-1] < lam_max: lam_bin_edges_psf += [(ang0 + d_ang * i) * diam / diam_arcsec * 1E6 ] i += 1 if i > 1000: raise ValueError("lam_bin_edges needs >1000 values") lam_bin_edges_psf[-1] = lam_max lam_bin_edges = np.unique( np.concatenate((np.round(lam_bin_edges_psf, 3), np.round(lam_bin_edges_adc, 3)))) if self.cmds["SIM_VERBOSE"] == "yes": print("PSF edges were", np.round(lam_bin_edges_psf, 3)) print("ADC edges were", np.round(lam_bin_edges_adc, 3)) print("All edges were", np.round(lam_bin_edges, 3)) return lam_bin_edges
def read_out(self, filename=None, to_disk=False, chips=None, read_out_type="superfast", **kwargs): """ Simulate the read-out process of the detector array Based on the parameters set in the ``UserCommands`` object, the detector will read out the images stored on the ``Chips`` according to the specified read-out scheme, i.e. Fowler, up-the-ramp, single read, etc. Parameters ---------- filename : str where the file is to be saved. If ``None`` and ``to_disk`` is true, the output file is called "output.fits". Default is ``None`` to_disk : bool a flag for where the output should go. If ``filename`` is given or if ``to_disk=True``, the ``Chip`` images will be written to a `.fits`` file on disk. If no `filename`` is specified, the output will be called "output.fits". chips : int, array-like, optional The chip or chips to be read out, based on the detector_layout.dat file. Default is the first ``Chip`` specified in the list, i.e. [0]. read_out_type : str, optional The name of the algorithm used to read out the chips: - "superfast" - "non_destructive" - "up_the_ramp" Returns ------- astropy.io.fits.HDUList Keyword Arguments (**kwargs) ---------------------------- **kwargs are used to update the ``UserCommands`` object that controls the ``Detector``. Therefore any dictionary keywords can be passed in the form of a dictionary, i.e. {"OBS_EXPTIME" : 60, "OBS_OUTPUT_DIR" : "./"} """ #removed kwargs self.cmds.update(kwargs) if filename is not None: to_disk = True if filename is None and to_disk is True: if self.cmds["OBS_OUTPUT_DIR"] is None: self.cmds["OBS_OUTPUT_DIR"] = "./output.fits" filename = self.cmds["OBS_OUTPUT_DIR"] if chips is not None: if np.isscalar(chips): ro_chips = [chips] else: ro_chips = chips elif chips is None: ro_chips = np.arange(len(self.chips)) else: raise ValueError("Something wrong with ``chips``") # Time stamp for FITS header #creation_date = datetime.now().isoformat(timespec='seconds') # timespec="seconds" throws an error on some python versions creation_date = datetime.now().strftime("%Y-%m-%dT%H-%M-%S") hdulist = fits.HDUList() # Create primary header unit for multi-extension files if len(ro_chips) > 1: primary_hdu = fits.PrimaryHDU() primary_hdu.header['DATE'] = creation_date for key in self.cmds.cmds: val = self.cmds.cmds[key] if isinstance(val, (sc.TransmissionCurve, sc.EmissionCurve, sc.UnityCurve, sc.BlackbodyCurve)): val = val.params["filename"] if isinstance(val, str) and len(val) > 35: val = "... " + val[-35:] try: primary_hdu.header["HIERARCH "+key] = val except NameError: # any other exceptions possible? pass hdulist.append(primary_hdu) # Save the detector image(s) for i in ro_chips: ###### # Put in a catch here so that only the chips specified in "chips" # are read out ###### print("Reading out chip", self.chips[i].id, "using", read_out_type) array = self.chips[i].read_out(self.cmds, read_out_type=read_out_type) ## TODO: transpose is just a hack - need to make sure ## x and y are handled correctly throughout ScopeSim thishdu = fits.ImageHDU(array.T) thishdu.header["EXTNAME"] = ("CHIP_{:02d}".format(self.chips[i].id), "Chip ID") thishdu.header["CHIP_ID"] = (self.chips[i].id, "Chip ID") thishdu.header['DATE'] = creation_date # Primary WCS for sky coordinates thishdu.header.extend(self.chips[i].wcs.to_header()) # Secondary WCS for focal plane coordinates try: thishdu.header.extend(self.chips[i].wcs_fp.to_header(key='A')) except AttributeError: print("No WCS_FP!") pass thishdu.header["BUNIT"] = ("ADU", "") thishdu.header["EXPTIME"] = (self.exptime, "[s] Exposure time") thishdu.header["NDIT"] = (self.ndit, "Number of exposures") #thishdu.header["TRO"] = (self.tro, # "[s] Time between non-destructive readouts") thishdu.header["GAIN"] = (self.chips[i].gain, "[e-/ADU]") thishdu.header["AIRMASS"] = (self.cmds["ATMO_AIRMASS"], "") thishdu.header["ZD"] = \ (airmass2zendist(self.cmds["ATMO_AIRMASS"]), "[deg]") for key in self.cmds.cmds: val = self.cmds.cmds[key] if isinstance(val, str): if len(val) > 35: val = "... " + val[-35:] try: thishdu.header["HIERARCH "+key] = val except NameError: # any other exceptions possible? pass except ValueError: logging.warning("ValueError - Couldn't add keyword: "+key) hdulist.append(thishdu) if to_disk: hdulist.writeto(filename, clobber=True, checksum=True) return hdulist