Source code for species.util.fit_util

"""
Utility functions for fit results.
"""

import warnings

from typing import Dict, List, Optional, Union

import numpy as np
import spectres

from typeguard import typechecked

from species.core.box import ObjectBox, ResidualsBox, SynphotBox, create_box
from species.phot.syn_phot import SyntheticPhotometry
from species.read.read_calibration import ReadCalibration
from species.read.read_filter import ReadFilter
from species.read.read_model import ReadModel
from species.read.read_planck import ReadPlanck
from species.read.read_radtrans import ReadRadtrans
from species.util.core_util import print_section
from species.util.model_util import binary_to_single, powerlaw_spectrum
from species.util.retrieval_util import convolve_spectrum


[docs] @typechecked def multi_photometry( datatype: str, spectrum: str, filters: List[str], parameters: Dict[str, float], radtrans: Optional[ReadRadtrans] = None, ) -> SynphotBox: """ Function for calculating synthetic photometry for a list of filters and a specified atmosphere model and related parameters. This function can for example be used for calculating the synthetic photometry from a best-fit model spectrum. It returns a :class:`~species.core.box_types.SynphotBox` that can be provided as input to :func:`~species.plot.plot_spectrum.plot_spectrum`. Parameters ---------- datatype : str Data type ('model' or 'calibration'). spectrum : str Spectrum name (e.g., 'drift-phoenix', 'bt-settl-cifist', planck', 'powerlaw', 'petitradtrans'). filters : list(str) List with the filter names. parameters : dict Dictionary with the model parameters. 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``. The argument can be set to ``None`` for any other model than petitRADTRANS. Returns ------- species.core.box.SynphotBox Box with synthetic photometry. """ print_section("Calculate multi-photometry") print(f"Data type: {datatype}") print(f"Spectrum name: {spectrum}") print("\nParameters:") for key, value in parameters.items(): if key == "luminosity": print(f" - {key} = {value:.2e}") else: print(f" - {key} = {value:.2f}") mean_wavel = {} for filter_item in filters: read_filt = ReadFilter(filter_item) mean_wavel[filter_item] = read_filt.mean_wavelength() 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 = SyntheticPhotometry(item) flux[item], _ = syn_phot.spectrum_to_flux( radtrans_box.wavelength, radtrans_box.flux ) elif spectrum == "powerlaw": synphot = SyntheticPhotometry(item) # Set the wavel_range attribute synphot.zero_point() powerl_box = powerlaw_spectrum(synphot.wavel_range, parameters) flux[item] = synphot.spectrum_to_flux( powerl_box.wavelength, powerl_box.flux )[0] else: if spectrum == "planck": readmodel = ReadPlanck(filter_name=item) else: readmodel = ReadModel(spectrum, filter_name=item) try: if "teff_0" in parameters and "teff_1" in parameters: # Binary system param_0 = binary_to_single(parameters, 0) model_flux_0 = readmodel.get_flux(param_0)[0] param_1 = 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 = ReadCalibration(spectrum, filter_name=item) flux[item] = readcalib.get_flux(parameters)[0] app_mag = {} abs_mag = {} for key, value in flux.items(): syn_phot = SyntheticPhotometry(key) if "parallax" in parameters: app_mag[key], abs_mag[key] = syn_phot.flux_to_magnitude( flux=value, error=None, parallax=(parameters["parallax"], None) ) elif "distance" in parameters: app_mag[key], abs_mag[key] = syn_phot.flux_to_magnitude( flux=value, error=None, distance=(parameters["distance"], None) ) else: app_mag[key], abs_mag[key] = syn_phot.flux_to_magnitude( flux=value, error=None ) print("\nMagnitudes:") for key, value in app_mag.items(): if value[1] is None: print(f" - {key} = {value[0]:.2f}") else: print(f" - {key} = {value[0]:.2f} +/- {value[1]:.2f}") print("\nFluxes (W m-2 um-1):") for key, value in flux.items(): print(f" - {key} = {value:.2e}") return create_box( "synphot", name="synphot", flux=flux, wavelength=mean_wavel, app_mag=app_mag, abs_mag=abs_mag, )
[docs] @typechecked def get_residuals( datatype: str, spectrum: str, parameters: Dict[str, float], objectbox: ObjectBox, inc_phot: Union[bool, List[str]] = True, inc_spec: Union[bool, List[str]] = True, radtrans: Optional[ReadRadtrans] = None, ) -> 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_objectbox` 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 : 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. """ print_section("Calculate residuals") print(f"Data type: {datatype}") print(f"Spectrum name: {spectrum}") print(f"\nInclude photometry: {inc_phot}") print(f"Include spectra: {inc_spec}") print("\nParameters:") for key, value in parameters.items(): if key == "luminosity": print(f" - {key} = {value:.2e}") else: print(f" - {key} = {value:.2f}") if inc_phot and objectbox.filters is not None: if isinstance(inc_phot, bool) and inc_phot: inc_phot = objectbox.filters model_phot = multi_photometry( datatype=datatype, spectrum=spectrum, filters=inc_phot, parameters=parameters, radtrans=radtrans, ) res_phot = {} for item in inc_phot: transmission = 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 and objectbox.spectrum is not None: res_spec = {} if spectrum == "petitradtrans": # Calculate the petitRADTRANS spectrum only once # Smoothing and resampling not with get_model 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 = 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": # Smoothing to the instrument resolution flux_smooth = convolve_spectrum( model.wavelength, model.flux, spec_res ) # Resampling to the new wavelength points flux_new = spectres.spectres( wl_new, model.wavelength, flux_smooth, spec_errs=None, fill=0.0, verbose=True, ) else: # Resampling to the new wavelength points # is done by the get_model method readmodel = ReadModel(spectrum, wavel_range=wavel_range) if "teff_0" in parameters and "teff_1" in parameters: # Binary system param_0 = binary_to_single(parameters, 0) model_spec_0 = readmodel.get_model( param_0, spec_res=spec_res, wavel_resample=wl_new, ) param_1 = binary_to_single(parameters, 1) model_spec_1 = readmodel.get_model( param_1, spec_res=spec_res, wavel_resample=wl_new, ) flux_comb = ( parameters["spec_weight"] * model_spec_0.flux + (1.0 - parameters["spec_weight"]) * model_spec_1.flux ) model_spec = 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, ) 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("\nResiduals (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(): if value.ndim == 1: chi2_stat += value[1] ** 2 n_dof += 1 elif value.ndim == 2: for i in range(value.shape[1]): chi2_stat += value[1][i] ** 2 n_dof += 1 if res_spec is not None: for key, value in res_spec.items(): count_nan = np.sum(np.isnan(value[:, 1])) chi2_stat += np.nansum(value[:, 1] ** 2) n_dof += value.shape[0] n_dof -= count_nan for item in parameters: if item not in ["mass", "luminosity", "distance"]: n_dof -= 1 chi2_red = chi2_stat / n_dof print(f"\nReduced chi2 = {chi2_red:.2f}") print(f"Number of degrees of freedom = {n_dof}") return create_box( boxtype="residuals", name=objectbox.name, photometry=res_phot, spectrum=res_spec, chi2_red=chi2_red, )