def thermal_spectrum(self, thermtable=None): """Calculate thermal spectrum using :func:`ThermalObservationMode.to_spectrum`. Parameters ---------- thermtable : str or `None` Thermal component table filename. If `None`, uses ``stsynphot.config.conf.thermtable``. Returns ------- sp : `synphot.spectrum.SourceSpectrum` Thermal spectrum in PHOTLAM. Raises ------ synphot.exceptions.SynphotError Calculation failed. """ thom = ThermalObservationMode( self._obsmode, graphtable=self.gtname, comptable=self.ctname, thermtable=thermtable) try: sp = thom.to_spectrum() except IndexError as e: # pragma: no cover raise synexceptions.SynphotError(f'Broken graph table: {repr(e)}') return sp
def _get_wave_intersection(self): """Find wavelengths in Angstrom where ``stsynphot.config.conf.waveset_array`` intersects with ``stsynphot.spectrum.Vega``. .. note:: No one knows why Vega is the chosen one. """ def_wave = conf.waveset_array # Angstrom minw = min(def_wave) maxw = max(def_wave) # Refine min and max wavelengths using thermal components for component in self.components[1:]: emissivity = component.emissivity if emissivity is not None: w = emissivity.waveset.value # Angstrom minw = max(minw, w.min()) maxw = min(maxw, w.max()) w = self._merge_em_wave() result = w[(w > minw) & (w < maxw)] # Intersect with Vega if Vega is None: raise synexceptions.SynphotError('Missing Vega spectrum.') w = Vega.waveset.value # Angstrom return result[(result > w.min()) & (result < w.max())]
def get_filenames(self, compnames): """Get filenames of given component names. For multiple matches, only the first match is kept. Parameters ---------- compnames : list of str List of component names to search. Case-sensitive. Returns ------- files : list of str List of matched filenames. Raises ------ synphot.exceptions.SynphotError Unmatched component name. """ files = [] for compname in compnames: if compname not in (None, '', conf.clear_filter): index = np.where(self.compnames == compname)[0] if len(index) < 1: raise synexceptions.SynphotError( 'Cannot find {0} in {1}.'.format(compname, self.name)) files.append(self.filenames[index[0]].lstrip()) else: files.append(conf.clear_filter) return files
def thermback(self, area=None, thermtable=None): """Calculate thermal background count rate for ``self.obsmode``. Calculation uses :func:`~stsynphot.observationmode.ObservationMode.thermal_spectrum` to extract thermal component source spectrum in PHOTLAM per square arcsec. Then this spectrum is integrated and multiplied by detector pixel scale and telescope collecting area to produce a count rate in count/s/pix. This unit is non-standard but used widely by STScI Exposure Time Calculator. .. note:: Similar to IRAF SYNPHOT THERMBACK. Parameters ---------- area : float, `~astropy.units.quantity.Quantity`, or `None` Area that flux covers. If not a Quantity, assumed to be in :math:`cm^{2}`. If `None`, use `area`. thermtable : str or `None` Thermal component table filename. If `None`, uses ``stsynphot.config.conf.thermtable``. Returns ------- bg : `~astropy.units.quantity.Quantity` Thermal background count rate. Raises ------ synphot.exceptions.SynphotError Calculation failed. """ if self.obsmode.pixscale is None: raise synexceptions.SynphotError( 'Undefined pixel scale for {0}.'.format(self.obsmode)) if area is None: area = self.area area = units.validate_quantity(area, units.AREA) sp = self.obsmode.thermal_spectrum(thermtable=thermtable) bg = sp.integrate() * self.obsmode.pixscale ** 2 * area return bg.value * (u.count / u.s / u.pix)
def __init__(self, modelclass, obsmode=None, **kwargs): if obsmode is None: raise synexceptions.SynphotError('Missing OBSMODE.') super(ObservationSpectralElement, self).__init__(modelclass, **kwargs) self._obsmode = obsmode self.meta['expr'] = str(obsmode) # Check for zero bounds, if applicable try: self.bounded_by_zero() except synexceptions.SynphotError: # pragma: no cover warnings.warn( 'Zero-bound check not done due to undefined waveset.', AstropyUserWarning)
def _read_table(filename, ext, dtypes): """Generic table reader. Parameters ---------- filename : str Table filename. If suffix is not 'fits' or 'fit', assume ASCII format. ext : int Data extension. This is ignored for ASCII file. dtypes : dict Dictionary that maps column names to data types. Returns ------- data : `~astropy.io.fits.FITS_rec` or `~astropy.table.Table` Data table. Raises ------ synphot.exceptions.SynphotError Failure to parse table. """ # FITS if filename.endswith('.fits') or filename.endswith('.fit'): with fits.open(filename) as f: data = f[ext].data.copy() err_str = '' for key, val in dtypes.items(): if not np.issubdtype(data[key].dtype, val): err_str += 'Expect {0} to be {1} but get {2}.\n'.format( key, val, data[key].dtype) if err_str: raise synexceptions.SynphotError(err_str) # ASCII else: # pragma: no cover converters = dict([[k, ascii.convert_numpy(v)] for k, v in dtypes.items()]) data = ascii.read(filename, converters=converters) return data
def from_obsmode(cls, obsmode, graphtable=None, comptable=None, component_dict={}): """Create a bandpass from observation mode string. Parameters ---------- obsmode : str Observation mode. graphtable : str or `None` Graph table filename. If `None`, uses ``stsynphot.config.conf.graphtable``. comptable : str or `None` Optical component table filename. If `None`, uses ``stsynphot.config.conf.comptable``. component_dict : dict Maps component filename to corresponding `~stsynphot.observationmode.Component`. Returns ------- bp : `ObservationSpectralElement` Empirical bandpass. Raises ------ synphot.exceptions.SynphotError Observation mode yields no throughput. """ from .observationmode import ObservationMode # Avoid circular import ob = ObservationMode(obsmode, graphtable=graphtable, comptable=comptable, component_dict=component_dict) if not isinstance(ob.throughput, SpectralElement): # pragma: no cover raise synexceptions.SynphotError( '{0} has no throughput.'.format(obsmode)) return cls(ob.throughput, obsmode=ob)
def get_catalog_index(gridname): """Extract catalog index (grid parameters). It is read once and then cached until the cache is cleared explicitly using :func:`reset_cache`. Parameters ---------- gridname : str See :func:`grid_to_spec`. Returns ------- cat_index : list List of ``[t_eff, metallicity, log_g, filename]``. catdir : str Directory containing the requested catalog. """ if gridname == 'ck04models': catdir = 'crgridck04$' elif gridname == 'k93models': catdir = 'crgridk93$' elif gridname == 'phoenix': catdir = 'crgridphoenix$' else: raise synexceptions.SynphotError( f'{gridname} is not a supported catalog grid.') catdir = stio.irafconvert(catdir) filename = stio.resolve_filename(catdir, 'catalog.fits') # If not cached, read from grid catalog and cache it if filename not in _CACHE: data = stio.read_catalog(filename) # EXT 1 _CACHE[filename] = [list(map(float, index.split(','))) + [data['FILENAME'][i]] for i, index in enumerate(data['INDEX'])] return _CACHE[filename], catdir
def read_graphtable(filename, tab_ext=1): """Read graph table file. Table must contain the following named columns: #. ``COMPNAME`` - Component name, usually filter name (str) #. ``KEYWORD`` - Usually instrument name (str) #. ``INNODE`` - Input node number (int) #. ``OUTNODE``- Output node number (int) #. ``THCOMPNAME`` - Thermal component name, usually filter name (str) #. ``COMMENT`` - Comment (str) Example: +--------+-------+------+-------+----------+--------+ |COMPNAME|KEYWORD|INNODE|OUTNODE|THCOMPNAME|COMMENT | +========+=======+======+=======+==========+========+ | clear |nicmos | 1 | 30 | clear |idno=100| +--------+-------+------+-------+----------+--------+ | clear | wfc3 | 1 | 30 | clear | | +--------+-------+------+-------+----------+--------+ | clear | wfpc | 1 | 20 | clear |idno=100| +--------+-------+------+-------+----------+--------+ Parameters ---------- filename : str Graph table filename. If suffix is not 'fits' or 'fit', assume ASCII format. tab_ext : int, optional FITS extension index of the data table. This is ignored for ASCII file. Returns ------- primary_area : `~astropy.units.quantity.Quantity` or `None` Value of PRIMAREA keyword in primary header. Always `None` for ASCII file. data : `~astropy.io.fits.FITS_rec` or `~astropy.table.Table` Data table. Raises ------ synphot.exceptions.SynphotError Failure to parse graph table. """ graph_dtypes = { 'COMPNAME': np.str_, 'KEYWORD': np.str_, 'INNODE': np.int32, 'OUTNODE': np.int32, 'THCOMPNAME': np.str_, 'COMMENT': np.str_ } data = _read_table(filename, tab_ext, graph_dtypes) # Get primary area if filename.endswith('.fits') or filename.endswith('.fit'): with fits.open(filename) as f: primary_area = f[str('PRIMARY')].header.get('PRIMAREA', None) else: # pragma: no cover primary_area = None if primary_area is not None and not isinstance(primary_area, u.Quantity): primary_area = primary_area * units.AREA # Check for segmented graph table if np.any([x.lower().endswith('graph') for x in data['COMPNAME']]): # pragma: no cover raise synexceptions.SynphotError( 'Segmented graph tables not supported.') return primary_area, data
def interpolate_spectral_element(parfilename, interpval, ext=1): """Interpolate (or extrapolate) throughput spectra in given parameterized FITS table to given parameter value. FITS table is parsed with :func:`stsynphot.stio.read_interp_spec`. Parameterized values must be in ascending order in the table columns. If extrapolation is needed but not allowed, default throughput from ``THROUGHPUT`` column will be used. Parameters ---------- parfilename : str Parameterized filename contains a suffix followed by a column name specificationin between square brackets. For example, ``path/acs_fr656n_006_syn.fits[fr656n#]``. interpval : float Desired parameter value. ext : int, optional FITS extension index of the data table. Returns ------- sp : `synphot.spectrum.SpectralElement` Empirical bandpass at ``interpval``. Raises ------ synphot.exceptions.ExtrapolationNotAllowed Extrapolation is not allowed by data table. synphot.exceptions.SynphotError No columns available for interpolation or extrapolation. """ def_colname = 'THROUGHPUT' warndict = {} # Separate real filename and column name specification xre = _interpfilepatt.search(parfilename) if xre is None: raise synexceptions.SynphotError( '{0} must be in the format of "path/filename.fits' '[col#]"'.format(parfilename)) filename = parfilename[0:xre.start()] col_prefix = xre.group('col').upper() # Read data table data, wave_unit, doshift, extrapolate = stio.read_interp_spec( filename, tab_ext=ext) wave_unit = units.validate_unit(wave_unit) wave0 = data['WAVELENGTH'] # Determine the columns that bracket the desired value. # Grab all columns that begin with the parameter name (e.g. 'MJD#') # and then split off the numbers after the '#'. col_names = [] col_pars = [] for n in data.names: cn = n.upper() if cn.startswith(col_prefix): col_names.append(cn) col_pars.append(float(cn.split('#')[1])) if len(col_names) < 1: raise synexceptions.SynphotError( '{0} contains no interpolated columns for {1}.'.format( filename, col_prefix)) # Assumes ascending order of parameter values in table. min_par = col_pars[0] max_par = col_pars[-1] # Exact match. No interpolation needed. if interpval in col_pars: thru = data[col_names[col_pars.index(interpval)]] # Need interpolation. elif (interpval > min_par) and (interpval < max_par): upper_ind = np.searchsorted(col_pars, interpval) lower_ind = upper_ind - 1 thru = _interp_spec( interpval, wave0, col_pars[lower_ind], col_pars[upper_ind], data[col_names[lower_ind]], data[col_names[upper_ind]], doshift) # Need extrapolation, if allowed. elif extrapolate: # Extrapolate below lowest columns. if interpval < min_par: thru = _extrap_spec(interpval, min_par, col_pars[1], data[col_names[0]], data[col_names[1]]) # Extrapolate above highest columns. else: # interpval > max_par thru = _extrap_spec(interpval, col_pars[-2], max_par, data[col_names[-2]], data[col_names[-1]]) # Extrapolation not allowed. else: # Use default, if available. if def_colname in data.names: warnings.warn( 'Extrapolation not allowed, using default throughput for ' '{0}.'.format(parfilename), AstropyUserWarning) warndict['DefaultThroughput'] = True thru = data[def_colname] # Nothing can be done. else: raise synexceptions.ExtrapolationNotAllowed( 'No default throughput for {0}.'.format(parfilename)) meta = {'expr': '{0}#{1:g}'.format(filename, interpval), 'warnings': warndict} return SpectralElement( Empirical1D, points=wave0*wave_unit, lookup_table=thru, meta=meta)
def grid_to_spec(gridname, t_eff, metallicity, log_g): """Extract spectrum from given catalog grid parameters. Interpolate if necessary. Grid parameters are only read once and then cached. Until the cache is cleared explicitly using :func:`reset_cache`, cached values are used. Parameters ---------- gridname : {'ck04models', 'k93models', 'phoenix'} Model to use: * ``ck04models`` - Castelli & Kurucz (2004) * ``k93models`` - Kurucz (1993) * ``phoenix`` - Allard et al. (2009) t_eff : str, float or `astropy.units.quantity.Quantity` Effective temperature of model. If not Quantity, assumed to be in Kelvin. If string (from parser), convert to Quantity. metallicity : str or float Metallicity of model. If string (from parser), convert to float. log_g : str or float Log surface gravity for model. If string (from parser), convert to float. Returns ------- sp : `synphot.spectrum.SourceSpectrum` Empirical source spectrum. Raises ------ stsynphot.exceptions.ParameterOutOfBounds Grid parameter out of bounds. synphot.exceptions.SynphotError Invalid inputs. """ if gridname == 'ck04models': catdir = 'crgridck04$' elif gridname == 'k93models': catdir = 'crgridk93$' elif gridname == 'phoenix': catdir = 'crgridphoenix$' else: raise synexceptions.SynphotError( '{0} is not a supported catalog grid.'.format(gridname)) metallicity = _par_from_parser(metallicity) if isinstance(metallicity, u.Quantity): raise synexceptions.SynphotError( 'Quantity is not supported for metallicity.') log_g = _par_from_parser(log_g) if isinstance(log_g, u.Quantity): raise synexceptions.SynphotError( 'Quantity is not supported for log surface gravity.') t_eff = units.validate_quantity(_par_from_parser(t_eff), u.K).value catdir = stio.irafconvert(catdir) filename = os.path.join(catdir, 'catalog.fits') # If not cached, read from grid catalog and cache it if filename not in _CACHE: data = stio.read_catalog(filename) # Ext 1 _CACHE[filename] = [[float(x) for x in index.split(',')] + [data['FILENAME'][i]] for i, index in enumerate(data['INDEX'])] indices = _CACHE[filename] list0, list1 = _break_list(indices, 0, t_eff) list2, list3 = _break_list(list0, 1, metallicity) list4, list5 = _break_list(list1, 1, metallicity) list6, list7 = _break_list(list2, 2, log_g) list8, list9 = _break_list(list3, 2, log_g) list10, list11 = _break_list(list4, 2, log_g) list12, list13 = _break_list(list5, 2, log_g) sp1 = _get_spectrum(list6[0], catdir) sp2 = _get_spectrum(list7[0], catdir) sp3 = _get_spectrum(list8[0], catdir) sp4 = _get_spectrum(list9[0], catdir) sp5 = _get_spectrum(list10[0], catdir) sp6 = _get_spectrum(list11[0], catdir) sp7 = _get_spectrum(list12[0], catdir) sp8 = _get_spectrum(list13[0], catdir) spa1 = _interpolate_spectrum(sp1, sp2, log_g) spa2 = _interpolate_spectrum(sp3, sp4, log_g) spa3 = _interpolate_spectrum(sp5, sp6, log_g) spa4 = _interpolate_spectrum(sp7, sp8, log_g) spa5 = _interpolate_spectrum(spa1, spa2, metallicity) spa6 = _interpolate_spectrum(spa3, spa4, metallicity) spa7 = _interpolate_spectrum(spa5, spa6, t_eff) sp = spa7[0] sp.meta['expr'] = '{0}(T_eff={1:g},metallicity={2:g},log_g={3:g})'.format( gridname, t_eff, metallicity, log_g) return sp
def grid_to_spec(gridname, t_eff, metallicity, log_g): """Extract spectrum from given catalog grid parameters. Interpolate if necessary. Grid parameters are read with :func:`get_catalog_index`. Parameters ---------- gridname : {'ck04models', 'k93models', 'phoenix'} Model to use: * ``ck04models`` - Castelli & Kurucz (2004) * ``k93models`` - Kurucz (1993) * ``phoenix`` - Allard et al. (2009) t_eff : str, float or `astropy.units.quantity.Quantity` Effective temperature of model. If not Quantity, assumed to be in Kelvin. If string (from parser), convert to Quantity. metallicity : str or float Metallicity of model. If string (from parser), convert to float. log_g : str or float Log surface gravity for model. If string (from parser), convert to float. Returns ------- sp : `synphot.spectrum.SourceSpectrum` Empirical source spectrum. Raises ------ stsynphot.exceptions.ParameterOutOfBounds Grid parameter out of bounds. synphot.exceptions.SynphotError Invalid inputs. """ indices, catdir = get_catalog_index(gridname) metallicity = _par_from_parser(metallicity) if isinstance(metallicity, u.Quantity): raise synexceptions.SynphotError( 'Quantity is not supported for metallicity.') log_g = _par_from_parser(log_g) if isinstance(log_g, u.Quantity): raise synexceptions.SynphotError( 'Quantity is not supported for log surface gravity.') t_eff = units.validate_quantity(_par_from_parser(t_eff), u.K).value list0, list1 = _break_list(indices, 0, t_eff) list2, list3 = _break_list(list0, 1, metallicity) list4, list5 = _break_list(list1, 1, metallicity) list6, list7 = _break_list(list2, 2, log_g) list8, list9 = _break_list(list3, 2, log_g) list10, list11 = _break_list(list4, 2, log_g) list12, list13 = _break_list(list5, 2, log_g) sp1 = _get_spectrum(list6[0], catdir) sp2 = _get_spectrum(list7[0], catdir) sp3 = _get_spectrum(list8[0], catdir) sp4 = _get_spectrum(list9[0], catdir) sp5 = _get_spectrum(list10[0], catdir) sp6 = _get_spectrum(list11[0], catdir) sp7 = _get_spectrum(list12[0], catdir) sp8 = _get_spectrum(list13[0], catdir) spa1 = _interpolate_spectrum(sp1, sp2, log_g) spa2 = _interpolate_spectrum(sp3, sp4, log_g) spa3 = _interpolate_spectrum(sp5, sp6, log_g) spa4 = _interpolate_spectrum(sp7, sp8, log_g) spa5 = _interpolate_spectrum(spa1, spa2, metallicity) spa6 = _interpolate_spectrum(spa3, spa4, metallicity) spa7 = _interpolate_spectrum(spa5, spa6, t_eff) sp = spa7[0] sp.meta['expr'] = (f'{gridname}(T_eff={t_eff:g},' f'metallicity={metallicity:g},log_g={log_g:g})') return sp