def add_luminosity(modelbox): """ Function to add the luminosity of a model spectrum to the parameter dictionary of the box. The luminosity is by default calculated at a spectral resolution of 1000. Parameters ---------- modelbox : species.core.box.ModelBox Box with the model spectrum. Should also contain the dictionary with the model parameters, the radius in particular. Returns ------- species.core.box.ModelBox The input box with the luminosity added in the parameter dictionary. """ readmodel = read_model.ReadModel(model=modelbox.model, wavelength=None, teff=None) fullspec = readmodel.get_model(model_par=modelbox.parameters) flux = simps(fullspec.flux, fullspec.wavelength) if 'distance' in modelbox.parameters: luminosity = 4. * math.pi * (fullspec.parameters['distance'] * constants.PARSEC)**2 * flux # [W] else: luminosity = 4. * math.pi * (fullspec.parameters['radius'] * constants.R_JUP)**2 * flux # [W] modelbox.parameters['luminosity'] = luminosity / constants.L_SUN # [Lsun] return modelbox
def multi_photometry(datatype: str, spectrum: str, filters: List[str], parameters: Dict[str, float]) -> box.SynphotBox: """ Parameters ---------- datatype : str Data type ('model' or 'calibration'). spectrum : str Spectrum name (e.g., 'drift-phoenix', 'planck', 'powerlaw'). filters : list(str, ) List with the filter names. parameters : dict Dictionary with the model parameters. Returns ------- species.core.box.SynphotBox Box with synthetic photometry. """ print('Calculating synthetic photometry...', end='', flush=True) flux = {} if datatype == 'model': for item in filters: if spectrum == 'planck': readmodel = read_planck.ReadPlanck(filter_name=item) elif spectrum == 'powerlaw': synphot = photometry.SyntheticPhotometry(item) synphot.zero_point() # Set the wavel_range attribute powerl_box = read_util.powerlaw_spectrum(synphot.wavel_range, parameters) flux[item] = synphot.spectrum_to_flux(powerl_box.wavelength, powerl_box.flux)[0] else: readmodel = read_model.ReadModel(spectrum, filter_name=item) try: flux[item] = readmodel.get_flux(parameters)[0] except IndexError: flux[item] = np.nan warnings.warn(f'The wavelength range of the {item} filter does not match with ' f'the wavelength range of {spectrum}. The flux is set to NaN.') elif datatype == 'calibration': for item in filters: readcalib = read_calibration.ReadCalibration(spectrum, filter_name=item) flux[item] = readcalib.get_flux(parameters)[0] print(' [DONE]') return box.create_box('synphot', name='synphot', flux=flux)
def add_luminosity(modelbox): """ Function to add the luminosity of a model spectrum to the parameter dictionary of the box. Parameters ---------- modelbox : species.core.box.ModelBox Box with the model spectrum. Should also contain the dictionary with the model parameters, the radius in particular. Returns ------- species.core.box.ModelBox The input box with the luminosity added in the parameter dictionary. """ print('Calculating the luminosity...', end='', flush=True) if modelbox.model == 'planck': readmodel = read_planck.ReadPlanck(wavel_range=(1e-1, 1e3)) fullspec = readmodel.get_spectrum(model_param=modelbox.parameters, spec_res=1000.) else: readmodel = read_model.ReadModel(modelbox.model) fullspec = readmodel.get_model(modelbox.parameters) flux = simps(fullspec.flux, fullspec.wavelength) if 'distance' in modelbox.parameters: luminosity = 4. * np.pi * (fullspec.parameters['distance'] * constants.PARSEC)**2 * flux # (W) # Analytical solution for a single-component Planck function # luminosity = 4.*np.pi*(modelbox.parameters['radius']*constants.R_JUP)**2* \ # constants.SIGMA_SB*modelbox.parameters['teff']**4. else: luminosity = 4. * np.pi * (fullspec.parameters['radius'] * constants.R_JUP)**2 * flux # (W) modelbox.parameters['luminosity'] = luminosity / constants.L_SUN # (Lsun) print(' [DONE]') print(f'Wavelength range (um): {fullspec.wavelength[0]:.2e} - ' f'{fullspec.wavelength[-1]:.2e}') print(f'Luminosity (Lsun): {luminosity/constants.L_SUN:.2e}') return modelbox
def multi_photometry(datatype, spectrum, filters, parameters): """ Parameters ---------- datatype : str Data type ('model' or 'calibration'). spectrum : str Spectrum name (e.g., 'drift-phoenix'). filters : tuple(str, ) Filter names. parameters : dict Parameters and values for the spectrum Returns ------- species.core.box.SynphotBox Box with synthetic photometry. """ print('Calculating synthetic photometry...', end='', flush=True) flux = {} if datatype == 'model': for item in filters: if spectrum == 'planck': readmodel = read_planck.ReadPlanck(filter_name=item) else: readmodel = read_model.ReadModel(spectrum, filter_name=item) try: flux[item] = readmodel.get_flux(parameters)[0] except IndexError: flux[item] = np.nan warnings.warn(f'The wavelength range of the {item} filter does not match with ' f'the wavelength coverage of {spectrum}. The flux is set to NaN.') elif datatype == 'calibration': for item in filters: readcalib = read_calibration.ReadCalibration(spectrum, filter_name=item) flux[item] = readcalib.get_flux(parameters)[0] print(' [DONE]') return box.create_box('synphot', name='synphot', flux=flux)
def multi_photometry(datatype, spectrum, filters, parameters): """ Parameters ---------- datatype : str Data type ('model' or 'calibration'). spectrum : str Spectrum name (e.g., 'drift-phoenix'). filters : tuple(str, ) Filter IDs. parameters : dict Parameters and values for the spectrum Returns ------- species.core.box.SynphotBox Box with synthetic photometry. """ sys.stdout.write('Calculating synthetic photometry...') sys.stdout.flush() flux = {} if datatype == 'model': for item in filters: readmodel = read_model.ReadModel(spectrum, item) flux[item] = readmodel.get_photometry(parameters) elif datatype == 'calibration': for item in filters: readcalib = read_calibration.ReadCalibration(spectrum, item) flux[item] = readcalib.get_photometry(parameters) sys.stdout.write(' [DONE]\n') sys.stdout.flush() return box.create_box('synphot', name='synphot', flux=flux)
def get_residuals( datatype: str, spectrum: str, parameters: Dict[str, float], objectbox: box.ObjectBox, inc_phot: Union[bool, List[str]] = True, inc_spec: Union[bool, List[str]] = True, radtrans: Optional[read_radtrans.ReadRadtrans] = None, ) -> box.ResidualsBox: """ Function for calculating the residuals from fitting model or calibration spectra to a set of spectra and/or photometry. Parameters ---------- datatype : str Data type ('model' or 'calibration'). spectrum : str Name of the atmospheric model or calibration spectrum. parameters : dict Parameters and values for the spectrum objectbox : species.core.box.ObjectBox Box with the photometry and/or spectra of an object. A scaling and/or error inflation of the spectra should be applied with :func:`~species.util.read_util.update_spectra` beforehand. inc_phot : bool, list(str) Include photometric data in the fit. If a boolean, either all (``True``) or none (``False``) of the data are selected. If a list, a subset of filter names (as stored in the database) can be provided. inc_spec : bool, list(str) Include spectroscopic data in the fit. If a boolean, either all (``True``) or none (``False``) of the data are selected. If a list, a subset of spectrum names (as stored in the database with :func:`~species.data.database.Database.add_object`) can be provided. radtrans : read_radtrans.ReadRadtrans, None Instance of :class:`~species.read.read_radtrans.ReadRadtrans`. Only required with ``spectrum='petitradtrans'`. Make sure that the ``wavel_range`` of the ``ReadRadtrans`` instance is sufficiently broad to cover all the photometric and spectroscopic data of ``inc_phot`` and ``inc_spec``. Not used if set to ``None``. Returns ------- species.core.box.ResidualsBox Box with the residuals. """ if isinstance(inc_phot, bool) and inc_phot: inc_phot = objectbox.filters if inc_phot: model_phot = multi_photometry( datatype=datatype, spectrum=spectrum, filters=inc_phot, parameters=parameters, radtrans=radtrans, ) res_phot = {} for item in inc_phot: transmission = read_filter.ReadFilter(item) res_phot[item] = np.zeros(objectbox.flux[item].shape) if objectbox.flux[item].ndim == 1: res_phot[item][0] = transmission.mean_wavelength() res_phot[item][1] = ( objectbox.flux[item][0] - model_phot.flux[item]) / objectbox.flux[item][1] elif objectbox.flux[item].ndim == 2: for j in range(objectbox.flux[item].shape[1]): res_phot[item][0, j] = transmission.mean_wavelength() res_phot[item][1, j] = ( objectbox.flux[item][0, j] - model_phot.flux[item]) / objectbox.flux[item][1, j] else: res_phot = None if inc_spec: res_spec = {} if spectrum == "petitradtrans": # Calculate the petitRADTRANS spectrum only once model = radtrans.get_model(parameters) for key in objectbox.spectrum: if isinstance(inc_spec, bool) or key in inc_spec: wavel_range = ( 0.9 * objectbox.spectrum[key][0][0, 0], 1.1 * objectbox.spectrum[key][0][-1, 0], ) wl_new = objectbox.spectrum[key][0][:, 0] spec_res = objectbox.spectrum[key][3] if spectrum == "planck": readmodel = read_planck.ReadPlanck(wavel_range=wavel_range) model = readmodel.get_spectrum(model_param=parameters, spec_res=1000.0) # Separate resampling to the new wavelength points flux_new = spectres.spectres( wl_new, model.wavelength, model.flux, spec_errs=None, fill=0.0, verbose=True, ) elif spectrum == "petitradtrans": # Separate resampling to the new wavelength points flux_new = spectres.spectres( wl_new, model.wavelength, model.flux, spec_errs=None, fill=0.0, verbose=True, ) else: # Resampling to the new wavelength points # is done by the get_model method readmodel = read_model.ReadModel(spectrum, wavel_range=wavel_range) if "teff_0" in parameters and "teff_1" in parameters: # Binary system param_0 = read_util.binary_to_single(parameters, 0) model_spec_0 = readmodel.get_model( param_0, spec_res=spec_res, wavel_resample=wl_new, smooth=True, ) param_1 = read_util.binary_to_single(parameters, 1) model_spec_1 = readmodel.get_model( param_1, spec_res=spec_res, wavel_resample=wl_new, smooth=True, ) flux_comb = ( parameters["spec_weight"] * model_spec_0.flux + (1.0 - parameters["spec_weight"]) * model_spec_1.flux) model_spec = box.create_box( boxtype="model", model=spectrum, wavelength=wl_new, flux=flux_comb, parameters=parameters, quantity="flux", ) else: # Single object model_spec = readmodel.get_model( parameters, spec_res=spec_res, wavel_resample=wl_new, smooth=True, ) flux_new = model_spec.flux data_spec = objectbox.spectrum[key][0] res_tmp = (data_spec[:, 1] - flux_new) / data_spec[:, 2] res_spec[key] = np.column_stack([wl_new, res_tmp]) else: res_spec = None print("Calculating residuals... [DONE]") print("Residuals (sigma):") if res_phot is not None: for item in inc_phot: if res_phot[item].ndim == 1: print(f" - {item}: {res_phot[item][1]:.2f}") elif res_phot[item].ndim == 2: for j in range(res_phot[item].shape[1]): print(f" - {item}: {res_phot[item][1, j]:.2f}") if res_spec is not None: for key in objectbox.spectrum: if isinstance(inc_spec, bool) or key in inc_spec: print(f" - {key}: min: {np.nanmin(res_spec[key]):.2f}, " f"max: {np.nanmax(res_spec[key]):.2f}") chi2_stat = 0 n_dof = 0 if res_phot is not None: for key, value in res_phot.items(): chi2_stat += value[1]**2 n_dof += 1 if res_spec is not None: for key, value in res_spec.items(): chi2_stat += np.sum(value[:, 1]**2) n_dof += value.shape[0] for item in parameters: if item not in ["mass", "luminosity", "distance"]: n_dof -= 1 chi2_red = chi2_stat / n_dof print(f"Reduced chi2 = {chi2_red:.2f}") print(f"Number of degrees of freedom = {n_dof}") return box.create_box( boxtype="residuals", name=objectbox.name, photometry=res_phot, spectrum=res_spec, chi2_red=chi2_red, )
def get_residuals(datatype: str, spectrum: str, parameters: Dict[str, float], objectbox: box.ObjectBox, inc_phot: Union[bool, List[str]] = True, inc_spec: Union[bool, List[str]] = True, **kwargs_radtrans: Optional[dict]) -> box.ResidualsBox: """ Parameters ---------- datatype : str Data type ('model' or 'calibration'). spectrum : str Name of the atmospheric model or calibration spectrum. parameters : dict Parameters and values for the spectrum objectbox : species.core.box.ObjectBox Box with the photometry and/or spectra of an object. A scaling and/or error inflation of the spectra should be applied with :func:`~species.util.read_util.update_spectra` beforehand. inc_phot : bool, list(str) Include photometric data in the fit. If a boolean, either all (``True``) or none (``False``) of the data are selected. If a list, a subset of filter names (as stored in the database) can be provided. inc_spec : bool, list(str) Include spectroscopic data in the fit. If a boolean, either all (``True``) or none (``False``) of the data are selected. If a list, a subset of spectrum names (as stored in the database with :func:`~species.data.database.Database.add_object`) can be provided. Keyword arguments ----------------- kwargs_radtrans : dict Dictionary with the keyword arguments for the ``ReadRadtrans`` object, containing ``line_species``, ``cloud_species``, and ``scattering``. Returns ------- species.core.box.ResidualsBox Box with the residuals. """ if 'filters' in kwargs_radtrans: warnings.warn('The \'filters\' parameter has been deprecated. Please use the \'inc_phot\' ' 'parameter instead. The \'filters\' parameter is ignored.') if isinstance(inc_phot, bool) and inc_phot: inc_phot = objectbox.filters if inc_phot: model_phot = multi_photometry(datatype=datatype, spectrum=spectrum, filters=inc_phot, parameters=parameters) res_phot = {} for item in inc_phot: transmission = read_filter.ReadFilter(item) res_phot[item] = np.zeros(objectbox.flux[item].shape) if objectbox.flux[item].ndim == 1: res_phot[item][0] = transmission.mean_wavelength() res_phot[item][1] = (objectbox.flux[item][0]-model_phot.flux[item]) / \ objectbox.flux[item][1] elif objectbox.flux[item].ndim == 2: for j in range(objectbox.flux[item].shape[1]): res_phot[item][0, j] = transmission.mean_wavelength() res_phot[item][1, j] = (objectbox.flux[item][0, j]-model_phot.flux[item]) / \ objectbox.flux[item][1, j] else: res_phot = None if inc_spec: res_spec = {} readmodel = None for key in objectbox.spectrum: if isinstance(inc_spec, bool) or key in inc_spec: wavel_range = (0.9*objectbox.spectrum[key][0][0, 0], 1.1*objectbox.spectrum[key][0][-1, 0]) wl_new = objectbox.spectrum[key][0][:, 0] spec_res = objectbox.spectrum[key][3] if spectrum == 'planck': readmodel = read_planck.ReadPlanck(wavel_range=wavel_range) model = readmodel.get_spectrum(model_param=parameters, spec_res=1000.) flux_new = spectres.spectres(wl_new, model.wavelength, model.flux, spec_errs=None, fill=0., verbose=True) else: if spectrum == 'petitradtrans': # TODO change back pass # radtrans = read_radtrans.ReadRadtrans(line_species=kwargs_radtrans['line_species'], # cloud_species=kwargs_radtrans['cloud_species'], # scattering=kwargs_radtrans['scattering'], # wavel_range=wavel_range) # # model = radtrans.get_model(parameters, spec_res=None) # # # separate resampling to the new wavelength points # # flux_new = spectres.spectres(wl_new, # model.wavelength, # model.flux, # spec_errs=None, # fill=0., # verbose=True) else: readmodel = read_model.ReadModel(spectrum, wavel_range=wavel_range) # resampling to the new wavelength points is done in teh get_model function model_spec = readmodel.get_model(parameters, spec_res=spec_res, wavel_resample=wl_new, smooth=True) flux_new = model_spec.flux data_spec = objectbox.spectrum[key][0] res_tmp = (data_spec[:, 1]-flux_new) / data_spec[:, 2] res_spec[key] = np.column_stack([wl_new, res_tmp]) else: res_spec = None print('Calculating residuals... [DONE]') print('Residuals (sigma):') if res_phot is not None: for item in inc_phot: if res_phot[item].ndim == 1: print(f' - {item}: {res_phot[item][1]:.2f}') elif res_phot[item].ndim == 2: for j in range(res_phot[item].shape[1]): print(f' - {item}: {res_phot[item][1, j]:.2f}') if res_spec is not None: for key in objectbox.spectrum: if isinstance(inc_spec, bool) or key in inc_spec: print(f' - {key}: min: {np.nanmin(res_spec[key]):.2f}, ' f'max: {np.nanmax(res_spec[key]):.2f}') return box.create_box(boxtype='residuals', name=objectbox.name, photometry=res_phot, spectrum=res_spec)
def get_mcmc_spectra(self, tag, burnin, random, wavelength, specres=None): """ Parameters ---------- tag : str Database tag with the MCMC samples. burnin : int Number of burnin steps. random : int Number of random samples. wavelength : tuple(float, float) or str Wavelength range (micron) or filter name. Full spectrum if set to None. specres : float Spectral resolution, achieved by smoothing with a Gaussian kernel. The original wavelength points are used if set to None. Returns ------- tuple(species.core.box.ModelBox, ) Boxes with the randomly sampled spectra. """ sys.stdout.write('Getting MCMC spectra...') sys.stdout.flush() h5_file = h5py.File(self.database, 'r') dset = h5_file['results/mcmc/' + tag] nparam = dset.attrs['nparam'] spectrum_type = dset.attrs['type'] spectrum_name = dset.attrs['spectrum'] if specres is not None and spectrum_type == 'calibration': warnings.warn( "Smoothing of the spectral resolution is not implemented for calibration " "spectra.") if dset.attrs.__contains__('distance'): distance = dset.attrs['distance'] else: distance = None samples = np.asarray(dset) samples = samples[:, burnin:, :] ran_walker = np.random.randint(samples.shape[0], size=random) ran_step = np.random.randint(samples.shape[1], size=random) samples = samples[ran_walker, ran_step, :] param = [] for i in range(nparam): param.append(str(dset.attrs['parameter' + str(i)])) if spectrum_type == 'model': readmodel = read_model.ReadModel(spectrum_name, wavelength) elif spectrum_type == 'calibration': readcalib = read_calibration.ReadCalibration(spectrum_name, None) boxes = [] progbar = progress.bar.Bar('\rGetting MCMC spectra...', max=samples.shape[0], suffix='%(percent)d%%') for i in range(samples.shape[0]): model_par = {} for j in range(samples.shape[1]): model_par[param[j]] = samples[i, j] if distance: model_par['distance'] = distance if spectrum_type == 'model': specbox = readmodel.get_model(model_par, specres) elif spectrum_type == 'calibration': specbox = readcalib.get_spectrum(model_par) box.type = 'mcmc' boxes.append(specbox) progbar.next() progbar.finish() h5_file.close() return tuple(boxes)
def get_color_color( self, age: float, masses: np.ndarray, model: str, filters_colors: Tuple[Tuple[str, str], Tuple[str, str]], ) -> box.ColorColorBox: """ Function for calculating color-magnitude combinations from a selected isochrone. Parameters ---------- age : float Age (Myr) at which the isochrone data is interpolated. masses : np.ndarray Masses (:math:`M_\\mathrm{J}`) at which the isochrone data is interpolated. model : str Atmospheric model used to compute the synthetic photometry. filters_colors : tuple(tuple(str, str), tuple(str, str)) Filter names for the colors as listed in the file with the isochrone data. The filter names should be provided in the format of the SVO Filter Profile Service. Returns ------- species.core.box.ColorColorBox Box with the color-color data. """ isochrone = self.get_isochrone(age=age, masses=masses, filters_color=None, filter_mag=None) model1 = read_model.ReadModel(model=model, filter_name=filters_colors[0][0]) model2 = read_model.ReadModel(model=model, filter_name=filters_colors[0][1]) model3 = read_model.ReadModel(model=model, filter_name=filters_colors[1][0]) model4 = read_model.ReadModel(model=model, filter_name=filters_colors[1][1]) if model1.get_parameters() == ["teff", "logg", "feh"]: if model == "sonora-bobcat": iso_feh = float(self.tag[-4:]) else: iso_feh = 0.0 elif model1.get_parameters() != ["teff", "logg"]: raise ValueError( "Creating synthetic colors and magnitudes from " "isochrones is currently only implemented for " "models with only Teff and log(g) as free parameters. " "Please contact Tomas Stolker if additional " "functionalities are required.") else: iso_feh = None mag1 = np.zeros(isochrone.masses.shape[0]) mag2 = np.zeros(isochrone.masses.shape[0]) mag3 = np.zeros(isochrone.masses.shape[0]) mag4 = np.zeros(isochrone.masses.shape[0]) radius = np.zeros(isochrone.masses.shape[0]) for i, mass_item in enumerate(isochrone.masses): model_param = { "teff": isochrone.teff[i], "logg": isochrone.logg[i], "mass": mass_item, "distance": 10.0, } if iso_feh is not None: model_param["feh"] = iso_feh radius[i] = read_util.get_radius(model_param["logg"], model_param["mass"]) # (Rjup) if np.isnan(isochrone.teff[i]): mag1[i] = np.nan mag2[i] = np.nan mag3[i] = np.nan mag4[i] = np.nan warnings.warn( f"The value of Teff is NaN for the following isochrone " f"sample: {model_param}. Setting the magnitudes to NaN.") else: for item_bounds in model1.get_bounds(): if model_param[item_bounds] < model1.get_bounds( )[item_bounds][0]: mag1[i] = np.nan mag2[i] = np.nan mag3[i] = np.nan mag4[i] = np.nan warnings.warn( f"The value of {item_bounds} is " f"{model_param[item_bounds]}, which is " f"below the lower bound of the model grid " f" ({model1.get_bounds()[item_bounds][0]}). " f"Setting the magnitudes to NaN for the " f"following isochrone sample: {model_param}.") elif model_param[item_bounds] > model1.get_bounds( )[item_bounds][1]: mag1[i] = np.nan mag2[i] = np.nan mag3[i] = np.nan mag4[i] = np.nan warnings.warn( f"The value of {item_bounds} is " f"{model_param[item_bounds]}, which is above " f"the upper bound of the model grid " f"({model1.get_bounds()[item_bounds][1]}). " f"Setting the magnitudes to NaN for the " f"following isochrone sample: {model_param}.") if (not np.isnan(mag1[i]) and not np.isnan(mag2[i]) and not np.isnan(mag3[i]) and not np.isnan(mag4[i])): mag1[i], _ = model1.get_magnitude(model_param) mag2[i], _ = model2.get_magnitude(model_param) mag3[i], _ = model3.get_magnitude(model_param) mag4[i], _ = model4.get_magnitude(model_param) return box.create_box( boxtype="colorcolor", library=model, object_type="model", filters=filters_colors, color1=mag1 - mag2, color2=mag3 - mag4, names=None, sptype=masses, mass=masses, radius=radius, iso_tag=self.tag, )
def get_color_color(self, age: float, masses: np.ndarray, model: str, filters_colors: Tuple[Tuple[str, str], Tuple[str, str]]) -> box.ColorColorBox: """ Function for calculating color-magnitude combinations from a selected isochrone. Parameters ---------- age : float Age (Myr) at which the isochrone data is interpolated. masses : np.ndarray Masses (Mjup) at which the isochrone data is interpolated. model : str Atmospheric model used to compute the synthetic photometry. filters_colors : tuple(tuple(str, str), tuple(str, str)) Filter names for the colors as listed in the file with the isochrone data. The filter names should be provided in the format of the SVO Filter Profile Service. Returns ------- species.core.box.ColorColorBox Box with the color-color data. """ isochrone = self.get_isochrone(age=age, masses=masses, filters_color=None, filter_mag=None) model1 = read_model.ReadModel(model=model, filter_name=filters_colors[0][0]) model2 = read_model.ReadModel(model=model, filter_name=filters_colors[0][1]) model3 = read_model.ReadModel(model=model, filter_name=filters_colors[1][0]) model4 = read_model.ReadModel(model=model, filter_name=filters_colors[1][1]) if model1.get_parameters() != ['teff', 'logg']: raise ValueError('Creating synthetic colors and magnitudes from isochrones is ' 'currently only implemented for models with only Teff and log(g) ' 'as free parameters. Please contact Tomas Stolker if additional ' 'functionalities are required.') mag1 = np.zeros(isochrone.masses.shape[0]) mag2 = np.zeros(isochrone.masses.shape[0]) mag3 = np.zeros(isochrone.masses.shape[0]) mag4 = np.zeros(isochrone.masses.shape[0]) for i, mass_item in enumerate(isochrone.masses): model_param = {'teff': isochrone.teff[i], 'logg': isochrone.logg[i], 'mass': mass_item, 'distance': 10.} if np.isnan(isochrone.teff[i]): mag1[i] = np.nan mag2[i] = np.nan mag3[i] = np.nan mag4[i] = np.nan warnings.warn(f'The value of Teff is NaN for the following isochrone sample: ' f'{model_param}. Setting the magnitudes to NaN.') else: for item_bounds in model1.get_bounds(): if model_param[item_bounds] <= model1.get_bounds()[item_bounds][0]: mag1[i] = np.nan mag2[i] = np.nan mag3[i] = np.nan mag4[i] = np.nan warnings.warn(f'The value of {item_bounds} is {model_param[item_bounds]}, ' f'which is below the lower bound of the model grid ' f'({model1.get_bounds()[item_bounds][0]}). Setting the ' f'magnitudes to NaN for the following isochrone sample: ' f'{model_param}.') elif model_param[item_bounds] >= model1.get_bounds()[item_bounds][1]: mag1[i] = np.nan mag2[i] = np.nan mag3[i] = np.nan mag4[i] = np.nan warnings.warn(f'The value of {item_bounds} is {model_param[item_bounds]}, ' f'which is above the upper bound of the model grid ' f'({model1.get_bounds()[item_bounds][1]}). Setting the ' f'magnitudes to NaN for the following isochrone sample: ' f'{model_param}.') if not np.isnan(mag1[i]) and not np.isnan(mag2[i]) and\ not np.isnan(mag3[i]) and not np.isnan(mag4[i]): mag1[i], _ = model1.get_magnitude(model_param) mag2[i], _ = model2.get_magnitude(model_param) mag3[i], _ = model3.get_magnitude(model_param) mag4[i], _ = model4.get_magnitude(model_param) return box.create_box(boxtype='colorcolor', library=model, object_type='model', filters=filters_colors, color1=mag1-mag2, color2=mag3-mag4, sptype=masses, names=None)
def get_residuals(datatype, spectrum, parameters, filters, objectbox, inc_phot=True, inc_spec=False): """ Parameters ---------- datatype : str Data type ('model' or 'calibration'). spectrum : str Name of the atmospheric model or calibration spectrum. parameters : dict Parameters and values for the spectrum filters : tuple(str, ) Filter IDs. All available photometry of the object is used if set to None. objectbox : species.core.box.ObjectBox Box with the photometry and/or spectrum of an object. inc_phot : bool Include photometry. inc_spec : bool Include spectrum. Returns ------- species.core.box.ResidualsBox Box with the photometry and/or spectrum residuals. """ if filters is None: filters = objectbox.filter if inc_phot: model_phot = multi_photometry(datatype=datatype, spectrum=spectrum, filters=filters, parameters=parameters) res_phot = np.zeros((2, len(objectbox.flux))) for i, item in enumerate(filters): transmission = read_filter.ReadFilter(item) res_phot[0, i] = transmission.mean_wavelength() res_phot[1, i] = (objectbox.flux[item][0] - model_phot.flux[item]) / objectbox.flux[item][1] else: res_phot = None sys.stdout.write('Calculating residuals...') sys.stdout.flush() if inc_spec: wl_range = (0.9 * objectbox.spectrum[0, 0], 1.1 * objectbox.spectrum[-1, 0]) readmodel = read_model.ReadModel(spectrum, wl_range) model = readmodel.get_model(parameters) wl_new = objectbox.spectrum[:, 0] flux_new = spectres.spectres(new_spec_wavs=wl_new, old_spec_wavs=model.wavelength, spec_fluxes=model.flux, spec_errs=None) res_spec = np.zeros((2, objectbox.spectrum.shape[0])) res_spec[0, :] = wl_new res_spec[1, :] = (objectbox.spectrum[:, 1] - flux_new) / objectbox.spectrum[:, 2] else: res_spec = None sys.stdout.write(' [DONE]\n') sys.stdout.flush() return box.create_box(boxtype='residuals', name=objectbox.name, photometry=res_phot, spectrum=res_spec)
def add_luminosity(modelbox): """ Function to add the luminosity of a model spectrum to the parameter dictionary of the box. Parameters ---------- modelbox : species.core.box.ModelBox Box with the model spectrum. Should also contain the dictionary with the model parameters, the radius in particular. Returns ------- species.core.box.ModelBox The input box with the luminosity added in the parameter dictionary. """ print("Calculating the luminosity...", end="", flush=True) if modelbox.model == "planck": readmodel = read_planck.ReadPlanck(wavel_range=(1e-1, 1e3)) fullspec = readmodel.get_spectrum(model_param=modelbox.parameters, spec_res=1000.0) else: readmodel = read_model.ReadModel(modelbox.model) fullspec = readmodel.get_model(modelbox.parameters) flux = simps(fullspec.flux, fullspec.wavelength) if "parallax" in modelbox.parameters: luminosity = ( 4.0 * np.pi * (1e3 * constants.PARSEC / fullspec.parameters["parallax"])**2 * flux) # (W) elif "distance" in modelbox.parameters: luminosity = (4.0 * np.pi * (fullspec.parameters["distance"] * constants.PARSEC)**2 * flux) # (W) # Analytical solution for a single-component Planck function # luminosity = 4.*np.pi*(modelbox.parameters['radius']*constants.R_JUP)**2* \ # constants.SIGMA_SB*modelbox.parameters['teff']**4. else: luminosity = (4.0 * np.pi * (fullspec.parameters["radius"] * constants.R_JUP)**2 * flux) # (W) modelbox.parameters["luminosity"] = luminosity / constants.L_SUN # (Lsun) print(" [DONE]") print(f"Wavelength range (um): {fullspec.wavelength[0]:.2e} - " f"{fullspec.wavelength[-1]:.2e}") print(f"Luminosity (Lsun): {luminosity/constants.L_SUN:.2e}") return modelbox
def update_objectbox(objectbox: box.ObjectBox, model_param: Dict[str, float], model: Optional[str] = None) -> box.ObjectBox: """ Function for updating the spectra and/or photometric fluxes in an :class:`~species.core.box.ObjectBox`, for example by applying a flux scaling and/or error inflation. Parameters ---------- objectbox : species.core.box.ObjectBox Box with the object's data, including the spectra and/or photometric fluxes. model_param : dict Dictionary with the model parameters. Should contain the value(s) of the flux scaling and/or the error inflation. model : str, None Name of the atmospheric model. Only required for inflating the errors of spectra. Otherwise, the argument can be set to ``None``. Not required when ``model='petitradtrans'`` because the error inflation is differently implemented with :class:`~species.analysis.retrieval.AtmosphericRetrieval`. Returns ------- species.core.box.ObjectBox The input box which includes the spectra with the scaled fluxes and/or inflated errors. """ if objectbox.flux is not None: for key, value in objectbox.flux.items(): instr_name = key.split(".")[0] if f"{key}_error" in model_param: # Inflate photometric error of filter var_add = model_param[f"{key}_error"]**2 * value[0]**2 elif f"{instr_name}_error" in model_param: # Inflate photometric error of instrument var_add = model_param[f"{instr_name}_error"]**2 * value[0]**2 else: # No inflation required var_add = None if var_add is not None: message = (f"Inflating the error of {key} " + f"(W m-2 um-1): {np.sqrt(var_add):.2e}...") print(message, end="", flush=True) value[1] = np.sqrt(value[1]**2 + var_add) print(" [DONE]") objectbox.flux[key] = value if objectbox.spectrum is not None: # Check if there are any spectra for key, value in objectbox.spectrum.items(): # Get the spectrum (3 columns) spec_tmp = value[0] if f"scaling_{key}" in model_param: # Scale the flux of the spectrum scaling = model_param[f"scaling_{key}"] print(f"Scaling the flux of {key}: {scaling:.2f}...", end="", flush=True) spec_tmp[:, 1] *= model_param[f"scaling_{key}"] print(" [DONE]") if f"error_{key}" in model_param: if model is None: warnings.warn( f"The dictionary with model parameters contains the error " f"inflation for {key} but the argument of 'model' is set " f"to None. Inflation of the errors is therefore not possible." ) elif model == "petitradtrans": # Increase the errors by a constant value add_error = 10.0**model_param[f"error_{key}"] log_msg = ( f"Inflating the error of {key} (W m-2 um-1): {add_error:.2e}..." ) print(log_msg, end="", flush=True) spec_tmp[:, 2] += add_error print(" [DONE]") else: # Calculate the model spectrum wavel_range = (0.9 * spec_tmp[0, 0], 1.1 * spec_tmp[-1, 0]) readmodel = read_model.ReadModel(model, wavel_range=wavel_range) model_box = readmodel.get_model( model_param, spec_res=value[3], wavel_resample=spec_tmp[:, 0], smooth=True, ) # Scale the errors relative to the model spectrum err_scaling = model_param[f"error_{key}"] log_msg = f"Inflating the error of {key}: {err_scaling:.2e}..." print(log_msg, end="", flush=True) spec_tmp[:, 2] = np.sqrt(spec_tmp[:, 2]**2 + (err_scaling * model_box.flux)**2) print(" [DONE]") # Store the spectra with the scaled fluxes and/or errors # The other three elements (i.e. the covariance matrix, # the inverted covariance matrix, and the spectral # resolution) remain unaffected objectbox.spectrum[key] = (spec_tmp, value[1], value[2], value[3]) return objectbox
def get_mcmc_photometry(self, tag, burnin, filter_id): """ Parameters ---------- tag : str Database tag with the MCMC samples. burnin : int Number of burnin steps. filter_id : str Filter ID for which the photometry is calculated. Returns ------- numpy.ndarray Synthetic photometry (mag). """ h5_file = h5py.File(self.database, 'r') dset = h5_file['results/mcmc/' + tag] nparam = dset.attrs['nparam'] spectrum_type = dset.attrs['type'] spectrum_name = dset.attrs['spectrum'] if dset.attrs.__contains__('distance'): distance = dset.attrs['distance'] else: distance = None samples = np.asarray(dset) samples = samples[:, burnin:, :] samples = samples.reshape( (samples.shape[0] * samples.shape[1], nparam)) param = [] for i in range(nparam): param.append(str(dset.attrs['parameter' + str(i)])) h5_file.close() if spectrum_type == 'model': readmodel = read_model.ReadModel(spectrum_name, filter_id) # elif spectrum_type == 'calibration': # readcalib = read_calibration.ReadCalibration(spectrum_name, None) mcmc_phot = np.zeros((samples.shape[0], 1)) progbar = progress.bar.Bar('Getting MCMC photometry...', max=samples.shape[0], suffix='%(percent)d%%') for i in range(samples.shape[0]): model_par = {} for j in range(nparam): model_par[param[j]] = samples[i, j] if distance: model_par['distance'] = distance if spectrum_type == 'model': mcmc_phot[i, 0], _ = readmodel.get_magnitude(model_par) # elif spectrum_type == 'calibration': # specbox = readcalib.get_spectrum(model_par) progbar.next() progbar.finish() return mcmc_phot
def multi_photometry( datatype: str, spectrum: str, filters: List[str], parameters: Dict[str, float], radtrans: Optional[read_radtrans.ReadRadtrans] = None, ) -> box.SynphotBox: """ Parameters ---------- datatype : str Data type ('model' or 'calibration'). spectrum : str Spectrum name (e.g., 'drift-phoenix', 'planck', 'powerlaw', 'petitradtrans'). filters : list(str) List with the filter names. parameters : dict Dictionary with the model parameters. radtrans : read_radtrans.ReadRadtrans, None Instance of :class:`~species.read.read_radtrans.ReadRadtrans`. Only required with ``spectrum='petitradtrans'`. Make sure that the ``wavel_range`` of the ``ReadRadtrans`` instance is sufficiently broad to cover all the ``filters``. Not used if set to `None`. Returns ------- species.core.box.SynphotBox Box with synthetic photometry. """ print("Calculating synthetic photometry...", end="", flush=True) flux = {} if datatype == "model": if spectrum == "petitradtrans": # Calculate the petitRADTRANS spectrum only once radtrans_box = radtrans.get_model(parameters) for item in filters: if spectrum == "petitradtrans": # Use an instance of SyntheticPhotometry instead # of get_flux from ReadRadtrans in order to not # recalculate the spectrum syn_phot = photometry.SyntheticPhotometry(item) flux[item], _ = syn_phot.spectrum_to_flux( radtrans_box.wavelength, radtrans_box.flux) elif spectrum == "powerlaw": synphot = photometry.SyntheticPhotometry(item) # Set the wavel_range attribute synphot.zero_point() powerl_box = read_util.powerlaw_spectrum( synphot.wavel_range, parameters) flux[item] = synphot.spectrum_to_flux(powerl_box.wavelength, powerl_box.flux)[0] else: if spectrum == "planck": readmodel = read_planck.ReadPlanck(filter_name=item) else: readmodel = read_model.ReadModel(spectrum, filter_name=item) try: if "teff_0" in parameters and "teff_1" in parameters: # Binary system param_0 = read_util.binary_to_single(parameters, 0) model_flux_0 = readmodel.get_flux(param_0)[0] param_1 = read_util.binary_to_single(parameters, 1) model_flux_1 = readmodel.get_flux(param_1)[0] flux[item] = ( parameters["spec_weight"] * model_flux_0 + (1.0 - parameters["spec_weight"]) * model_flux_1) else: # Single object flux[item] = readmodel.get_flux(parameters)[0] except IndexError: flux[item] = np.nan warnings.warn( f"The wavelength range of the {item} filter does not " f"match with the wavelength range of {spectrum}. The " f"flux is set to NaN.") elif datatype == "calibration": for item in filters: readcalib = read_calibration.ReadCalibration(spectrum, filter_name=item) flux[item] = readcalib.get_flux(parameters)[0] print(" [DONE]") return box.create_box("synphot", name="synphot", flux=flux)
def get_color_magnitude(self, age, mass, model, filters_color, filter_mag): """ Parameters ---------- age : str Age (Myr) that is used to interpolate the isochrone data. mass : numpy.ndarray Masses (Mjup) for which the isochrone data is interpolated. model : str Atmospheric model used to compute the synthetic photometry. filters_color : tuple(str, str), None Filter IDs for the color as listed in the file with the isochrone data. Not selected if set to None or if only evolutionary tracks are available. filter_mag : str, None Filter ID for the absolute magnitude as listed in the file with the isochrone data. Not selected if set to None or if only evolutionary tracks are available. Returns ------- species.core.box.ColorMagBox Box with the isochrone data. """ isochrone = self.get_isochrone(age=age, mass=mass, filters_color=None, filter_mag=None) model1 = read_model.ReadModel(model=model, wavelength=filters_color[0]) model2 = read_model.ReadModel(model=model, wavelength=filters_color[1]) mag1 = np.zeros(isochrone.mass.shape[0]) mag2 = np.zeros(isochrone.mass.shape[0]) for i, item in enumerate(isochrone.mass): model_par = { 'teff': isochrone.teff[i], 'logg': isochrone.logg[i], 'feh': 0., 'mass': item, 'distance': 10. } mag1[i], _ = model1.get_magnitude(model_par=model_par) mag2[i], _ = model2.get_magnitude(model_par=model_par) if filter_mag == filters_color[0]: abs_mag = mag1 elif filter_mag == filters_color[1]: abs_mag = mag2 else: raise ValueError( 'The filter_mag argument should be equal to one of the two filter ' 'values of filters_color.') return box.create_box(boxtype='colormag', library=model, object_type='temperature', filters_color=filters_color, filter_mag=filter_mag, color=mag1 - mag2, magnitude=abs_mag, sptype=isochrone.teff)
def __init__(self, object_name: str, model: str, bounds: Dict[str, Union[Tuple[float, float], Tuple[Optional[Tuple[float, float]], Optional[Tuple[float, float]]], List[Tuple[float, float]]]], inc_phot: Union[bool, List[str]] = True, inc_spec: Union[bool, List[str]] = True, fit_corr: Optional[List[str]] = None) -> None: """ The grid of spectra is linearly interpolated for each photometric point and spectrum while taking into account the filter profile, spectral resolution, and wavelength sampling. Therefore, when fitting spectra from a model grid, the computation time of the interpolation will depend on the wavelength range, spectral resolution, and parameter space of the spectra that are stored in the database. Parameters ---------- object_name : str Object name as stored in the database with :func:`~species.data.database.Database.add_object` or :func:`~species.data.database.Database.add_companion`. model : str Atmospheric model (e.g. 'bt-settl', 'exo-rem', or 'planck'). bounds : dict(str, tuple(float, float)), None The boundaries that are used for the uniform priors. Atmospheric model parameters (e.g. ``model='bt-settl'``): - Boundaries are provided as tuple of two floats. For example, ``bounds={'teff': (1000, 1500.), 'logg': (3.5, 5.), 'radius': (0.8, 1.2)}``. - The grid boundaries are used if set to ``None``. For example, ``bounds={'teff': None, 'logg': None}``. The radius range is set to 0.8-1.5 Rjup if the boundary is set to None. Blackbody emission parameters (``model='planck'``): - Parameter boundaries have to be provided for 'teff' and 'radius'. - For a single blackbody component, the values are provided as a tuple with two floats. For example, ``bounds={'teff': (1000., 2000.), 'radius': (0.8, 1.2)}``. - For multiple blackbody component, the values are provided as a list with tuples. For example, ``bounds={'teff': [(1000., 1400.), (1200., 1600.)], 'radius': [(0.8, 1.5), (1.2, 2.)]}``. - When fitting multiple blackbody components, a prior is used which restricts the temperatures and radii to decreasing and increasing values, respectively, in the order as provided in ``bounds``. Calibration parameters: - For each spectrum/instrument, two optional parameters can be fitted to account for biases in the calibration: a scaling of the flux and a constant inflation of the uncertainties. - For example, ``bounds={'SPHERE': ((0.8, 1.2), (-18., -14.))}`` if the scaling is fitted between 0.8 and 1.2, and the error is inflated with a value between 1e-18 and 1e-14 W m-2 um-1. - The dictionary key should be equal to the database tag of the spectrum. For example, ``{'SPHERE': ((0.8, 1.2), (-18., -14.))}`` if the spectrum is stored as ``'SPHERE'`` with :func:`~species.data.database.Database.add_object`. - Each of the two calibration parameters can be set to ``None`` in which case the parameter is not used. For example, ``bounds={'SPHERE': ((0.8, 1.2), None)}``. - No calibration parameters are fitted if the spectrum name is not included in ``bounds``. ISM extinction parameters: - There are three approaches for fitting extinction. The first is with the empirical relation from Cardelli et al. (1989) for ISM extinction. - The extinction is parametrized by the V band extinction, A_V (``ism_ext``), and the reddening, R_V (``ism_red``). - The prior boundaries of ``ism_ext`` and ``ext_red`` should be provided in the ``bounds`` dictionary, for example ``bounds={'ism_ext': (0., 10.), 'ism_red': (0., 20.)}``. - Only supported by ``run_multinest``. Log-normal size distribution: - The second approach is fitting the extinction of a log-normal size distribution of grains with a crystalline MgSiO3 composition, and a homogeneous, spherical structure. - The size distribution is parameterized with a mean geometric radius (``lognorm_radius`` in um) and a geometric standard deviation (``lognorm_sigma``, dimensionless). - The extinction (``lognorm_ext``) is fitted in the V band (A_V in mag) and the wavelength-dependent extinction cross sections are interpolated from a pre-tabulated grid. - The prior boundaries of ``lognorm_radius``, ``lognorm_sigma``, and ``lognorm_ext`` should be provided in the ``bounds`` dictionary, for example ``bounds={'lognorm_radius': (0.01, 10.), 'lognorm_sigma': (1.2, 10.), 'lognorm_ext': (0., 5.)}``. - A uniform prior is used for ``lognorm_sigma`` and ``lognorm_ext``, and a log-uniform prior for ``lognorm_radius``. - Only supported by ``run_multinest``. Power-law size distribution: - The third approach is fitting the extinction of a power-law size distribution of grains, again with a crystalline MgSiO3 composition, and a homogeneous, spherical structure. - The size distribution is parameterized with a maximum radius (``powerlaw_max`` in um) and a power-law exponent (``powerlaw_exp``, dimensionless). The minimum radius is fixed to 1 nm. - The extinction (``powerlaw_ext``) is fitted in the V band (A_V in mag) and the wavelength-dependent extinction cross sections are interpolated from a pre-tabulated grid. - The prior boundaries of ``powerlaw_max``, ``powerlaw_exp``, and ``powerlaw_ext`` should be provided in the ``bounds`` dictionary, for example ``'powerlaw_max': (0.5, 10.), 'powerlaw_exp': (-5., 5.), 'powerlaw_ext': (0., 5.)}``. - A uniform prior is used for ``powerlaw_exp`` and ``powerlaw_ext``, and a log-uniform prior for ``powerlaw_max``. - Only supported by ``run_multinest``. inc_phot : bool, list(str) Include photometric data in the fit. If a boolean, either all (``True``) or none (``False``) of the data are selected. If a list, a subset of filter names (as stored in the database) can be provided. inc_spec : bool, list(str) Include spectroscopic data in the fit. If a boolean, either all (``True``) or none (``False``) of the data are selected. If a list, a subset of spectrum names (as stored in the database with :func:`~species.data.database.Database.add_object`) can be provided. fit_corr : list(str), None List with spectrum names for which the correlation length and fractional amplitude are fitted (see Wang et al. 2020). Returns ------- NoneType None """ if not inc_phot and not inc_spec: raise ValueError( 'No photometric or spectroscopic data has been selected.') if model == 'planck' and 'teff' not in bounds or 'radius' not in bounds: raise ValueError( 'The \'bounds\' dictionary should contain \'teff\' and \'radius\'.' ) self.object = read_object.ReadObject(object_name) self.distance = self.object.get_distance() if fit_corr is None: self.fit_corr = [] else: self.fit_corr = fit_corr self.model = model self.bounds = bounds if self.model == 'planck': # Fitting blackbody radiation if isinstance(bounds['teff'], list) and isinstance( bounds['radius'], list): # Update temperature and radius parameters in case of multiple blackbody components self.n_planck = len(bounds['teff']) self.modelpar = [] self.bounds = {} for i, item in enumerate(bounds['teff']): self.modelpar.append(f'teff_{i}') self.modelpar.append(f'radius_{i}') self.bounds[f'teff_{i}'] = bounds['teff'][i] self.bounds[f'radius_{i}'] = bounds['radius'][i] else: # Fitting a single blackbody compoentn self.n_planck = 1 self.modelpar = ['teff', 'radius'] self.bounds = bounds else: # Fitting self-consistent atmospheric models if self.bounds is not None: readmodel = read_model.ReadModel(self.model) bounds_grid = readmodel.get_bounds() for item in bounds_grid: if item not in self.bounds: # Set the parameter boundaries to the grid boundaries if set to None self.bounds[item] = bounds_grid[item] else: # Set all parameter boundaries to the grid boundaries readmodel = read_model.ReadModel(self.model, None, None) self.bounds = readmodel.get_bounds() if 'radius' not in self.bounds: self.bounds['radius'] = (0.8, 1.5) self.n_planck = 0 self.modelpar = readmodel.get_parameters() self.modelpar.append('radius') # Select filters and spectra if isinstance(inc_phot, bool): if inc_phot: # Select all filters if True species_db = database.Database() objectbox = species_db.get_object(object_name) inc_phot = objectbox.filters else: inc_phot = [] if isinstance(inc_spec, bool): if inc_spec: # Select all filters if True species_db = database.Database() objectbox = species_db.get_object(object_name) inc_spec = list(objectbox.spectrum.keys()) else: inc_spec = [] # Include photometric data self.objphot = [] self.modelphot = [] for item in inc_phot: if self.model == 'planck': # Create SyntheticPhotometry objects when fitting a Planck function print(f'Creating synthetic photometry: {item}...', end='', flush=True) self.modelphot.append(photometry.SyntheticPhotometry(item)) else: # Or interpolate the model grid for each filter print(f'Interpolating {item}...', end='', flush=True) readmodel = read_model.ReadModel(self.model, filter_name=item) readmodel.interpolate_grid(wavel_resample=None, smooth=False, spec_res=None) self.modelphot.append(readmodel) print(' [DONE]') # Store the flux and uncertainty for each filter obj_phot = self.object.get_photometry(item) self.objphot.append(np.array([obj_phot[2], obj_phot[3]])) # Include spectroscopic data if inc_spec: # Select all spectra self.spectrum = self.object.get_spectrum() # Select the spectrum names that are not in inc_spec spec_remove = [] for item in self.spectrum: if item not in inc_spec: spec_remove.append(item) # Remove the spectra that are not included in inc_spec for item in spec_remove: del self.spectrum[item] self.n_corr_par = 0 for item in self.spectrum: if item in self.fit_corr: self.modelpar.append(f'corr_len_{item}') self.modelpar.append(f'corr_amp_{item}') self.bounds[f'corr_len_{item}'] = ( -3., 0.) # log10(corr_len) (um) self.bounds[f'corr_amp_{item}'] = (0., 1.) self.n_corr_par += 2 self.modelspec = [] if self.model != 'planck': for key, value in self.spectrum.items(): print(f'\rInterpolating {key}...', end='', flush=True) wavel_range = (0.9 * value[0][0, 0], 1.1 * value[0][-1, 0]) readmodel = read_model.ReadModel(self.model, wavel_range=wavel_range) readmodel.interpolate_grid( wavel_resample=self.spectrum[key][0][:, 0], smooth=True, spec_res=self.spectrum[key][3]) self.modelspec.append(readmodel) print(' [DONE]') else: self.spectrum = {} self.modelspec = None self.n_corr_par = 0 for item in self.spectrum: if item in bounds: if bounds[item][0] is not None: # Add the flux scaling parameter self.modelpar.append(f'scaling_{item}') self.bounds[f'scaling_{item}'] = (bounds[item][0][0], bounds[item][0][1]) if bounds[item][1] is not None: # Add the error offset parameters self.modelpar.append(f'error_{item}') self.bounds[f'error_{item}'] = (bounds[item][1][0], bounds[item][1][1]) if item in self.bounds: del self.bounds[item] if 'lognorm_radius' in self.bounds and 'lognorm_sigma' in self.bounds and \ 'lognorm_ext' in self.bounds: self.cross_sections, _, _ = dust_util.interp_lognorm( inc_phot, inc_spec, self.spectrum) self.modelpar.append('lognorm_radius') self.modelpar.append('lognorm_sigma') self.modelpar.append('lognorm_ext') self.bounds['lognorm_radius'] = ( np.log10(self.bounds['lognorm_radius'][0]), np.log10(self.bounds['lognorm_radius'][1])) elif 'powerlaw_max' in self.bounds and 'powerlaw_exp' in self.bounds and \ 'powerlaw_ext' in self.bounds: self.cross_sections, _, _ = dust_util.interp_powerlaw( inc_phot, inc_spec, self.spectrum) self.modelpar.append('powerlaw_max') self.modelpar.append('powerlaw_exp') self.modelpar.append('powerlaw_ext') self.bounds['powerlaw_max'] = (np.log10( self.bounds['powerlaw_max'][0]), np.log10( self.bounds['powerlaw_max'][1])) else: self.cross_sections = None if 'ism_ext' in self.bounds and 'ism_red' in self.bounds: self.modelpar.append('ism_ext') self.modelpar.append('ism_red') print(f'Fitting {len(self.modelpar)} parameters:') for item in self.modelpar: print(f' - {item}') print('Prior boundaries:') for key, value in self.bounds.items(): print(f' - {key} = {value}')
def get_color_magnitude( self, age: float, masses: np.ndarray, model: str, filters_color: Tuple[str, str], filter_mag: str, adapt_logg: bool = False, ) -> box.ColorMagBox: """ Function for calculating color-magnitude combinations from a selected isochrone. Parameters ---------- age : float Age (Myr) at which the isochrone data is interpolated. masses : np.ndarray Masses (:math:`M_\\mathrm{J}`) at which the isochrone data is interpolated. model : str Atmospheric model used to compute the synthetic photometry. filters_color : tuple(str, str) Filter names for the color as listed in the file with the isochrone data. The filter names should be provided in the format of the SVO Filter Profile Service. filter_mag : str Filter name for the absolute magnitude as listed in the file with the isochrone data. The value should be equal to one of the ``filters_color`` values. adapt_logg : bool Adapt :math:`\\log(g)` to the upper or lower boundary of the atmospheric model grid whenever the :math:`\\log(g)` that has been calculated from the isochrone mass and radius lies outside the available range of the synthetic spectra. Typically :math:`\\log(g)` has only a minor impact on the broadband magnitudes and colors. Returns ------- species.core.box.ColorMagBox Box with the color-magnitude data. """ isochrone = self.get_isochrone(age=age, masses=masses, filters_color=None, filter_mag=None) model1 = read_model.ReadModel(model=model, filter_name=filters_color[0]) model2 = read_model.ReadModel(model=model, filter_name=filters_color[1]) param_bounds = model1.get_bounds() if model1.get_parameters() == ["teff", "logg", "feh"]: if model == "sonora-bobcat": iso_feh = float(self.tag[-4:]) else: iso_feh = 0.0 elif model1.get_parameters() != ["teff", "logg"]: raise ValueError( "Creating synthetic colors and magnitudes from " "isochrones is currently only implemented for " "models with only Teff and log(g) as free parameters. " "Please contact Tomas Stolker if additional " "functionalities are required.") else: iso_feh = None mag1 = np.zeros(isochrone.masses.shape[0]) mag2 = np.zeros(isochrone.masses.shape[0]) radius = np.zeros(isochrone.masses.shape[0]) for i, mass_item in enumerate(isochrone.masses): model_param = { "teff": isochrone.teff[i], "logg": isochrone.logg[i], "mass": mass_item, "distance": 10.0, } if iso_feh is not None: model_param["feh"] = iso_feh radius[i] = read_util.get_radius(model_param["logg"], model_param["mass"]) # (Rjup) if np.isnan(isochrone.teff[i]): mag1[i] = np.nan mag2[i] = np.nan warnings.warn(f"The value of Teff is NaN for the following " f"isochrone sample: {model_param}. Setting " f"the magnitudes to NaN.") else: for item_bounds in param_bounds: if model_param[item_bounds] < param_bounds[item_bounds][0]: if adapt_logg and item_bounds == "logg": warnings.warn( f"The log(g) is {model_param[item_bounds]} but the " f"lower boundary of the model grid is " f"{param_bounds[item_bounds][0]}. Adapting " f"log(g) to {param_bounds[item_bounds][0]} since " f"adapt_logg=True.") model_param["logg"] = param_bounds["logg"][0] else: mag1[i] = np.nan mag2[i] = np.nan warnings.warn( f"The value of {item_bounds} is " f"{model_param[item_bounds]}, which is below " f"the lower bound of the model grid " f"({param_bounds[item_bounds][0]}). Setting the " f"magnitudes to NaN for the following isochrone " f"sample: {model_param}.") elif model_param[item_bounds] > param_bounds[item_bounds][ 1]: if adapt_logg and item_bounds == "logg": warnings.warn( f"The log(g) is {model_param[item_bounds]} but " f"the upper boundary of the model grid is " f"{param_bounds[item_bounds][1]}. Adapting " f"log(g) to {param_bounds[item_bounds][1]} " f"since adapt_logg=True.") model_param["logg"] = param_bounds["logg"][1] else: mag1[i] = np.nan mag2[i] = np.nan warnings.warn( f"The value of {item_bounds} is " f"{model_param[item_bounds]}, which is above " f"the upper bound of the model grid " f"({param_bounds[item_bounds][1]}). Setting the " f"magnitudes to NaN for the following isochrone " f"sample: {model_param}.") if not np.isnan(mag1[i]): mag1[i], _ = model1.get_magnitude(model_param) mag2[i], _ = model2.get_magnitude(model_param) if filter_mag == filters_color[0]: abs_mag = mag1 elif filter_mag == filters_color[1]: abs_mag = mag2 else: raise ValueError("The argument of filter_mag should be equal to " "one of the two filter values of filters_color.") return box.create_box( boxtype="colormag", library=model, object_type="model", filters_color=filters_color, filter_mag=filter_mag, color=mag1 - mag2, magnitude=abs_mag, names=None, sptype=masses, mass=masses, radius=radius, iso_tag=self.tag, )
def __init__(self, objname, filters, model, bounds, inc_phot=True, inc_spec=True): """ Parameters ---------- objname : str Object name in the database. filters : tuple(str, ) Filter IDs for which the photometry is selected. All available photometry of the object is selected if set to None. model : str Atmospheric model. bounds : dict Parameter boundaries. Full parameter range is used if set to None or not specified. The radius parameter range is set to 0-5 Rjup if not specified. inc_phot : bool Include photometry data with the fit. inc_spec : bool Include spectral data with the fit. Returns ------- NoneType None """ self.object = read_object.ReadObject(objname) self.distance = self.object.get_distance() self.model = model self.bounds = bounds if not inc_phot and not inc_spec: raise ValueError('No photometric or spectral data has been selected.') if self.bounds is not None and 'teff' in self.bounds: teff_bound = self.bounds['teff'] else: teff_bound = None if self.bounds is not None: readmodel = read_model.ReadModel(self.model, None, teff_bound) bounds_grid = readmodel.get_bounds() for item in bounds_grid: if item not in self.bounds: self.bounds[item] = bounds_grid[item] else: readmodel = read_model.ReadModel(self.model, None, None) self.bounds = readmodel.get_bounds() if 'radius' not in self.bounds: self.bounds['radius'] = (0., 5.) if inc_phot: self.objphot = [] self.modelphot = [] self.synphot = [] if not filters: species_db = database.Database() objectbox = species_db.get_object(objname, None) filters = objectbox.filter for item in filters: readmodel = read_model.ReadModel(self.model, item, teff_bound) readmodel.interpolate() self.modelphot.append(readmodel) sphot = photometry.SyntheticPhotometry(item) self.synphot.append(sphot) obj_phot = self.object.get_photometry(item) self.objphot.append((obj_phot[2], obj_phot[3])) else: self.objphot = None self.modelphot = None self.synphot = None if inc_spec: self.spectrum = self.object.get_spectrum() self.instrument = self.object.get_instrument() self.modelspec = read_model.ReadModel(self.model, (0.9, 2.5), teff_bound) else: self.spectrum = None self.instrument = None self.modelspec = None self.modelpar = readmodel.get_parameters() self.modelpar.append('radius')
def compare_model( self, tag: str, model: str, av_points: Optional[Union[List[float], np.array]] = None, fix_logg: Optional[float] = None, scale_spec: Optional[List[str]] = None, weights: bool = True, inc_phot: Optional[List[str]] = None, ) -> None: """ Method for finding the best fitting spectrum from a grid of atmospheric model spectra by evaluating the goodness-of-fit statistic from Cushing et al. (2008). Currently, this method only supports model grids with only :math:`T_\\mathrm{eff}` and :math:`\\log(g)` as free parameters (e.g. BT-Settl). Please create an issue on Github if support for models with more than two parameters is required. Parameters ---------- tag : str Database tag where for each spectrum from the spectral library the best-fit parameters will be stored. So when testing a range of values for ``av_ext`` and ``rad_vel``, only the parameters that minimize the goodness-of-fit statistic will be stored. model : str Name of the atmospheric model grid with synthetic spectra. av_points : list(float), np.array, None List of :math:`A_V` extinction values for which the goodness-of-fit statistic will be tested. The extinction is calculated with the relation from Cardelli et al. (1989). fix_logg : float, None Fix the value of :math:`\\log(g)`, for example if estimated from gravity-sensitive spectral features. Typically, :math:`\\log(g)` can not be accurately determined when comparing the spectra over a broad wavelength range. scale_spec : list(str), None List with names of observed spectra to which a flux scaling is applied to best match the spectral templates. weights : bool Apply a weighting based on the widths of the wavelengths bins. inc_phot : list(str), None Filter names of the photometry to include in the comparison. Photometry points are weighted by the FWHM of the filter profile. No photometric fluxes will be used if the argument is set to ``None``. Returns ------- NoneType None """ w_i = {} for spec_item in self.spec_name: obj_wavel = self.object.get_spectrum()[spec_item][0][:, 0] diff = (np.diff(obj_wavel)[1:] + np.diff(obj_wavel)[:-1]) / 2.0 diff = np.insert(diff, 0, diff[0]) diff = np.append(diff, diff[-1]) if weights: w_i[spec_item] = diff else: w_i[spec_item] = np.ones(obj_wavel.shape[0]) if inc_phot is None: inc_phot = [] if scale_spec is None: scale_spec = [] phot_wavel = {} for phot_item in inc_phot: read_filt = read_filter.ReadFilter(phot_item) w_i[phot_item] = read_filt.filter_fwhm() phot_wavel[phot_item] = read_filt.mean_wavelength() if av_points is None: av_points = np.array([0.0]) elif isinstance(av_points, list): av_points = np.array(av_points) readmodel = read_model.ReadModel(model) model_param = readmodel.get_parameters() grid_points = readmodel.get_points() coord_points = [] for key, value in grid_points.items(): if key == "logg" and fix_logg is not None: if fix_logg in value: coord_points.append(np.array([fix_logg])) else: raise ValueError( f"The argument of 'fix_logg' ({fix_logg}) is not found " f"in the parameter grid of the model spectra. The following " f"values of log(g) are available: {value}") else: coord_points.append(value) if av_points is not None: model_param.append("ism_ext") coord_points.append(av_points) grid_shape = [] for item in coord_points: grid_shape.append(len(item)) fit_stat = np.zeros(grid_shape) flux_scaling = np.zeros(grid_shape) if len(scale_spec) == 0: extra_scaling = None else: grid_shape.append(len(scale_spec)) extra_scaling = np.zeros(grid_shape) count = 1 if len(coord_points) == 3: n_iter = len(coord_points[0]) * len(coord_points[1]) * len( coord_points[2]) for i, item_i in enumerate(coord_points[0]): for j, item_j in enumerate(coord_points[1]): for k, item_k in enumerate(coord_points[2]): print( f"\rProcessing model spectrum {count}/{n_iter}...", end="") model_spec = {} model_phot = {} for spec_item in self.spec_name: obj_spec = self.object.get_spectrum()[spec_item][0] obj_res = self.object.get_spectrum()[spec_item][3] param_dict = { model_param[0]: item_i, model_param[1]: item_j, model_param[2]: item_k, } wavel_range = (0.9 * obj_spec[0, 0], 1.1 * obj_spec[-1, 0]) readmodel = read_model.ReadModel( model, wavel_range=wavel_range) model_box = readmodel.get_data( param_dict, spec_res=obj_res, wavel_resample=obj_spec[:, 0], ) model_spec[spec_item] = model_box.flux for phot_item in inc_phot: readmodel = read_model.ReadModel( model, filter_name=phot_item) model_phot[phot_item] = readmodel.get_flux( param_dict)[0] def g_fit(x, scaling): g_stat = 0.0 for spec_item in self.spec_name: obs_spec = self.object.get_spectrum( )[spec_item][0] if spec_item in scale_spec: spec_idx = scale_spec.index(spec_item) c_numer = (w_i[spec_item] * obs_spec[:, 1] * model_spec[spec_item] / obs_spec[:, 2]**2) c_denom = (w_i[spec_item] * model_spec[spec_item]**2 / obs_spec[:, 2]**2) extra_scaling[i, j, k, spec_idx] = np.sum( c_numer) / np.sum(c_denom) g_stat += np.sum( w_i[spec_item] * (obs_spec[:, 1] - extra_scaling[i, j, k, spec_idx] * model_spec[spec_item])**2 / obs_spec[:, 2]**2) else: g_stat += np.sum( w_i[spec_item] * (obs_spec[:, 1] - scaling * model_spec[spec_item])**2 / obs_spec[:, 2]**2) for phot_item in inc_phot: obs_phot = self.object.get_photometry( phot_item) g_stat += ( w_i[phot_item] * (obs_phot[2] - scaling * model_phot[phot_item])**2 / obs_phot[3]**2) return g_stat popt, _ = curve_fit(g_fit, xdata=[0.0], ydata=[0.0]) scaling = popt[0] flux_scaling[i, j, k] = scaling fit_stat[i, j, k] = g_fit(0.0, scaling) count += 1 print(" [DONE]") species_db = database.Database() species_db.add_comparison( tag=tag, goodness_of_fit=fit_stat, flux_scaling=flux_scaling, model_param=model_param, coord_points=coord_points, object_name=self.object_name, spec_name=self.spec_name, model=model, scale_spec=scale_spec, extra_scaling=extra_scaling, )
def get_color_magnitude(self, age: float, masses: np.ndarray, model: str, filters_color: Tuple[str, str], filter_mag: str) -> box.ColorMagBox: """ Function for calculating color-magnitude combinations from a selected isochrone. Parameters ---------- age : float Age (Myr) at which the isochrone data is interpolated. masses : np.ndarray Masses (Mjup) at which the isochrone data is interpolated. model : str Atmospheric model used to compute the synthetic photometry. filters_color : tuple(str, str) Filter names for the color as listed in the file with the isochrone data. The filter names should be provided in the format of the SVO Filter Profile Service. filter_mag : str Filter name for the absolute magnitude as listed in the file with the isochrone data. The value should be equal to one of the ``filters_color`` values. Returns ------- species.core.box.ColorMagBox Box with the color-magnitude data. """ isochrone = self.get_isochrone(age=age, masses=masses, filters_color=None, filter_mag=None) model1 = read_model.ReadModel(model=model, filter_name=filters_color[0]) model2 = read_model.ReadModel(model=model, filter_name=filters_color[1]) if model1.get_parameters() != ['teff', 'logg']: raise ValueError('Creating synthetic colors and magnitudes from isochrones is ' 'currently only implemented for models with only Teff and log(g) ' 'as free parameters. Please contact Tomas Stolker if additional ' 'functionalities are required.') mag1 = np.zeros(isochrone.masses.shape[0]) mag2 = np.zeros(isochrone.masses.shape[0]) for i, mass_item in enumerate(isochrone.masses): model_param = {'teff': isochrone.teff[i], 'logg': isochrone.logg[i], 'mass': mass_item, 'distance': 10.} if np.isnan(isochrone.teff[i]): mag1[i] = np.nan mag2[i] = np.nan else: for item_bounds in model1.get_bounds(): if model_param[item_bounds] <= model1.get_bounds()[item_bounds][0]: mag1[i] = np.nan mag2[i] = np.nan elif model_param[item_bounds] >= model1.get_bounds()[item_bounds][1]: mag1[i] = np.nan mag2[i] = np.nan if not np.isnan(mag1[i]): mag1[i], _ = model1.get_magnitude(model_param) mag2[i], _ = model2.get_magnitude(model_param) if filter_mag == filters_color[0]: abs_mag = mag1 elif filter_mag == filters_color[1]: abs_mag = mag2 else: raise ValueError('The filter_mag argument should be equal to one of the two filter ' 'values of filters_color.') return box.create_box(boxtype='colormag', library=model, object_type='model', filters_color=filters_color, filter_mag=filter_mag, color=mag1-mag2, magnitude=abs_mag, sptype=masses, names=None)