Пример #1
0
def misc():
    # get_vega() downloads this one
    synphot.specio.read_remote_spec(
        'http://ssb.stsci.edu/cdbs/calspec/alpha_lyr_stis_008.fits')

    # G5V of UVKLIB subset of Pickles library
    # see http://www.stsci.edu/hst/instrumentation/reference-data-for-calibration-and-tools/astronomical-catalogs/pickles-atlas.html
    synphot.specio.read_remote_spec(
        'http://ssb.stsci.edu/cdbs/grid/pickles/dat_uvk/pickles_uk_27.fits')

    # read the sourcespectrum
    spG5V = SourceSpectrum.from_file(
        'http://ssb.stsci.edu/cdbs/grid/pickles/dat_uvk/pickles_uk_27.fits')
    spG2V = SourceSpectrum.from_file(
        'http://ssb.stsci.edu/cdbs/grid/pickles/dat_uvk/pickles_uk_26.fits')

    filtR = SpectralElement.from_filter('johnson_r')
    spG2V_19r = spG2V.normalize(19 * synphot.units.VEGAMAG,
                                filtR,
                                vegaspec=SourceSpectrum.from_vega())

    bp = SpectralElement(Box1D, x_0=700 * u.nm, width=600 * u.nm)
    obs = Observation(spG2V_19r, bp)
    obs.countrate(area=50 * u.m**2)

    bp220 = SpectralElement(Box1D, x_0=800 * u.nm, width=400 * u.nm)
    bpCRed = SpectralElement(Box1D, x_0=1650 * u.nm, width=300 * u.nm) * 0.8

    Observation(spG2V_19r, bp220).countrate(area=50 * u.m**2)
    Observation(spG2V_19r, bpCRed).countrate(area=50 * u.m**2)

    spM0V_8R = get_normalized_star_spectrum('M0V', 8.0, 'johnson_r')

    uPhotonSecM2Micron = u.photon / (u.s * u.m**2 * u.micron)

    spG2V_8R = get_normalized_star_spectrum("G2V", 8.0, 'johnson_r')
    plt.plot(spG2V_8R.waveset,
             spG2V_8R(spG2V_8R.waveset).to(uPhotonSecM2Micron))

    # compare with Armando's
    spG2V_19R = get_normalized_star_spectrum("G2V", 19, 'johnson_r')
    bp = SpectralElement(Box1D, x_0=700 * u.nm, width=600 * u.nm)
    obs = Observation(spG2V_19R, bp)
    obs.countrate(area=50 * u.m**2)

    # zeropoint in filtro r in erg/s/cm2/A
    Observation(get_normalized_star_spectrum('A0V', 0, 'johnson_r'),
                SpectralElement.from_filter('johnson_r')).effstim('flam')
    # zeropoint in ph/s/m2
    Observation(get_normalized_star_spectrum('A0V', 0, 'johnson_r'),
                SpectralElement.from_filter('johnson_r')).countrate(area=1 *
                                                                    u.m**2)
Пример #2
0
    def sso_to_source_spec(self, tax_type):
        """Returns a SourceSpectrum from the passed <tax_type> if it is found
        in the Bus-DeMeo taxonomy, otherwise None is returned.
        The spectrum is produced by multiplying the reflectance spectra by
        a Kurucz model for the Sun so will need to be normalized"""

        source_spec = None

        if tax_type.lower().startswith('sso::') is False:
            tax_type = 'sso::' + tax_type.strip()
        config_item = conf.source_mapping.get(tax_type, None)
        if config_item is not None:
            filename = config_item()
            file_path = os.path.expandvars(filename)
            if not os.path.exists(file_path):
                file_path = str(
                    pkg_resources.files('etc.data').joinpath(filename))
            # Flux unit for LSST throughputs (*almost* FLAM but nm not Angstroms)
            lsst_funit = u.erg / u.cm**2 / u.s / u.nm
            source_spec = SourceSpectrum.from_file(file_path,
                                                   wave_unit=u.nm,
                                                   flux_unit=lsst_funit,
                                                   header_start=1)
            source_spec.meta['header']['source'] = config_item.description
            source_spec.meta['header']['filename'] = filename

        return source_spec
Пример #3
0
def calcMagFromSpec(bp, specPath, redden=None):
    sp = SourceSpectrum.from_file(specPath)
    if redden:
        sp = sp * redden
    obs = Observation(sp, bp)
    counts = obs.countrate(1.0)
    return -2.5 * np.log10(counts.value)
Пример #4
0
def get_normalized_star_spectrum(spectral_type, magnitude, filter_name):
    """
    spec_data = get_normalized_star_spectrum(spectral_type, magnitude, filter_name)

    Returns a structure containing the synthetic spectrum of the star having the spectral type and magnitude 
    in the specified input filter. Magnitude is in VEGAMAG-F(lambda) system.
    Spectra are from PICKLES, PASP, 110, 863 (1998)
    Absolute flux spectra, no effect of atmospheric and instrument transmission


    Parameters
    ----------
        r0AtZenith: float
                    overall r0 at zenith [m]

        spectral_type:  string.
                        spectral type and luminosity class (e.g. G2V or M4III) or 'vega'
        magnitude:  float.
                    magnitude in the filter_name filter
        filter_name: string.
                     Name of the filter. See Filters.get() for the list of available filters

    Returns
    -------
        spectrum: synphot.SourceSpectrum object defining the spectrum

    Examples
    --------
    Plot the spectrum of a vega, A0V, G2V stars of mag=8 defined on JohnsonR filter

    >>> sp= get_normalized_star_spectrum('vega', 8, Filters.JOHNSON_R)
    >>> spA0V= get_normalized_star_spectrum('A0V', 8, Filters.JOHNSON_R)
    >>> spG2V= get_normalized_star_spectrum('G2V', 8, Filters.JOHNSON_R)
    >>> plt.plot(sp.waveset, sp(sp.waveset), label='Vega')
    >>> plt.plot(spA0V.waveset, spA0V(spA0V.waveset), label='A0V')
    >>> plt.plot(spG2V.waveset, spG2V(spG2V.waveset), label='G2V')
    >>> plt.grid(True)
    >>> plt.xlabel('nm')
    >>> plt.ylabel('FLAM')
    >>> plt.xlim(0, 10000)
    >>> plt.legend()
"""
    # read the sourcespectrum
    if spectral_type == 'vega':
        spectrum = SourceSpectrum.from_vega()
    else:
        spectrum = SourceSpectrum.from_file(
            PickelsLibrary.filename(spectral_type))

    bandpass = Filters.get(filter_name)

    spectrum_norm = spectrum.normalize(
        magnitude * synphot.units.VEGAMAG,
        bandpass,
        vegaspec=SourceSpectrum.from_vega())

    return spectrum_norm
Пример #5
0
    def pickles_to_source_spec(self, sp_type):
        """Returns a SourceSpectrum from the passed <sp_type> if it is found
        in the Pickles atlas, otherwise None is returned.
        The path to the library (which can be remote e.g.
        https://ssb.stsci.edu/trds/grid/pickles/dat_uvi) is given in
        `config.Conf.pickles_library_path"""

        source_spec = None
        filename = sptype_to_pickles_standard(sp_type)
        if filename:
            filepath = os.path.expandvars(
                os.path.join(conf.pickles_library_path, filename))
            source_spec = SourceSpectrum.from_file(filepath)
        return source_spec
Пример #6
0
    def from_file(cls, filename, **kwargs):
        """
        This is just an wrapper of the ``synphot.SourceSpectrum.from_file()`` method

        Parameters
        ----------
        filename
        kwargs

        Returns
        -------
        Spextrum instance

        """
        modelclass = SourceSpectrum.from_file(filename, **kwargs)
        sp = cls(modelclass=modelclass)
        sp.repr = "Spextrum.from_file(%s)" % filename

        return sp
Пример #7
0
class ETC(object):

    _internal_wave_unit = u.nm
    _sun_raw = SourceSpectrum.from_file(os.path.expandvars(conf.sun_file))
    _vega = SourceSpectrum.from_file(os.path.expandvars(conf.vega_file))
    _V_band = SpectralElement.from_filter('johnson_v')

    def __init__(self, config_file=None, components=None):
        PRESET_MODELS = toml.loads(
            pkg_resources.read_text(data, "FTN_FLOYDS.toml"))
        self.components = components if components is not None else []
        if config_file is None and len(self.components) == 0:
            component_config = PRESET_MODELS
        elif config_file is not None:
            if type(config_file) == dict:
                component_config = config_file
            else:
                component_config = toml.load(config_file)
        else:
            component_config = {}
        if len(component_config) > 0:
            self._create_components_from_config(component_config)

        self.combined = None
        self.combined_noatmos = None
        self.obs = None

    def _create_components_from_config(self, config):
        class_mapping = {
            'site': Site,
            'telescope': Telescope,
            'instrument': Instrument
        }
        for component, component_config in config.items():
            class_name = class_mapping.get(component.lower(), None)
            if class_name:
                #                print(class_name, component_config)
                output_component = class_name(**component_config)
                self.components.append(output_component)

    @property
    def site(self):
        sites = [x for x in self.components if isinstance(x, Site)]
        return sites[0] if len(sites) == 1 else None

    @property
    def telescope(self):
        tels = [x for x in self.components if isinstance(x, Telescope)]
        return tels[0] if len(tels) == 1 else None

    @property
    def instrument(self):
        insts = [x for x in self.components if isinstance(x, Instrument)]
        return insts[0] if len(insts) == 1 else None

    def _convert_filtername(self, filtername):
        """Converts system+filter name (e.g. 'WHT::B' to a bare filter name
        which is returned"""

        new_filtername = filtername
        if '::' in filtername:
            new_filtername = filtername.split('::')[-1]
        return new_filtername

    def _map_filter_to_standard(self, filtername):
        """Maps a passed <filtername> to a standard Johnson/Cousins/Bessell
        filter profile. Returns a SpectralElement, defaulting to V"""

        band_mapping = {
            'U': 'johnson_u',
            'B': 'johnson_b',
            'V': 'johnson_v',
            'R': conf.bessell_R_file,
            'Rc': 'cousins_r',
            'I': conf.bessell_I_file,
            'Ic': 'cousins_i',
            'J': 'bessel_j',
            'H': 'bessel_h',
            'K': 'bessel_k',
            'G': conf.gaiadr2_g_file
        }
        band_name = band_mapping.get(self._convert_filtername(filtername),
                                     'johnson_v')
        if band_name.startswith('http://svo'):
            band = SpectralElement.from_file(band_name)
        else:
            band = SpectralElement.from_filter(band_name)

        return band

    def pickles_to_source_spec(self, sp_type):
        """Returns a SourceSpectrum from the passed <sp_type> if it is found
        in the Pickles atlas, otherwise None is returned.
        The path to the library (which can be remote e.g.
        https://ssb.stsci.edu/trds/grid/pickles/dat_uvi) is given in
        `config.Conf.pickles_library_path"""

        source_spec = None
        filename = sptype_to_pickles_standard(sp_type)
        if filename:
            filepath = os.path.expandvars(
                os.path.join(conf.pickles_library_path, filename))
            source_spec = SourceSpectrum.from_file(filepath)
        return source_spec

    def sso_to_source_spec(self, tax_type):
        """Returns a SourceSpectrum from the passed <tax_type> if it is found
        in the Bus-DeMeo taxonomy, otherwise None is returned.
        The spectrum is produced by multiplying the reflectance spectra by
        a Kurucz model for the Sun so will need to be normalized"""

        source_spec = None

        if tax_type.lower().startswith('sso::') is False:
            tax_type = 'sso::' + tax_type.strip()
        config_item = conf.source_mapping.get(tax_type, None)
        if config_item is not None:
            filename = config_item()
            file_path = os.path.expandvars(filename)
            if not os.path.exists(file_path):
                file_path = str(
                    pkg_resources.files('etc.data').joinpath(filename))
            # Flux unit for LSST throughputs (*almost* FLAM but nm not Angstroms)
            lsst_funit = u.erg / u.cm**2 / u.s / u.nm
            source_spec = SourceSpectrum.from_file(file_path,
                                                   wave_unit=u.nm,
                                                   flux_unit=lsst_funit,
                                                   header_start=1)
            source_spec.meta['header']['source'] = config_item.description
            source_spec.meta['header']['filename'] = filename

        return source_spec

    def photons_from_source(self,
                            mag,
                            mag_filter,
                            filtername,
                            source_spec=None,
                            force_overlap='taper',
                            normalize=True):
        """Computes the photons coming from the source given by [source_spec] (Vega
        is assumed if not given) of the given <mag> in <mag_filter> (e.g. for "V=20",
        mag=20, mag_filter='V') when observed through the instrument's filter
        given by <filtername>
        """
        print("photons_from_source:", filtername)
        standard_filter = self._convert_filtername(filtername)
        band = self._map_filter_to_standard(mag_filter)
        print(band.meta.get('expr', 'Unknown'), type(source_spec))
        source_spec = source_spec or self._vega
        if type(source_spec) != SourceSpectrum:
            raise ETCError('Invalid sourcespec; must be a SourceSpectrum')

        # XXX Todo: allow support for AB mags here
        # test line below for comparison with SIGNAL. Difference in mag when
        # using proper normalization is ~mag-0.0155 (for B=22.7)
#        source_spec_norm = source_spec*10**(-0.4*mag)
        if normalize is True:
            source_spec_norm = source_spec.normalize(mag * units.VEGAMAG,
                                                     band,
                                                     vegaspec=self._vega)
        else:
            source_spec_norm = source_spec

        self._create_combined()
        filter_waves, filter_trans = self.instrument.filterset[
            filtername]._get_arrays(None)
        throughput = self._throughput_for_filter(filtername)
        waves, thru = throughput._get_arrays(filter_waves)
        spec_elements = SpectralElement(Empirical1D,
                                        points=filter_waves,
                                        lookup_table=filter_trans * thru)

        # get the synphot observation object
        synphot_obs = Observation(source_spec_norm,
                                  spec_elements,
                                  force=force_overlap)
        if self.obs is None:
            self.obs = synphot_obs
        countrate = synphot_obs.countrate(area=self.telescope.area)

        return countrate

    def ccd_snr(self,
                exp_time,
                V_mag,
                filtername,
                source_spec=None,
                npix=1 * u.pixel,
                n_background=np.inf * u.pixel,
                background_rate=0 * (u.ph / u.pixel / u.s),
                sky_mag=None,
                darkcurrent_rate=0 * (u.ph / u.pixel / u.s),
                normalize=True):
        """Calculate the SNR in a given exposure time <exp_time> for a given <V_mag>
        """

        if filtername not in self.instrument.filterlist:
            raise ETCError('Filter name {0} is invalid.'.format(filtername))

        try:
            exp_time = exp_time.to(u.s)
        except AttributeError:
            exp_time = exp_time * u.s

        # Get countrate per second for this magnitude in the filter
        # Check first if we weren't given a rate directly
        try:
            countrate = V_mag.to(u.photon / u.s)
        except (AttributeError, u.UnitConversionError):
            countrate = self.photons_from_source(V_mag,
                                                 'V',
                                                 filtername,
                                                 source_spec=source_spec,
                                                 normalize=normalize)

        # define countrate to be in photons/sec, which can be treated as the
        # same as (photo)electrons for CCDs but are not technically convertible in
        # astropy.units:
        countrate = countrate.value * (u.photon / u.s)

        print("Counts/s=", countrate)

        readnoise = self.instrument.ccd_readnoise.value * (u.photon / u.pixel)
        readnoise_sq = readnoise.value**2 * (u.photon / u.pixel)
        gain_err = self._get_shotnoise(self.instrument.ccd_gain *
                                       self.instrument.adc_error)
        print("Readnoise=", readnoise, readnoise_sq, gain_err)

        fwhm = self.instrument.fwhm
        pixel_scale = self.instrument.ccd_pixscale
        pixel_area = pixel_scale * pixel_scale

        obs_filter = self.instrument.filterset[filtername]

        if background_rate > 0:
            print("Specific rate given")
            sky_countrate = background_rate / pixel_area / obs_filter.equivwidth(
            )
        else:
            # Obtain sky value based on flux at mag=0 for given filter
            sky_filtername = self._convert_filtername(filtername)
            sky = self.site.sky_spectrum(sky_filtername)
            print("  Original sky for =", sky(sky.avgwave()),
                  sky.meta.get('header', {}).get('filename', ''))
            prefix = "Using"
            sky2 = None
            if sky_mag is None:
                # Try and obtain sky magnitude from Site. This should probably
                # move into sky_spectrum()
                sky_mag = self.site.sky_mags.get(sky_filtername, None)
                prefix = "Determined new"
                if sky_mag is None:
                    if self.site.radiance is None:
                        raise ETCError(
                            'Could not determine a valid sky magnitude for filter: {0}'
                            .format(sky_filtername))
                    else:
                        print(
                            "Attempting synthesized sky magnitude from radiance spectrum"
                        )
                        normalizing_sky_mag = self.site.sky_mags.get('V', None)
                        V_band = self._map_filter_to_standard('V')
                        print(
                            "Normalizing sky spectrum to {} in V band".format(
                                normalizing_sky_mag))
                        radiance_norm = sky.normalize(normalizing_sky_mag *
                                                      units.VEGAMAG,
                                                      V_band,
                                                      vegaspec=self._vega)
                        obs_rad_norm = Observation(radiance_norm,
                                                   obs_filter,
                                                   force='taper')
                        sky_mag = obs_rad_norm.effstim(units.VEGAMAG,
                                                       vegaspec=self._vega)
                        sky_mag = sky_mag.value
                        sky2 = radiance_norm

            print("{} sky magnitude of {:.2f} for {} band".format(
                prefix, sky_mag, sky_filtername))
            if sky2 is None:
                # Compute countrate from given sky magnitude.
                # XXX need to refactor photons_from_source() to do this also
                sky_filter = self._map_filter_to_standard(sky_filtername)
                print("sky_filter", sky_filter.meta.get('expr', 'Unknown'),
                      type(sky))

                sky2 = sky.normalize(sky_mag * units.VEGAMAG,
                                     sky_filter,
                                     vegaspec=self._vega)
            print("Normalized sky=", sky2(sky2.avgwave()),
                  sky2(sky2.avgwave()) * 10**(-sky_mag / 2.5))
            self._create_combined()
            throughput = self._throughput_for_filter(filtername, atmos=False)
            waves, thru = throughput._get_arrays(None)
            filter_waves, filter_trans = obs_filter._get_arrays(waves)
            print('thruput=', thru.max(),
                  self.telescope.area.to(units.AREA) * thru.max())
            spec_elements = SpectralElement(Empirical1D,
                                            points=filter_waves,
                                            lookup_table=thru * filter_trans)

            # get the synphot observation object
            warnings.simplefilter('ignore', category=AstropyUserWarning)
            sky_obs = Observation(sky2, spec_elements, force='taper')

            sky_countrate = sky_obs.countrate(area=self.telescope.area)
            # Actually a count rate per arcsec^2 as the original magnitude is
            # also per sq. arcsec. Also set to photons/s to match signal from object
            sky_countrate = sky_countrate.value * (u.photon / u.s /
                                                   u.arcsec**2)
            print("Sky (photons/AA/arcsec^2)=", sky_countrate,
                  sky_countrate / obs_filter.equivwidth())
            sky_per_pixel = sky_countrate * pixel_area
            sky_per_pixel /= u.pixel
            print("Sky (photons/pixel)=", sky_per_pixel)
            background_rate = sky_per_pixel

        # For spectrographs, calculate fraction of light passing through slit
        vign = self.instrument.slit_vignette()

        if npix == 1 * u.pixel:
            # Calculate new value based on area of FWHM and pixelscale
            # IRAF `ccdtime` uses this definition. Pass in a suitable
            # value for npix into this method to emulate this.
            #        npix = 1.4*(fwhm / pixel_scale)**2
            npix = np.pi * (fwhm / pixel_scale)**2
            print("npix (fractional pixels)=", npix)
            npix = int(round(npix.value)) * u.pixel

        print("npix (whole pixels)=", npix)
        sobj2 = countrate * exp_time
        sky_countrate = sky_countrate * exp_time / obs_filter.equivwidth()
        dark_count = darkcurrent_rate.to(u.photon / u.pix / u.s) * exp_time
        print("Noise(Star)=", np.sqrt(sobj2))
        print("Noise(Dark)=", np.sqrt(npix * dark_count), dark_count)
        print("Noise( CCD)=", np.sqrt(npix * readnoise_sq))
        noise_ccd = np.sqrt(npix * (dark_count + readnoise_sq))
        ssky2 = background_rate * exp_time
        peak = sobj2 / 1.14 / fwhm / fwhm * pixel_area


        print('Detected photons            from object =', sobj2, \
             ' (max',peak,' /pixel)')
        print('Detected photons/A/arcsec^2 from sky    =', sky_countrate)
        print('Detected photons/pixel      from sky    =', ssky2)
        snr = self._compute_snr(sobj2, ssky2, npix, dark_count, readnoise_sq)
        print('Signal-to-noise                         =', snr.value)
        return snr.value

    def _compute_snr(self, sobj2, ssky2, npix, dark_count, readnoise_sq):
        """Compute the signal-to-noise via the CCD equation
        Ref: Howell (2006), page 76
        """
        snr = sobj2 / np.sqrt(sobj2 + npix *
                              (ssky2 + dark_count + readnoise_sq))

        return snr

    def exptime_from_ccd_snr(self,
                             snr,
                             V_mag,
                             filtername,
                             npix=1 * u.pixel,
                             n_background=np.inf * u.pixel,
                             background_rate=0 * (u.ct / u.pixel / u.s),
                             sky_mag=None,
                             darkcurrent_rate=0 * (u.ct / u.pixel / u.s)):

        countrate = self.photons_from_source(V_mag, filtername)

        # make sure the gain is in the correct units
        gain = self.instrument.ccd_gain
        if gain.unit in (u.electron / u.adu, u.photon / u.adu):
            gain = gain.value * (u.ct / u.adu)
        elif gain.unit != u.ct / u.adu:
            raise u.UnitsError('gain must have units of (either '
                               'astropy.units.ct, astropy.units.electron, or '
                               'astropy.units.photon) / astropy.units.adu')

        # get the countrate from the synphot observation object
        countrate = countrate / gain
        print(countrate)
        # define counts to be in ADU, which are not technically convertible in
        # astropy.units:
        countrate = countrate.value * (u.ct / u.s)

        # necessary for units to work in countrate calculation:
        if not hasattr(snr, 'unit'):
            snr = snr * np.sqrt(1 * u.ct)
        readnoise = self._get_shotnoise(self.instrument.ccd_readnoise)
        gain_err = self._get_shotnoise(self.instrument.ccd_gain *
                                       self.instrument.adc_error)
        print(readnoise, gain_err)
        if sky_mag is not None:
            # Compute countrate from given sky magnitude.
            # XXX need to refactor photons_from_source() to do this also
            sky_filtername = filtername
            if '::' in filtername:
                sky_filtername = filtername.split('::')[-1]
            sky = self.site.sky_spectrum(sky_filtername)
            print("  Original sky=", sky(sky.avgwave()))
            sky2 = sky.normalize(sky_mag * units.VEGAMAG,
                                 self._V_band,
                                 vegaspec=self._vega)
            print("Normalized sky=", sky2(sky2.avgwave()),
                  sky(sky.avgwave()) * 10**(-sky_mag / 2.5))
            self._create_combined()
            waves, thru = self.combined._get_arrays(None)
            obs_filter = self.instrument.filterset[filtername]
            filter_waves, filter_trans = obs_filter._get_arrays(waves)
            signal_combined = self.combined / self.instrument.ccd_qe
            signal_waves, signal_thru = signal_combined._get_arrays(None)
            print('thruput (Atm*tel*instr)=', signal_thru.max())
            print('thruput (Atm*tel*instr*CCD)=', thru.max(),
                  self.telescope.area.to(units.AREA) * thru.max())
            spec_elements = SpectralElement(Empirical1D,
                                            points=filter_waves,
                                            lookup_table=thru * filter_trans)

            # get the synphot observation object
            with warnings.simplefilter('ignore', category=AstropyUserWarning):
                sky_obs = Observation(sky2, spec_elements)

            sky_countrate = sky_obs.countrate(area=self.telescope.area)
            # Actually a count rate per arcsec^2 as the original magnitude is
            # also per sq. arcsec
            sky_countrate /= u.arcsec**2
            print("Sky (photons/AA/arcsec^2)=", sky_countrate,
                  sky_countrate / (1.0 * 860) * u.AA)
            sky_per_pixel = sky_countrate * self.instrument.ccd_pixscale * self.instrument.ccd_pixscale
            sky_per_pixel /= u.pixel
            print("Sky (photons/pixel)=", sky_per_pixel)
            background_rate = sky_per_pixel

        npix = np.pi * (self.instrument.fwhm / self.instrument.ccd_pixscale)**2
        npix *= u.pixel
        print("npix=", npix)

        # solve t with the quadratic equation (pg. 77 of Howell 2006)
        A = countrate**2
        B = (-1) * snr**2 * (countrate + npix *
                             (background_rate + darkcurrent_rate))
        C = (-1) * snr**2 * npix * readnoise**2
        t = (-B + np.sqrt(B**2 - 4 * A * C)) / (2 * A)

        if gain_err.value > 1 or np.isfinite(n_background.value):
            from scipy.optimize import fsolve
            # solve t numerically
            t = fsolve(_t_with_small_errs,
                       t,
                       args=(background_rate, darkcurrent_rate, gain_err,
                             readnoise, countrate, npix, n_background))
            t = float(t) * u.s

        return t

    def _get_shotnoise(self, detector_property):
        """
        Returns the shot noise (i.e. non-Poissonion noise) in the correct
        units. ``detector_property`` must be a Quantity.
        """
        # Ensure detector_property is in the correct units:
        if detector_property.unit in (u.electron / u.pix, ):
            detector_property = (detector_property.value /
                                 self.instrument.ccd_gain.value) * (u.ct /
                                                                    u.pix)
        detector_property = detector_property.to(u.ct / u.pixel)
        return detector_property.value * np.sqrt(1 * (u.ct / u.pixel))

    def _t_with_small_errs(self, t, background_rate, darkcurrent_rate,
                           gain_err, readnoise, countrate, npix, n_background):
        """
        Returns the full expression for the exposure time including the
        contribution to the noise from the background and the gain.
        """
        if not hasattr(t, 'unit'):
            t = t * u.s

        detector_noise = (background_rate * t + darkcurrent_rate * t +
                          gain_err**2 + readnoise**2)
        radicand = countrate * t + (npix *
                                    (1 + npix / n_background) * detector_noise)

        return countrate * t / np.sqrt(radicand)

    def _do_plot(self,
                 waves,
                 thru,
                 filterlist=[],
                 filterset=None,
                 title='',
                 plot_label='',
                 left=300 * u.nm,
                 right=1200 * u.nm,
                 bottom=0.0,
                 top=None):
        """Plot worker.

        Parameters
        ----------
        waves, thru : `~astropy.units.quantity.Quantity`
            Wavelength and throughput to plot.

        filterset : list
            List of filter bandpasses to plot

        kwargs :

        title : str
            Plot title.

        plot_label : str
            Line plot label for legend

        left, right : `None`, number or `Quantity`
            Minimum and maximum wavelengths to plot.
            If `None`, uses the whole range. If a number is given,
            must be in Angstrom. If an `~astropy.units.quantity.Quantity`
            is given, it will be converted tp the internal wave units.

        bottom, top : `None` or number
            Minimum and maximum flux/throughput to plot.
            If `None`, uses the whole range. If a number is given,
            it assumed to be in throughput (0..1).
        """
        try:
            import matplotlib.pyplot as plt
        except ImportError:
            print(
                'No matplotlib installation found; plotting disabled as a result.'
            )
            return

        fig, ax = plt.subplots()
        ax.plot(waves, thru, label=plot_label)

        # Plot filters
        for filtername in filterlist:
            filter_wave, filter_trans = filterset[filtername]._get_arrays(
                waves)
            filt_plot = ax.plot(filter_wave.to(self._internal_wave_unit),
                                filter_trans * thru,
                                linestyle='--',
                                linewidth=1,
                                label=filtername)

        # Custom wavelength limits
        if left is not None:
            if isinstance(left, u.Quantity):
                left_val = left.to(self._internal_wave_unit).value
            else:
                left_val = left
            ax.set_xlim(left=left_val)
        if right is not None:
            if isinstance(right, u.Quantity):
                right_val = right.to(self._internal_wave_unit).value
            else:
                right_val = right
            ax.set_xlim(right=right_val)

        # Custom flux/throughput limit
        if bottom is not None:
            ax.set_ylim(bottom=bottom)
        if top is not None:
            ax.set_ylim(top=top)

        wave_unit = waves.unit
        ax.set_xlabel('Wavelength ({0})'.format(wave_unit))

        yu = thru.unit
        if yu is u.dimensionless_unscaled:
            ax.set_ylabel('Unitless')
        elif yu is u.percent:
            ax.set_ylabel('Efficiency ({0})'.format(yu))
        else:
            ax.set_ylabel('Flux ({0})'.format(yu))

        if title:
            ax.set_title(title)

        # Turn on minorticks on both axes
        ax.minorticks_on()
        if len(filterlist) > 0 or plot_label != '':
            ax.legend()

        plt.draw()

    def plot(self, filterlist=[], **kwargs):
        """Plot combined system throughput and filters. By default (if
        `filterlist=[]`) then all instrument filters are plotted. To plot
        specific filters, set `filterlist` to a string of the filtername or
        a list of filternames. To disable filter plotting completely, set
        `filterlist=None`

        Parameters
        ----------
        waves, thru : `~astropy.units.quantity.Quantity`
            Wavelength and throughput to plot.

        filterlist : list, str or None
            Filter name(s) to plot

        kwargs :
            atmos : bool
                Whether to include the atmosphere or not
            Also see :func:`do_plot`.

        """
        self._create_combined()
        if kwargs.get('atmos', True) is True:
            waves, trans = self.combined[0]._get_arrays(None)
        else:
            waves, trans = self.combined_noatmos[0]._get_arrays(None)
        for channel_num in range(1, self.instrument.num_channels):
            if kwargs.get('atmos', True) is True:
                chan_trans = self.combined[channel_num](waves)
            else:
                chan_trans = self.combined_noatmos[channel_num](waves)
            trans *= chan_trans

        if 'atmos' in kwargs:
            del (kwargs['atmos'])
        filterset = None
        # Handle single filter string case
        if isinstance(filterlist, str):
            filterlist = [
                filterlist,
            ]
        elif filterlist == []:
            filterlist = self.instrument.filterlist
        elif filterlist is None:
            filterlist = []
        if len(filterlist) > 0 and set(filterlist).issubset(
                set(self.instrument.filterlist)):
            filterset = self.instrument.filterset
        self._do_plot(waves.to(self._internal_wave_unit), trans, filterlist,
                      filterset, **kwargs)

    def plot_efficiency(self, filtername, **kwargs):
        """Plot combined system throughput for filtername.

        Parameters
        ----------
        filtername : str
            Filter name to plot

        kwargs :
            atmos : bool
                Whether to include the atmosphere or not (default is True)
            Also see :func:`do_plot` for additional options.

        """
        # Handle single filter string case
        if filtername in self.instrument.filterlist:
            throughput = self._throughput_for_filter(filtername,
                                                     atmos=kwargs.get(
                                                         'atmos', True))

            if kwargs.get('atmos', True) is True:
                atmos_presence = 'incl.'
            else:
                atmos_presence = 'excl.'
            if 'atmos' in kwargs:
                del (kwargs['atmos'])
            if 'title' not in kwargs:
                kwargs['title'] = 'System Efficiency {} Atmosphere'.format(
                    atmos_presence)

            efficiency = throughput * self.instrument.filterset[filtername]
            # Add slit/fiber vignetting (if applicable)
            vignetting = self.instrument.slit_vignette()
            #            print("Vignetting", vignetting, (1.0-vignetting)*100., self.instrument.fiber_diameter, self.instrument.fwhm)
            efficiency = efficiency * vignetting
            self.efficiency = efficiency
            waves = efficiency.waveset
            thru = efficiency(waves) * 100.0 * u.percent
            self._do_plot(waves.to(self._internal_wave_unit),
                          thru,
                          filterlist=[],
                          filterset=[],
                          **kwargs)
        else:
            raise ETCError('Filter name {0} is invalid.'.format(filtername))

    def _channel_for_filter(self, filtername):
        """Maps the passed <filtername> to Instrument channel number which is
        returned"""

        channel_name = self.instrument.filter2channel_map.get(
            filtername, 'default')
        try:
            index = list(self.instrument.channelset).index(channel_name)
        except ValueError:
            print("Filter {} not in instrument channel map".format(filtername))
        return index

    def _throughput_for_filter(self, filtername, atmos=True):
        self._create_combined()
        if atmos is True:
            throughput = self.combined
        else:
            throughput = self.combined_noatmos
        # Map filtername to instrument channel and correct throughput
        index = self._channel_for_filter(filtername)
        throughput = throughput[index]

        return throughput

    def _create_combined(self):
        if self.combined_noatmos is None:
            self.combined_noatmos = []
            for channel in self.instrument.channelset.values():
                channel_combined = self.telescope.reflectivity * self.instrument.transmission * channel.transmission * channel.ccd_qe
                self.combined_noatmos.append(channel_combined)
        if self.combined is None:
            self.combined = []
            for channel in self.combined_noatmos:
                channel_combined = channel * self.site.transmission
                self.combined.append(channel_combined)
        # Collapse lists back down to single instance if only one channel
        # if self.instrument.num_channels == 1:
        # self.combined_noatmos = self.combined_noatmos[0]
        # self.combined = self.combined[0]

    def __repr__(self):
        prefixstr = '<' + self.__class__.__name__ + ' '
        compstr = "->".join([repr(x) for x in self.components])
        return f'{prefixstr}{compstr}>'
Пример #8
0
def vega_spectrum(mag=0):
    vega = SourceSpectrum.from_file(pth.join(__pkg_dir__, "vega.fits"))
    vega = vega * 10**(-0.4 * mag)
    return vega
Пример #9
0
def sim_heeps():
    """Do the simulation"""

    # Prepare the source
    cube = fits.open(FNAME)
    imwcs = WCS(cube[0].header).sub([1, 2])
    naxis1, naxis2, naxis3 = (cube[0].header['NAXIS1'],
                              cube[0].header['NAXIS2'],
                              cube[0].header['NAXIS3'])

    # Set up the instrument
    cmd = sim.UserCommands(use_instrument='METIS', set_modes='img_lm')

    # Set up detector size to match the input cube - using the full
    # METIS detector of 2048 x 2048 pixels would require too much memory
    cmd["!DET.width"] = naxis1
    cmd["!DET.height"] = naxis2

    metis = sim.OpticalTrain(cmd)

    # Set included and excluded effects
    metis['armazones_atmo_skycalc_ter_curve'].include = True
    metis['armazones_atmo_default_ter_curve'].include = False

    metis['scope_surface_list'].include = False
    metis['eso_combined_reflection'].include = True

    metis['detector_array_list'].include = False
    metis['detector_window'].include = True

    metis['scope_vibration'].include = False
    metis['detector_linearity'].include = False
    metis['metis_psf_img'].include = False

    # We attach the spectrum of Vega to the image
    spec = SourceSpectrum.from_file("alpha_lyr_stis_008.fits")

    # Exposure parameters
    dit = cube[0].header['CDELT3']
    ndit = 1

    # Loop over the input cube
    nplanes = 100
    # nplanes = naxis3  # for full cube

    nplanes = min(nplanes, naxis3)

    for i in range(nplanes):
        print("Plane", i + 1, "/", nplanes)
        imhdu = fits.ImageHDU(header=imwcs.to_header(),
                              data=cube[0].data[i, :, :] * FLUX_SCALE)
        src = sim.Source(spectra=[spec], image_hdu=imhdu)

        metis.observe(src)
        outhdu = metis.readout(dit=dit, ndit=ndit)[0]
        cube[0].data[i, :, :] = outhdu[1].data.astype(np.float32)

    # We have filled up the original cube with the simulated data
    # Write out only the simulated planes
    fits.writeto(OUTFILE,
                 data=cube[0].data[:nplanes, :, :],
                 header=cube[0].header,
                 overwrite=True)