def collapse(self, waveset): throughput = self.radiometry_table.throughput(waveset) self._throughput = SpectralElement(Empirical1D, points=waveset, lookup_table=throughput) emission = self.radiometry_table.emission(waveset) self._emission = SourceSpectrum(Empirical1D, points=waveset, lookup_table=emission)
def rebin_spectra(self, new_waves): """ Rebin a synphot spectra to a new wavelength grid conserving flux. Grid does not need to be linear and can be at higher or lower resolution Parameters ---------- new_waves: an array of the output wavelenghts in Angstroms but other units can be specified Returns ------- a new Spextrum instance """ if isinstance(new_waves, u.Quantity): new_waves = new_waves.to(u.AA).value waves = self.waveset.value # else assumed to be angstroms f = np.ones(len(waves)) filt = SpectralElement(Empirical1D, points=waves, lookup_table=f) obs = Observation(self, filt, binset=new_waves, force='taper') newflux = obs.binflux rebin_spec = Empirical1D(points=new_waves, lookup_table=newflux, meta=self.meta) sp = Spextrum(modelclass=rebin_spec) sp = self._restore_attr(sp) sp.meta.update({"rebinned_spectra": True}) return sp
def photons_in_range(self, wmin=None, wmax=None, area=1 * u.cm**2, filter_curve=None): """ Return the number of photons between wave_min and wave_max or within a bandpass (filter) Parameters ---------- wmin : [Angstrom] wmax : [Angstrom] area : u.Quantity [cm2] filter_curve : str Name of a filter from - a generic filter name (see ``DEFAULT_FILTERS``) - a spanish-vo filter service reference (e.g. ``"Paranal/HAWKI.Ks"``) - a filter in the spextra database - the path to the file containing the filter (see ``Passband``) - a ``Passband`` or ``synphot.SpectralElement`` object Returns ------- counts : u.Quantity array """ if isinstance(area, u.Quantity): area = area.to(u.cm**2).value # if isinstance(wmin, u.Quantity): wmin = wmin.to(u.Angstrom).value if isinstance(wmax, u.Quantity): wmin = wmax.to(u.Angstrom).value if filter_curve is None: # this makes a bandpass out of wmin and wmax try: mid_point = 0.5 * (wmin + wmax) width = abs(wmax - wmin) filter_curve = SpectralElement(Box1D, amplitude=1, x_0=mid_point, width=width) except ValueError("Please specify wmin/wmax or a filter"): raise elif os.path.exists(filter_curve): filter_curve = Passband.from_file(filename=filter_curve) elif isinstance(filter_curve, (Passband, SpectralElement)): filter_curve = filter_curve else: filter_curve = Passband(filter_curve) obs = Observation(self, filter_curve) counts = obs.countrate(area=area * u.cm**2) return counts
def _get_ter_property(self, ter_property): """ Looks for arrays for transmission, emissivity, or reflection Parameters ---------- ter_property : str ``transmission``, ``emissivity``, ``reflection`` Returns ------- response_curve : ``synphot.SpectralElement`` """ compliment_names = ["transmission", "emissivity", "reflection"] ii = np.where([ter_property == name for name in compliment_names])[0][0] compliment_names.pop(ii) wave = self._get_array("wavelength") value_arr = self._get_array(ter_property) if value_arr is None: value_arr = self._compliment_array(*compliment_names) if value_arr is not None and wave is not None: response_curve = SpectralElement(Empirical1D, points=wave, lookup_table=value_arr) else: response_curve = None warnings.warn("Both wavelength and {} must be set" "".format(ter_property)) return response_curve
def test_blackbody_maximum_agrees_with_wien(self, temp): '''Check the maximum of emission against Wien's law for photon rate''' emissivity = SpectralElement(Empirical1D, points=[1, 20], lookup_table=[1., 1.]) flux = surf_utils.make_emission_from_emissivity(temp, emissivity) dlam = 0.1 wave = np.arange(3, 20, dlam) * u.um wavemax = wave[np.argmax(flux(wave))] if isinstance(temp, u.Quantity): temp = temp.to(u.Kelvin, equivalencies=u.temperature()).value wienmax = 3669.7 * u.um / temp assert np.abs(wavemax - wienmax.to(u.um)) < dlam * u.um
def test_returns_correct_half_flux_with_bandpass(self): flux = np.ones(11) * u.Unit("ph s-1 m-2 um-1") wave = np.linspace(0.5, 2.5, 11) * u.um spec = SourceSpectrum(Empirical1D, points=wave, lookup_table=flux) bandpass = SpectralElement(Empirical1D, points=np.linspace(1, 2, 13) * u.um, lookup_table=0.5 * np.ones(13)) counts = source_utils.photons_in_range([spec], 1 * u.um, 2 * u.um, bandpass=bandpass) assert counts.value == approx(0.5)
def filter_mag(filterfile, filterzero, syn_spec, syn_err=None, wrange=None): ''' ###################################################### # Input # # -------------------------------------------------- # # filterfile: file containing filter function # # filterzero: flux [Jy] corresponding to mag=0 # # syn_spec: synphot spectrum object of source # # wrange: (start,end) observed wavelength range # # -------------------------------------------------- # # Output # # -------------------------------------------------- # # mag: magnitude of source in filter system. # ###################################################### ''' from synphot import SpectralElement, Observation, Empirical1D, SourceSpectrum import astropy.units as u #Load ascii filter function if filterfile.split('/')[-1][:6] == 'Bessel': filt = SpectralElement.from_file(filterfile, wave_unit='nm') else: filt = SpectralElement.from_file(filterfile, wave_unit='AA') if wrange is None: fwave = filt.waveset swave = syn_spec.waveset wrange = (max(fwave[0],swave[0]),min(fwave[-1],swave[-1])) waves = np.linspace(wrange[0], wrange[-1], 10000) #Synthetic observation obs = Observation(syn_spec, filt, force='extrap') flux = obs.effstim(flux_unit='jy', waverange=wrange).value #flux = obs.effstim(flux_unit='jy', wavelengths=waves).value #Calibrate magnitude with zero point mag = -2.512*np.log10(flux/filterzero) #Synthetic observation of error spectrum if syn_err is not None: #square filter and error spectrum filt2 = SpectralElement(Empirical1D, points=fwave, lookup_table=np.square(filt(fwave))) pseudo_flux = np.square(syn_err(swave,flux_unit='jy')).value syn_err2 = SourceSpectrum(Empirical1D, points=swave, lookup_table=pseudo_flux) #sum errors in quadrature obs = Observation(syn_err2, filt2, force='extrap') #flux_err = np.sqrt(obs.effstim(waverange=wrange).value) flux_err = np.sqrt(obs.effstim(wavelengths=waves).value) #convert to magnitude error mag_err = (2.5/np.log(10))*(flux_err/flux) return mag, mag_err else: return mag
def rebin_transmission(self, wavenew): """Rebins a potentially higher resolution atmospheric transmission spectrum to a new wavelength grid <wavenew>. Returns a new BaseUnitlessSpectrum constructed from the new passed wavelength grid and the resampled transmission""" wave = self.transmission.waveset specin = self.transmission(wave) # Have to pretend transmission is in a flux unit to make a SourceSpectrum # so drop here and put back at the end orig_unit = specin.unit spec = SourceSpectrum(Empirical1D, points=wave, lookup_table=specin.value) f = np.ones(len(wave)) filt = SpectralElement(Empirical1D, points=wave, lookup_table=f) obs = Observation(spec, filt, binset=wavenew, force='taper') new_trans = SpectralElement(Empirical1D, points=wavenew, lookup_table=obs.binflux.value * orig_unit) return new_trans
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 test_with_bandpass_and_area_returns_correct_value( self, flux, area, expected): flux = flux * u.Unit("ph s-1 m-2 um-1") spec = SourceSpectrum(Empirical1D, points=np.linspace(0.5, 2.5, 11) * u.um, lookup_table=flux) bandpass = SpectralElement(Empirical1D, points=np.linspace(1, 2, 13) * u.um, lookup_table=0.5 * np.ones(13)) counts = source_utils.photons_in_range([spec], 1 * u.um, 2 * u.um, bandpass=bandpass, area=area) assert counts.value == approx(expected)
def filter_flux(filterfile, syn_spec, syn_err=None, wrange=None): ''' ###################################################### # Input # # -------------------------------------------------- # # filterfile: file containing filter function # # syn_spec: synphot spectrum object of source # # wrange: (start,end) observed wavelength range # # -------------------------------------------------- # # Output # # -------------------------------------------------- # # flux: flux of source in filter system. # ###################################################### ''' from synphot import SpectralElement, Observation, Empirical1D, SourceSpectrum import astropy.units as u #Load ascii filter function if filterfile.split('/')[-1][:6] == 'Bessel': filt = SpectralElement.from_file(filterfile, wave_unit='nm') else: filt = SpectralElement.from_file(filterfile, wave_unit='AA') if wrange is None: fwave = filt.waveset swave = syn_spec.waveset wrange = (max(fwave[0],swave[0]),min(fwave[-1],swave[-1])) wlengths = fwave[np.logical_and(fwave>wrange[0], fwave<wrange[1])] else: fwave = filt.waveset wlengths = fwave[np.logical_and(fwave>wrange[0]*u.AA, fwave<wrange[1]*u.AA)] #Synthetic observation obs = Observation(syn_spec, filt, force='extrap') flux = obs.effstim(flux_unit='jy', waverange=wrange).value #flux = obs.effstim(flux_unit='jy', wavelengths=wlengths).value #Synthetic observation of error spectrum if syn_err is not None: #square filter and error spectrum filt2 = SpectralElement(Empirical1D, points=fwave, lookup_table=np.square(filt(fwave))) pseudo_flux = np.square(syn_err(swave,flux_unit='jy')).value syn_err2 = SourceSpectrum(Empirical1D, points=swave, lookup_table=pseudo_flux) #sum errors in quadrature obs = Observation(syn_err2, filt2, force='extrap') flux_err = np.sqrt(obs.effstim(waverange=wrange).value) return flux, flux_err else: return flux
def system_transmission(self): wave_unit = u.Unit(rc.__currsys__["!SIM.spectral.wave_unit"]) dwave = rc.__currsys__["!SIM.spectral.spectral_bin_width"] wave_min = rc.__currsys__["!SIM.spectral.wave_min"] wave_max = rc.__currsys__["!SIM.spectral.wave_max"] wave = np.arange(wave_min, wave_max, dwave) trans = np.ones_like(wave) sys_trans = SpectralElement(Empirical1D, points=wave * u.Unit(wave_unit), lookup_table=trans) for effect in self.get_z_order_effects(100): sys_trans *= effect.throughput return sys_trans
def square(cls, wmin, wmax, transmission): """ Creates a filter with a rectangular shape Returns ------- """ if isinstance(wmin, u.Quantity) is False: wmin = wmin*u.AA if isinstance(wmax, u.Quantity) is False: wmax = wmax*u.AA center = (wmax + wmin) * 0.5 width = wmax - wmin modelclass = SpectralElement(Box1D, amplitude=transmission, x_0=center, width=width) return cls(modelclass=modelclass)
def gaussian(cls, center, fwhm, peak, **kwargs): """ Creates a filter with a gaussian shape with given user parameters Returns ------- """ if isinstance(center, u.Quantity) is False: center = center*u.AA if isinstance(fwhm, u.Quantity) is False: fwhm = fwhm*u.AA sigma = fwhm / (2.0 * np.sqrt(2.0 * np.log(2.0))) modelclass = SpectralElement(Gaussian1D, amplitude=peak, mean=center, stddev=sigma, **kwargs) return cls(modelclass=modelclass)
def download_svo_filter(filter_name, return_style="synphot"): """ Query the SVO service for the true transmittance for a given filter Copied 1 to 1 from tynt by Brett Morris Parameters ---------- filt : str Name of the filter as available on the spanish VO filter service e.g: ``Paranal/HAWKI.Ks`` return_style : str, optional Defines the format the data is returned - synphot: synphot.SpectralElement - table: astropy.table.Table - quantity: astropy.unit.Quantity [wave, trans] - array: np.ndarray [wave, trans], where wave is in Angstrom - vo_table : astropy.table.Table - original output from SVO service Returns ------- filt_curve : See return_style Astronomical filter object. """ path = download_file('http://svo2.cab.inta-csic.es/' 'theory/fps3/fps.php?ID={}'.format(filter_name), cache=True) tbl = Table.read(path, format='votable') wave = u.Quantity(tbl['Wavelength'].data.data, u.Angstrom, copy=False) trans = tbl['Transmission'].data.data if return_style == "synphot": filt = SpectralElement(Empirical1D, points=wave, lookup_table=trans) elif return_style == "table": filt = Table(data=[wave, trans], names=["wavelength", "transmission"]) filt.meta["wavelength_unit"] = "Angstrom" elif return_style == "quantity": filt = [wave, trans] elif return_style == "array": filt = [wave.value, trans] elif return_style == "vo_table": filt = tbl return filt
def set_bandpass_from_filter(self, filtername): """Loads the specified <filtername> from the transmission profile file which is mapped via the etc.config.Conf() items. Returns a SpectralElement instance for the filter profile """ if len(filtername) == 2 and filtername[1] == 'p': filtername = filtername[0] filename = conf.mapping.get(filtername, None) if filename is None: raise ETCError('Filter name {0} is invalid.'.format(filtername)) if 'LCO_' in filename().upper() and '.csv' in filename().lower(): file_path = pkg_resources.files('etc.data').joinpath( os.path.expandvars(filename())) source = "LCO iLab format" header, wavelengths, throughput = self._read_lco_filter_csv( file_path) elif 'http://svo' in filename().lower(): source = "SVO filter service" header, wavelengths, throughput = specio.read_remote_spec( filename(), wave_unit=u.AA, flux_unit=units.THROUGHPUT) else: source = "local file" file_path = pkg_resources.files('etc.data').joinpath( os.path.expandvars(filename())) warnings.simplefilter('ignore', category=AstropyUserWarning) header, wavelengths, throughput = specio.read_ascii_spec( file_path, wave_unit=u.nm, flux_unit=units.THROUGHPUT) if throughput.mean() > 1.0: throughput /= 100.0 header['notes'] = 'Divided by 100.0 to convert from percentage' print("Reading from {} for {}".format(source, filtername)) header['filename'] = filename header['descrip'] = filename.description meta = {'header': header, 'expr': filtername} return SpectralElement(Empirical1D, points=wavelengths, lookup_table=throughput, meta=meta)
def from_vectors(cls, waves, trans, meta=None, wave_unit=u.AA): """ Create a ``Passband`` directly from from vectos (lists, numpy.arrays, etc) Parameters ---------- waves: list-like trans: list-like meta: dictionary containing the metadata wave_unit: u.Quantity, defaulted to angstroms Returns ------- Passband """ if isinstance(waves, u.Quantity) is False: waves = waves*wave_unit modelclass = SpectralElement(Empirical1D, points=waves, lookup_table=trans, meta=meta) return cls(modelclass=modelclass)
def get_filter(filter_name): # first check locally # check generics # check spanish_vo path = find_file(filter_name, silent=True) if path is not None: tbl = ioascii.read(path) wave = quantity_from_table("wavelength", tbl, u.um).to(u.um) filt = SpectralElement(Empirical1D, points=wave, lookup_table=tbl["transmission"]) elif filter_name in FILTER_DEFAULTS: filt = download_svo_filter(FILTER_DEFAULTS[filter_name]) else: try: filt = download_svo_filter(filter_name) except: filt = None return filt
def __init__(self, name=None, camera_type="CCD", **kwargs): _cam_types = [ "CCD", ] self.camera_type = camera_type.upper() if camera_type.upper( ) in _cam_types else "CCD" self.name = name if name is not None else self.camera_type + ' camera' # Defaults assume no elements per channel on the assumption that it's # in the common optics (populated in Instrument()) self.num_ar_coatings = kwargs.get('num_chan_ar_coatings', 0) self.num_lenses = kwargs.get('num_chan_lenses', 0) self.num_mirrors = kwargs.get('num_chan_mirrors', 0) # Fused silica (common lens material) and fused quartz (common for CCD windows) # turn out to have the same transmission... self.lens_trans = kwargs.get('chan_lens_trans', 0.93) self.mirror_refl = kwargs.get('chan_mirror_refl', 0.9925) # Transmission/Reflection values of optical elements coating self.ar_coating = kwargs.get('chan_ar_coating_refl', 0.99) # Read common components first trans_components = kwargs.get('trans_components', None) wavelengths = np.arange(300, 1501, 1) * u.nm if trans_components: print("Computing channel transmission from components") trans = len(wavelengths) * [ 1.0, ] for comp_name in trans_components.split(","): print(comp_name) element = read_element(comp_name) trans *= element(wavelengths) else: print("Computing channel transmission from elements") transmission = self._compute_transmission() trans = len(wavelengths) * [ transmission, ] header = {} self.transmission = SpectralElement(Empirical1D, points=wavelengths, lookup_table=trans, keep_neg=True, meta={'header': header}) ccd_qe = kwargs.get('ccd_qe', 0.9) if not isinstance(ccd_qe, (u.Quantity, numbers.Number)): file_path = os.path.expandvars(ccd_qe) if not os.path.exists(file_path): file_path = str( pkg_resources.files('etc.data').joinpath(ccd_qe)) header, wavelengths, throughput = specio.read_ascii_spec( file_path, wave_unit=u.nm, flux_unit=units.THROUGHPUT) if throughput.mean() > 1.0: throughput /= 100.0 header['notes'] = 'Divided by 100.0 to convert from percentage' header['filename'] = ccd_qe self.ccd_qe = BaseUnitlessSpectrum(Empirical1D, points=wavelengths, lookup_table=throughput, keep_neg=False, meta={'header': header}) else: self.ccd_qe = ccd_qe self.ccd_gain = kwargs.get('ccd_gain', 1) * (u.electron / u.adu) self.ccd_readnoise = kwargs.get('ccd_readnoise', 0) * (u.electron / u.pix) ccd_pixsize = kwargs.get('ccd_pixsize', 0) try: ccd_pixsize_units = u.Unit( kwargs.get('ccd_pixsize_units', 'micron')) except ValueError: ccd_pixsize_units = u.micron self.ccd_pixsize = (ccd_pixsize * ccd_pixsize_units).to(u.micron) self.ccd_xpixels = kwargs.get('ccd_xpixels', 0) self.ccd_ypixels = kwargs.get('ccd_ypixels', 0) self.ccd_xbinning = kwargs.get('ccd_xbinning', 1) self.ccd_ybinning = kwargs.get('ccd_ybinning', 1)
def __init__(self, name=None, inst_type="IMAGER", **kwargs): _ins_types = ["IMAGER", "SPECTROGRAPH"] self.name = name if name is not None else "Undefined" self.inst_type = inst_type.upper() if inst_type.upper( ) in _ins_types else "IMAGER" self.fiber_diameter = kwargs.get('fiber_diameter', None) if self.fiber_diameter is not None: self.fiber_diameter = self.fiber_diameter * u.arcsec self.grating_linespermm = kwargs.get('grating_linespermm', None) if self.grating_linespermm: self.grating_spacing = 1.0 / (self.grating_linespermm * (1 / u.mm)) self.grating_blaze = kwargs.get('grating_blaze', 0.0) self.grating_gamma = kwargs.get('grating_gamma', 0.0) self.cam_focallength = kwargs.get('cam_focallength', None) if self.cam_focallength: self.cam_focallength = self.cam_focallength * u.mm self.dispersion_along_x = kwargs.get('dispersion_along_x', True) self._echelle_constant = None # Defaults assume a "standard" imager with a single lens, 2 AR coatings # on the front and back surfaces and no mirrors self.num_ar_coatings = kwargs.get('num_ar_coatings', 2) self.num_lenses = kwargs.get('num_inst_lenses', 1) self.num_mirrors = kwargs.get('num_inst_mirrors', 0) # Fused silica (common lens material) and fused quartz (common for CCD windows) # turn out to have the same transmission... self.lens_trans = kwargs.get('inst_lens_trans', 0.93) self.mirror_refl = kwargs.get('inst_mirror_refl', 0.9925) # Transmission/Reflection values of optical elements coating self.ar_coating = kwargs.get('inst_ar_coating_refl', 0.99) # Read common components first trans_components = kwargs.get('trans_components', None) wavelengths = np.arange(300, 1501, 1) * u.nm if trans_components: trans = len(wavelengths) * [ 1.0, ] for comp_name in trans_components.split(","): print(comp_name) element = read_element(comp_name.strip()) trans *= element(wavelengths) else: print("Computing transmission from elements") transmission = self._compute_transmission() trans = len(wavelengths) * [ transmission, ] header = {} self.transmission = SpectralElement(Empirical1D, points=wavelengths, lookup_table=trans,\ keep_neg=True, meta={'header': header}) fwhm = kwargs.get('fwhm', 1) try: fwhm_units = u.Unit(kwargs.get('fwhm_units', 'arcsec')) except ValueError: fwhm_units = u.arcsec try: # Already a Quantity self.fwhm = fwhm.to(u.arcsec) except AttributeError: self.fwhm = fwhm * fwhm_units focal_scale = kwargs.get('focal_scale', 0) try: focal_scale_units = u.Unit( kwargs.get('focal_scale_units', 'arcsec/mm')) except ValueError: focal_scale_units = u.arcsec / u.mm self.focal_scale = focal_scale * focal_scale_units channels = kwargs.get('channels', {'default': {}}) self._num_channels = max(len(channels), 1) self.channelset = OrderedDict() self.filter2channel_map = OrderedDict() self.filterlist = kwargs.get('filterlist', []) self.filterset = OrderedDict() for filtername in self.filterlist: if filtername not in self.filterset: self.filterset[filtername] = self.set_bandpass_from_filter( filtername) self.filter2channel_map[filtername] = 'default' for channel in channels: # Make copy of common params defined at the Instrument level and then # overwrite with channel/camera-specific versions camera_kwargs = kwargs.copy() # Delete any trans_components if 'trans_components' in camera_kwargs: del (camera_kwargs['trans_components']) for k, v in channels[channel].items(): camera_kwargs[k] = v self.channelset[channel] = Camera(**camera_kwargs) if self.channelset[ channel].ccd_pixsize != 0 and self.focal_scale != 0: self.channelset[channel].ccd_pixscale = self.focal_scale.to( u.arcsec / u.mm) * self.channelset[channel].ccd_pixsize.to( u.mm) cam_filterlist = camera_kwargs.get('filterlist', []) for filtername in cam_filterlist: if filtername not in self.filterset: self.filterlist.append(filtername) self.filterset[filtername] = self.set_bandpass_from_filter( filtername) self.filter2channel_map[filtername] = channel
def read_element(filtername_or_filename, element_type='element', wave_units=u.nm, flux_units=units.THROUGHPUT): """Generic reader for optical elements, filters and others. The passed <filtername_or_filename> is first looked up in the `Conf` mapping dictionary for a match; if there is no match, it is assumed to be a filename or location specifier. These can be of 3 types: 1. LCO Imaging Lab scanned optical element CSV files (contain 'LCO_' and '.csv' in the filename) 2. SVO filter service references (contain 'http://svo') 3. Local files (either ASCII or FITS) Checking on the read wavelengths is performed for local files: * if the first wavelength value is <100nm and the user didn't override the units through [wave_units], the wavelengths are assumed to be in, and are converted to, microns. * if the first wavelength value is >3000nm and the user didn't override the units through [wave_units], the wavelengths are assumed to be in, and are converted to, angstroms. """ element_type = element_type.lower() filename = conf.mapping.get(filtername_or_filename, None) if filename is None: filename = filtername_or_filename else: filename = filename() element_type = 'spectral_element' if 'LCO_' in filename.upper() and '.csv' in filename.lower(): file_path = pkg_resources.files('etc.data').joinpath( os.path.expandvars(filename)) source = "LCO iLab format" header, wavelengths, throughput = read_lco_filter_csv(file_path) elif 'http://svo' in filename.lower(): source = "SVO filter service" header, wavelengths, throughput = specio.read_remote_spec( filename, wave_unit=u.AA, flux_unit=units.THROUGHPUT) else: source = "local file" file_path = os.path.expandvars(filename) if not os.path.exists(file_path): file_path = str(pkg_resources.files('etc.data').joinpath(filename)) warnings.simplefilter('ignore', category=AstropyUserWarning) if filename.lower().endswith('fits') or filename.lower().endswith( 'fit'): try: header, wavelengths, throughput = specio.read_spec( file_path, wave_col='lam', flux_col='trans', wave_unit=u.nm, flux_unit=u.dimensionless_unscaled) except KeyError: # ESO-SM01 format; different column name for transmission and micron vs nm header, wavelengths, throughput = specio.read_spec( file_path, wave_col='lam', flux_col='flux', wave_unit=u.micron, flux_unit=u.dimensionless_unscaled) else: header, wavelengths, throughput = specio.read_ascii_spec( file_path, wave_unit=wave_units, flux_unit=flux_units) if wavelengths[0].value < 100.0 and wave_units == u.nm: # Small values seen, Convert to microns wavelengths = wavelengths.value * u.micron elif wavelengths[0].value > 3000.0 and wave_units == u.nm: # Large values seen, Convert to angstroms wavelengths = wavelengths.value * u.AA if element_type != 'spectrum' and throughput.mean() > 1.5: # Test for mean throughput is above 1 to catch case where a throughput # fudge may be in the range ~1 to a few e.g. ESO Omegacam optics fudge # which goes to 3.2 and averages out to ~1.4 throughput /= 100.0 header['notes'] = 'Divided by 100.0 to convert from percentage' header['source'] = source header['filename'] = filename if element_type == 'spectrum': # SourceSpectrum can't use the default units.THROUGHPUT so we need to # change to an assumed units.PHOTLAM (u.photon / (u.cm**2 * u.s * u.AA)) # if nothing was passed by the user if flux_units == units.THROUGHPUT: throughput = throughput.value * units.PHOTLAM element = SourceSpectrum(Empirical1D, points=wavelengths, lookup_table=throughput, keep_neg=False, meta={'header': header}) elif element_type == 'spectral_element': element = SpectralElement(Empirical1D, points=wavelengths, lookup_table=throughput, keep_neg=False, meta={'header': header}) else: element = BaseUnitlessSpectrum(Empirical1D, points=wavelengths, lookup_table=throughput, keep_neg=False, meta={'header': header}) return element
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 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 _get_bandpass(name): with resources.path(data, f'{name}_effective_area.ecsv') as p: t = QTable.read(p) return SpectralElement(Empirical1D, points=t['wavelength'], lookup_table=t['effective_area'] / constants.AREA)