Module deeplenstronomy.timeseries

Generate light curves from time-series spectral energy distributions

Expand source code
"""Generate light curves from time-series spectral energy distributions"""

import glob
import os
import random
import warnings
warnings.filterwarnings("ignore")

from astropy.cosmology import FlatLambdaCDM
import astropy.units as u
import numpy as np
import pandas as pd
from scipy.interpolate import interp1d
from scipy.integrate import quad

class LCGen():
    """Light Curve Generation"""    
    def __init__(self, bands=''):
        """
        Initialize the LCGen object, download data if necessary, collect 
        necessary filter and sed information.
        
        Args:
            bands (str): comma-separated string of bands, i.e. 'g,r,i,z'
        """
        # Check for filtes / seds and download if necessary
        self.__download_data()
        
        # Collect sed files
        self.ia_sed_files = glob.glob('seds/ia/*.dat')
        self.cc_sed_files = glob.glob('seds/cc/*.SED')
        self.__read_cc_weights()
        
        # Collect filter transmission curves
        self.filter_files = glob.glob('filters/*.dat')
        self.bands = bands.split(',')
        
        # Interpolate the transmission curves
        self.norm_dict = {}
        for band in self.bands:
            transmission_frequency, transmission_wavelength = self.__read_passband(band)
            setattr(self, '{0}_transmission_frequency'.format(band), transmission_frequency)
            setattr(self, '{0}_transmission_wavelength'.format(band), transmission_wavelength)
            
            # Store normalizations
            lower_bound = eval('self._{0}_obs_frame_freq_min'.format(band))
            upper_bound = eval('self._{0}_obs_frame_freq_max'.format(band))        
            frequency_arr = np.linspace(upper_bound, lower_bound, 10000)
            norm_arr = np.ones(len(frequency_arr)) * 3631.0
            
            norm_sed = pd.DataFrame(data=np.vstack((norm_arr, frequency_arr)).T,
                                    columns=['FLUX', 'FREQUENCY_REST'])
            self.norm_dict[band] = self._integrate_through_band(norm_sed, band, 0.0, frame='REST')
        
        return

    def __download_data(self):
        """
        Check for required data and download if it is missing
        """
        if not os.path.exists('seds'):
            os.mkdir('seds')
        if not os.path.exists('seds/ia'):
            os.system('svn checkout https://github.com/rmorgan10/deeplenstronomy_data/trunk/seds/ia')
            os.system('mv ia seds')
        if not os.path.exists('seds/cc'):
            os.system('svn checkout https://github.com/rmorgan10/deeplenstronomy_data/trunk/seds/cc')
            os.system('mv cc seds')
        if not os.path.exists('seds/kn'):
            os.system('svn checkout https://github.com/rmorgan10/deeplenstronomy_data/trunk/seds/kn')
            os.system('mv kn seds')
        if not os.path.exists('filters'):
            os.system('svn checkout https://github.com/rmorgan10/deeplenstronomy_data/trunk/filters')
    
    def __read_cc_weights(self):
        """
        Read Core-Collapse SNe metadata
        
        :assign cc_info_df: dataframe containing all CC template metadata
        :assign cc_weights: lists of weights for each template
        """
        df = pd.read_csv('seds/cc/SIMGEN_INCLUDE_NON1A.INPUT', comment='#', delim_whitespace=True)
        self.cc_info_df = df
        self.cc_weights = [df['WGT'].values[df['SED'].values == x.split('/')[-1].split('.')[0]][0] for x in self.cc_sed_files]
        return
        
    
    def __read_passband(self, band):
        """
        Read and interolate filter transmission curves
        
        :param band: the single-letter band identifier
        :return: transmisison_frequency: interpolated filter transmission as a function of frequency
        :return: transmisison_wavelength: interpolated filter transmission as a function of wavelength
        """
        #Target filter file associated with band
        filter_file = [x for x in self.filter_files if x.find('_' + band) != -1][0]
        
        # Read and format filter transmission info
        passband = pd.read_csv(filter_file, 
                               names=['WAVELENGTH', 'TRANSMISSION'], 
                               delim_whitespace=True, comment='#')
        setattr(self, '_{0}_obs_frame_freq_min'.format(band), 2.99792458e18 / np.max(passband['WAVELENGTH'].values))
        setattr(self, '_{0}_obs_frame_freq_max'.format(band), 2.99792458e18 / np.min(passband['WAVELENGTH'].values))
        
        # Add boundary terms to cover the whole range
        passband.loc[passband.shape[0]] = (1.e-9, 0.0)
        passband.loc[passband.shape[0]] = (4.e+9, 0.0)
        
        # Convert to frequency using speed of light in angstroms
        passband['FREQUENCY'] = 2.99792458e18 / passband['WAVELENGTH'].values
        setattr(self, '{0}_obs_frame_transmission'.format(band), passband)
        
        # Interpolate and return
        transmission_frequency = interp1d(passband['FREQUENCY'].values, passband['TRANSMISSION'].values, fill_value=0.0)
        transmission_wavelength = interp1d(passband['WAVELENGTH'].values, passband['TRANSMISSION'].values, fill_value=0.0)
        return transmission_frequency, transmission_wavelength

    
    def _read_sed(self, sed_filename):
        """
        Read a Spectral Enerrgy Distribution into a dataframe
        
        :param sed_filename: name of file describing the sed
        :return: sed: a dataframe of the sed
        """
        
        # Read and format sed info
        sed = pd.read_csv(sed_filename,
                          names=['NITE', 'WAVELENGTH_REST', 'FLUX'], 
                          delim_whitespace=True, comment='#')

        # Remove unrealistic wavelengths
        sed = sed[sed['WAVELENGTH_REST'].values > 10.0].copy().reset_index(drop=True)
        
        # Add new boundaries
        boundary_data = []
        for nite in np.unique(sed['NITE'].values):
            boundary_data.append((nite, 10.0, 0.0))
            boundary_data.append((nite, 25000.0, 0.0))
        sed = sed.append(pd.DataFrame(data=boundary_data, columns=['NITE', 'WAVELENGTH_REST', 'FLUX']))

        # Convert to frequency
        sed['FREQUENCY_REST'] = 2.99792458e18 / sed['WAVELENGTH_REST'].values

        # Normalize
        func = interp1d(sed['WAVELENGTH_REST'].values, sed['FLUX'].values)
        sed['FLUX'] = sed['FLUX'].values / quad(func, 10.0, 25000.0)[0]
        
        # Round nights to nearest int
        sed['NITE'] = sed['NITE'].values.round()
        
        return sed
    
    def _get_kcorrect(self, sed, band, redshift):
        """
        Calculate the K-Correction
        
        :param sed: the sed on the night of peak flux
        :param band: the single-letter band being used
        :param redshift: the redshift of the object
        :return: kcor: the k-correction to the absolute magnitude
        """
        return -2.5 * np.log10((1.0 + redshift) * 
                               (self._integrate_through_band(sed, band, redshift, frame='OBS') /
                                self._integrate_through_band(sed, band, redshift, frame='REST')))
    
    def _get_kcorrections(self, sed, sed_filename, redshift):
        """
        Cache the k-correction factors and return
        """
        attr_name = sed_filename.split('.')[0] + '-kcorrect_dict-' + str(redshift*100).split('.')[0]
        if hasattr(self, attr_name):
            return [getattr(self, attr_name)[b] for b in self.bands]
        else:
            peak_sed = sed[sed['NITE'].values == self._get_closest_nite(np.unique(sed['NITE'].values), 0)].copy().reset_index(drop=True)
            k_corrections = [self._get_kcorrect(peak_sed, band, redshift) for band in self.bands]
            setattr(self, attr_name, {b: k for b, k in zip(self.bands, k_corrections)})
            return k_corrections
        
        
    def _get_distance_modulus(self, redshift, cosmo):
        """
        Calculate the dimming effect of distance to the source
        
        :param redshift: the redshift of the object
        :param cosmo: an astropy.cosmology instance
        :return: dmod: the distance modulus contribution to the apparent magnitude
        """
        return 5.0 * np.log10(cosmo.luminosity_distance(redshift).value * 10 ** 6 / 10)

    
    def _integrate_through_band(self, sed, band, redshift, frame='REST'):
        """
        Calculate the flux through a given band by integrating in frequency
        
        :param sed: a dataframe containing the sed of the object
        :param band: the single-letter filter being used
        :param redshift: the redshift of the source
        :param frame: chose from ['REST', 'OBS'] to choose the rest frame or the observer frame
        :return: flux: the measured flux from the source through the filter
        """     
        frequency_arr = sed['FREQUENCY_{0}'.format(frame)].values
        delta_frequencies = np.diff(frequency_arr) * -1.0
        integrand = eval("self.{0}_transmission_frequency(frequency_arr) * sed['FLUX'].values / frequency_arr".format(band))
        average_integrands = 0.5 * np.diff(integrand) + integrand[0:-1]
        return np.sum(delta_frequencies * average_integrands)
    
    def _get_closest_nite(self, unique_nites, nite):
        """
        Return the nite in the sed closest to a desired nite
        
        :param unique_nites: a set of the nights in an sed
        :param nite: the nite you wish to find the closest neighbor for
        :return: closest_nite: the closest nite in the sed to the given nite
        """
        
        ## If nite not in the sed, (but within range) set nite to the closest nite in the sed
        ## If nite is outside range, keep the same
        if nite > unique_nites.max() or nite < unique_nites.min():
            return nite
        else:
            return unique_nites[np.argmin(np.abs(nite - unique_nites))]

    def gen_variable(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
        """
        Generate a random variable light curve

        Args:
            redshift (float): ignored 
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey    
            sed_filename (str): ignored
            cosmo (astropy.cosmology): ignored

        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object     
              - 'obj_type' contains a string for the type of object. Will always be 'Variable' here
              - 'sed' contains the filename of the sed used. Will always be 'Variable' here  
        """
        output_data_cols = ['NITE', 'BAND', 'MAG']
        output_data = []
        central_mag = random.uniform(12.0, 23.0)
        colors = {band: mag for band, mag in zip(self.bands, np.random.uniform(low=-2.0, high=2.0, size=len(self.bands)))}
        for band in self.bands:
            for nite in nite_dict[band]:
                central_mag = random.uniform(central_mag - 1.0, central_mag + 1.0)
                output_data.append([nite, band, central_mag + colors[band]])

        return {'lc': pd.DataFrame(data=output_data, columns=output_data_cols),
                'obj_type': 'Variable',
                'sed': 'Variable'}
    
    def gen_flat(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
        """
        Generate a random flat light curve.
        
        Args:
            redshift (float): ignored
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
            sed_filename (str): ignored
            cosmo (astropy.cosmology): ignored

        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object
              - 'obj_type' contains a string for the type of object. Will always be 'Flat' here
              - 'sed' contains the filename of the sed used. Will always be 'Flat' here      
        """
        output_data_cols = ['NITE', 'BAND', 'MAG']
        central_mag = random.uniform(12.0, 23.0)
        mags = {band: mag for band, mag in zip(self.bands, central_mag + np.random.uniform(low=-2.0, high=2.0, size=len(self.bands)))}
        output_data = []
        for band in self.bands:
            for nite in nite_dict[band]:
                output_data.append([nite, band, mags[band]])

        return {'lc': pd.DataFrame(data=output_data, columns=output_data_cols),
                'obj_type': 'Flat',
                'sed': 'Flat'}

    def gen_static(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
        """
        Make a static source capable of having time-series data by introducing a mag=99 source
        on each NITE of the simulation.

        Args:
            redshift (float): ignored 
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
            sed_filename (str): ignored                                                                                                                                                               
            cosmo (astropy.cosmology): ignored                                                                                                                                                                                            
        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object
              - 'obj_type' contains a string for the type of object. Will always be 'Static' here
              - 'sed' contains the filename of the sed used. Will always be 'Flat' here     
        """
        output_data_cols = ['NITE', 'BAND', 'MAG']
        central_mag = 99.0
        mags = {band: central_mag for band in self.bands}
        output_data = []
        for band in self.bands:
            for nite in nite_dict[band]:
                output_data.append([nite, band, mags[band]])

        return {'lc': pd.DataFrame(data=output_data, columns=output_data_cols),
                'obj_type': 'Static',
                'sed': 'Static'}


        
    def gen_variablenoise(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
        """ 
        Generate a variable light curve with small random noise

        Args:
            redshift (float): ignored
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
            sed_filename (str): ignored
            cosmo (astropy.cosmology): ignored

        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object
              - 'obj_type' contains a string for the type of object. Will always be 'VariableNoise' here
              - 'sed' contains the filename of the sed used. Will always be 'VariableNoise' here              
        """
        noiseless_lc_dict = self.gen_variable(redshift, nite_dict)
        noise = np.random.normal(loc=0, scale=0.25, size=noiseless_lc_dict['lc'].shape[0])
        noiseless_lc_dict['lc']['MAG'] = noiseless_lc_dict['lc']['MAG'].values + noise
        noiseless_lc_dict['obj_type'] = 'VariableNoise'
        noiseless_lc_dict['sed'] = 'VariableNoise'
        return noiseless_lc_dict

    
    def gen_flatnoise(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
        """
        Generate a flat light curve will small random noise

        Args:
            redshift (float): ignored
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
            sed_filename (str): ignored
            cosmo (astropy.cosmology): ignored

        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object
              - 'obj_type' contains a string for the type of object. Will always be 'FlatNoise' here
              - 'sed' contains the filename of the sed used. Will always be 'FlatNoise' here              
        """
        noiseless_lc_dict = self.gen_flat(redshift, nite_dict)
        noise = np.random.normal(loc=0, scale=0.25, size=noiseless_lc_dict['lc'].shape[0])
        noiseless_lc_dict['lc']['MAG'] = noiseless_lc_dict['lc']['MAG'].values + noise
        noiseless_lc_dict['obj_type'] = 'FlatNoise'
        noiseless_lc_dict['sed'] = 'FlatNoise'
        return noiseless_lc_dict
        
    def gen_user(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
        """
        Generate a light curve from a user-specidied SED

        Args:
            redshift (float): the redshift of the source
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey 
            sed (None or pandas.DataFrame, optional, default=None): a dataframe containing the sed of the SN 
            sed_filename (str): filename containing the time-series sed you want to use 
            cosmo (astropy.cosmology): an astropy.cosmology instance used for distance calculations

        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object
              - 'obj_type' contains a string for the type of object. Will always be <sed_filename> here 
              - 'sed' contains the filename of the sed used  
        """
        if sed is None:
            if sed_filename.startswith('seds/user/'):
                attr_name = sed_filename.split('.')[0]
            else:
                attr_name = 'seds/user/' + sed_filename.split('.')[0]
                sed_filename = 'seds/user/' + sed_filename
                
            if hasattr(self, attr_name):
                sed = getattr(self, attr_name)
            else:
                sed = self._read_sed(sed_filename)
                setattr(self, attr_name, sed)

        return self.gen_lc_from_sed(redshift, nite_dict, sed, sed_filename, sed_filename, cosmo=cosmo)

    def gen_kn(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
        """
        Generate a GW170817-like light curve.

        Args:
            redshift (float): the redshift of the source
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey 
            sed (None or pandas.DataFrame, optional, default=None): a dataframe containing the sed of the SN 
            sed_filename (str): filename containing the time-series sed you want to use 
            cosmo (astropy.cosmology): an astropy.cosmology instance used for distance calculations

        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object
              - 'obj_type' contains a string for the type of object. Will always be KN here 
              - 'sed' contains the filename of the sed used  
        """

        sed_filename = 'seds/kn/kn.SED'
        if sed is None:
            attr_name = sed_filename.split('.')[0]
            if hasattr(self, attr_name):
                sed = getattr(self, attr_name)
            else:
                sed = self._read_sed(sed_filename)
                setattr(self, attr_name, sed)
                
        return self.gen_lc_from_sed(redshift, nite_dict, sed, 'KN', sed_filename, cosmo=cosmo)
    
    def gen_ia(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
        """
        Generate a SN-Ia light curve.

        Args:
            redshift (float): the redshift of the source
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey 
            sed (None or pandas.DataFrame, optional, default=None): a dataframe containing the sed of the SN 
            sed_filename (str): filename containing the time-series sed you want to use 
            cosmo (astropy.cosmology): an astropy.cosmology instance used for distance calculations

        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object
              - 'obj_type' contains a string for the type of object. Will always be Ia here 
              - 'sed' contains the filename of the sed used  
        """
        
        # Read rest-frame sed if not supplied as argument
        if sed is None:
            if sed_filename is None:
                sed_filename = random.choice(self.ia_sed_files)
            
            if sed_filename.startswith('seds/ia/'):
                attr_name = sed_filename.split('.')[0]
            else:
                attr_name = 'seds/ia/' + sed_filename.split('.')[0]
                sed_filename = 'seds/ia/' + sed_filename

            if hasattr(self, attr_name):
                sed = getattr(self, attr_name)
            else:
                sed = self._read_sed(sed_filename)
                setattr(self, attr_name, sed)
                
        # Trigger the lc generation function on this sed
        return self.gen_lc_from_sed(redshift, nite_dict, sed, 'Ia', sed_filename, cosmo=cosmo)
    
    def gen_cc(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
        """
        Generate a SN-CC light curve
        
        Args:
            redshift (float): the redshift of the source
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey 
            sed (None or pandas.DataFrame, optional, default=None): a dataframe containing the sed of the SN 
            sed_filename (str): filename containing the time-series sed you want to use 
            cosmo (astropy.cosmology): an astropy.cosmology instance used for distance calculations

        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object
              - 'obj_type' contains a string for the type of object. Will be 'II', 'Ibc, etc. 
              - 'sed' contains the filename of the sed used  
        """

        # If sed not specified, choose sed based on weight map
        if sed is None:
            if sed_filename is None:
                sed_filename = random.choices(self.cc_sed_files, weights=self.cc_weights, k=1)[0]

            if sed_filename.startswith('seds/cc/'):
                attr_name = sed_filename.split('.')[0]
            else:
                attr_name = 'seds/cc/' + sed_filename.split('.')[0]
                sed_filename = 'seds/cc/' + sed_filename
                    
            if hasattr(self, attr_name):
                sed = getattr(self, attr_name)
            else:
                sed = self._read_sed(sed_filename)
                setattr(self, attr_name, sed)
        
        # Get the type of SN-CC
        obj_type = self.cc_info_df['SNTYPE'].values[self.cc_info_df['SED'].values == sed_filename.split('/')[-1].split('.')[0]][0]
        
        # Trigger the lc generation function on this sed
        return self.gen_lc_from_sed(redshift, nite_dict, sed, obj_type, sed_filename, cosmo=cosmo)
                
    def gen_lc_from_sed(self, redshift, nite_dict, sed, obj_type, sed_filename, cosmo=None):
        """
        Generate a light curve based on a time-series sed.
        
        Args:
            redshift (float): the redshift of the source
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey 
            sed (None or pandas.DataFrame, optional, default=None): a dataframe containing the sed of the SN 
            sed_filename (str): filename containing the time-series sed you want to use 
            cosmo (astropy.cosmology): an astropy.cosmology instance used for distance calculations

        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object
              - 'obj_type' contains a string for the type of object. 
              - 'sed' contains the filename of the sed used  
        """
        
        # Adjust nites
        nites = {}
        sed_nites = np.unique(sed['NITE'].values)
        for band, cad_nites in nite_dict.items():
            useable_nites = []
            for nite in cad_nites:
                if nite not in sed_nites:
                    useable_nites.append(self._get_closest_nite(sed_nites, nite))
                else:
                    useable_nites.append(nite)
            nites[band] = useable_nites
            
        # Redshift the sed frequencies and wavelengths
        sed['WAVELENGTH_OBS'] = (1.0 + redshift) * sed['WAVELENGTH_REST'].values
        sed['FREQUENCY_OBS'] = sed['FREQUENCY_REST'].values * (1.0 + redshift)
        
        # Calculate distance modulus
        if not cosmo:
            cosmo = FlatLambdaCDM(H0=69.3 * u.km / (u.Mpc * u.s), 
                                  Om0=0.286, Tcmb0=2.725 * u.K, Neff=3.04, Ob0=0.0463)
        distance_modulus = self._get_distance_modulus(redshift, cosmo=cosmo)
        
        # Calculate k-correction at peak
        k_corrections = self._get_kcorrections(sed, sed_filename, redshift)
        
        # On each nite, in each band, calculate the absolute mag
        output_data = []
        output_data_cols = ['NITE', 'BAND', 'MAG']
        
        for band, k_correction in zip(self.bands, k_corrections):
            
            for nite in nites[band]:
                nite_sed = sed[sed['NITE'].values == nite].copy().reset_index(drop=True)
                
                # Flux is zero if requested nite is noe in sed
                if len(nite_sed) == 0:
                    output_data.append([nite, band, 99.0])
                    continue
                
                # Apply factors to calculate absolute mag
                nite_sed['FLUX'] = (cosmo.luminosity_distance(redshift).value * 10 ** 6 / 10) ** 2 / (1 + redshift) * nite_sed['FLUX'].values
                nite_sed['FREQUENCY_REST'] = nite_sed['FREQUENCY_REST'].values / (1. + redshift)
            
                # Calculate the apparent magnitude
                absolute_ab_mag = self._integrate_through_band(nite_sed, band, redshift, frame='REST') / self.norm_dict[band]
                output_data.append([nite, band, -2.5 * np.log10(absolute_ab_mag) + distance_modulus + k_correction])
                
        return {'lc': pd.DataFrame(data=output_data, columns=output_data_cols).replace(np.nan, 99.0, inplace=False),
                'obj_type': obj_type,
                'sed': sed_filename}

Classes

class LCGen (bands='')

Light Curve Generation

Initialize the LCGen object, download data if necessary, collect necessary filter and sed information.

Args

bands : str
comma-separated string of bands, i.e. 'g,r,i,z'
Expand source code
class LCGen():
    """Light Curve Generation"""    
    def __init__(self, bands=''):
        """
        Initialize the LCGen object, download data if necessary, collect 
        necessary filter and sed information.
        
        Args:
            bands (str): comma-separated string of bands, i.e. 'g,r,i,z'
        """
        # Check for filtes / seds and download if necessary
        self.__download_data()
        
        # Collect sed files
        self.ia_sed_files = glob.glob('seds/ia/*.dat')
        self.cc_sed_files = glob.glob('seds/cc/*.SED')
        self.__read_cc_weights()
        
        # Collect filter transmission curves
        self.filter_files = glob.glob('filters/*.dat')
        self.bands = bands.split(',')
        
        # Interpolate the transmission curves
        self.norm_dict = {}
        for band in self.bands:
            transmission_frequency, transmission_wavelength = self.__read_passband(band)
            setattr(self, '{0}_transmission_frequency'.format(band), transmission_frequency)
            setattr(self, '{0}_transmission_wavelength'.format(band), transmission_wavelength)
            
            # Store normalizations
            lower_bound = eval('self._{0}_obs_frame_freq_min'.format(band))
            upper_bound = eval('self._{0}_obs_frame_freq_max'.format(band))        
            frequency_arr = np.linspace(upper_bound, lower_bound, 10000)
            norm_arr = np.ones(len(frequency_arr)) * 3631.0
            
            norm_sed = pd.DataFrame(data=np.vstack((norm_arr, frequency_arr)).T,
                                    columns=['FLUX', 'FREQUENCY_REST'])
            self.norm_dict[band] = self._integrate_through_band(norm_sed, band, 0.0, frame='REST')
        
        return

    def __download_data(self):
        """
        Check for required data and download if it is missing
        """
        if not os.path.exists('seds'):
            os.mkdir('seds')
        if not os.path.exists('seds/ia'):
            os.system('svn checkout https://github.com/rmorgan10/deeplenstronomy_data/trunk/seds/ia')
            os.system('mv ia seds')
        if not os.path.exists('seds/cc'):
            os.system('svn checkout https://github.com/rmorgan10/deeplenstronomy_data/trunk/seds/cc')
            os.system('mv cc seds')
        if not os.path.exists('seds/kn'):
            os.system('svn checkout https://github.com/rmorgan10/deeplenstronomy_data/trunk/seds/kn')
            os.system('mv kn seds')
        if not os.path.exists('filters'):
            os.system('svn checkout https://github.com/rmorgan10/deeplenstronomy_data/trunk/filters')
    
    def __read_cc_weights(self):
        """
        Read Core-Collapse SNe metadata
        
        :assign cc_info_df: dataframe containing all CC template metadata
        :assign cc_weights: lists of weights for each template
        """
        df = pd.read_csv('seds/cc/SIMGEN_INCLUDE_NON1A.INPUT', comment='#', delim_whitespace=True)
        self.cc_info_df = df
        self.cc_weights = [df['WGT'].values[df['SED'].values == x.split('/')[-1].split('.')[0]][0] for x in self.cc_sed_files]
        return
        
    
    def __read_passband(self, band):
        """
        Read and interolate filter transmission curves
        
        :param band: the single-letter band identifier
        :return: transmisison_frequency: interpolated filter transmission as a function of frequency
        :return: transmisison_wavelength: interpolated filter transmission as a function of wavelength
        """
        #Target filter file associated with band
        filter_file = [x for x in self.filter_files if x.find('_' + band) != -1][0]
        
        # Read and format filter transmission info
        passband = pd.read_csv(filter_file, 
                               names=['WAVELENGTH', 'TRANSMISSION'], 
                               delim_whitespace=True, comment='#')
        setattr(self, '_{0}_obs_frame_freq_min'.format(band), 2.99792458e18 / np.max(passband['WAVELENGTH'].values))
        setattr(self, '_{0}_obs_frame_freq_max'.format(band), 2.99792458e18 / np.min(passband['WAVELENGTH'].values))
        
        # Add boundary terms to cover the whole range
        passband.loc[passband.shape[0]] = (1.e-9, 0.0)
        passband.loc[passband.shape[0]] = (4.e+9, 0.0)
        
        # Convert to frequency using speed of light in angstroms
        passband['FREQUENCY'] = 2.99792458e18 / passband['WAVELENGTH'].values
        setattr(self, '{0}_obs_frame_transmission'.format(band), passband)
        
        # Interpolate and return
        transmission_frequency = interp1d(passband['FREQUENCY'].values, passband['TRANSMISSION'].values, fill_value=0.0)
        transmission_wavelength = interp1d(passband['WAVELENGTH'].values, passband['TRANSMISSION'].values, fill_value=0.0)
        return transmission_frequency, transmission_wavelength

    
    def _read_sed(self, sed_filename):
        """
        Read a Spectral Enerrgy Distribution into a dataframe
        
        :param sed_filename: name of file describing the sed
        :return: sed: a dataframe of the sed
        """
        
        # Read and format sed info
        sed = pd.read_csv(sed_filename,
                          names=['NITE', 'WAVELENGTH_REST', 'FLUX'], 
                          delim_whitespace=True, comment='#')

        # Remove unrealistic wavelengths
        sed = sed[sed['WAVELENGTH_REST'].values > 10.0].copy().reset_index(drop=True)
        
        # Add new boundaries
        boundary_data = []
        for nite in np.unique(sed['NITE'].values):
            boundary_data.append((nite, 10.0, 0.0))
            boundary_data.append((nite, 25000.0, 0.0))
        sed = sed.append(pd.DataFrame(data=boundary_data, columns=['NITE', 'WAVELENGTH_REST', 'FLUX']))

        # Convert to frequency
        sed['FREQUENCY_REST'] = 2.99792458e18 / sed['WAVELENGTH_REST'].values

        # Normalize
        func = interp1d(sed['WAVELENGTH_REST'].values, sed['FLUX'].values)
        sed['FLUX'] = sed['FLUX'].values / quad(func, 10.0, 25000.0)[0]
        
        # Round nights to nearest int
        sed['NITE'] = sed['NITE'].values.round()
        
        return sed
    
    def _get_kcorrect(self, sed, band, redshift):
        """
        Calculate the K-Correction
        
        :param sed: the sed on the night of peak flux
        :param band: the single-letter band being used
        :param redshift: the redshift of the object
        :return: kcor: the k-correction to the absolute magnitude
        """
        return -2.5 * np.log10((1.0 + redshift) * 
                               (self._integrate_through_band(sed, band, redshift, frame='OBS') /
                                self._integrate_through_band(sed, band, redshift, frame='REST')))
    
    def _get_kcorrections(self, sed, sed_filename, redshift):
        """
        Cache the k-correction factors and return
        """
        attr_name = sed_filename.split('.')[0] + '-kcorrect_dict-' + str(redshift*100).split('.')[0]
        if hasattr(self, attr_name):
            return [getattr(self, attr_name)[b] for b in self.bands]
        else:
            peak_sed = sed[sed['NITE'].values == self._get_closest_nite(np.unique(sed['NITE'].values), 0)].copy().reset_index(drop=True)
            k_corrections = [self._get_kcorrect(peak_sed, band, redshift) for band in self.bands]
            setattr(self, attr_name, {b: k for b, k in zip(self.bands, k_corrections)})
            return k_corrections
        
        
    def _get_distance_modulus(self, redshift, cosmo):
        """
        Calculate the dimming effect of distance to the source
        
        :param redshift: the redshift of the object
        :param cosmo: an astropy.cosmology instance
        :return: dmod: the distance modulus contribution to the apparent magnitude
        """
        return 5.0 * np.log10(cosmo.luminosity_distance(redshift).value * 10 ** 6 / 10)

    
    def _integrate_through_band(self, sed, band, redshift, frame='REST'):
        """
        Calculate the flux through a given band by integrating in frequency
        
        :param sed: a dataframe containing the sed of the object
        :param band: the single-letter filter being used
        :param redshift: the redshift of the source
        :param frame: chose from ['REST', 'OBS'] to choose the rest frame or the observer frame
        :return: flux: the measured flux from the source through the filter
        """     
        frequency_arr = sed['FREQUENCY_{0}'.format(frame)].values
        delta_frequencies = np.diff(frequency_arr) * -1.0
        integrand = eval("self.{0}_transmission_frequency(frequency_arr) * sed['FLUX'].values / frequency_arr".format(band))
        average_integrands = 0.5 * np.diff(integrand) + integrand[0:-1]
        return np.sum(delta_frequencies * average_integrands)
    
    def _get_closest_nite(self, unique_nites, nite):
        """
        Return the nite in the sed closest to a desired nite
        
        :param unique_nites: a set of the nights in an sed
        :param nite: the nite you wish to find the closest neighbor for
        :return: closest_nite: the closest nite in the sed to the given nite
        """
        
        ## If nite not in the sed, (but within range) set nite to the closest nite in the sed
        ## If nite is outside range, keep the same
        if nite > unique_nites.max() or nite < unique_nites.min():
            return nite
        else:
            return unique_nites[np.argmin(np.abs(nite - unique_nites))]

    def gen_variable(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
        """
        Generate a random variable light curve

        Args:
            redshift (float): ignored 
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey    
            sed_filename (str): ignored
            cosmo (astropy.cosmology): ignored

        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object     
              - 'obj_type' contains a string for the type of object. Will always be 'Variable' here
              - 'sed' contains the filename of the sed used. Will always be 'Variable' here  
        """
        output_data_cols = ['NITE', 'BAND', 'MAG']
        output_data = []
        central_mag = random.uniform(12.0, 23.0)
        colors = {band: mag for band, mag in zip(self.bands, np.random.uniform(low=-2.0, high=2.0, size=len(self.bands)))}
        for band in self.bands:
            for nite in nite_dict[band]:
                central_mag = random.uniform(central_mag - 1.0, central_mag + 1.0)
                output_data.append([nite, band, central_mag + colors[band]])

        return {'lc': pd.DataFrame(data=output_data, columns=output_data_cols),
                'obj_type': 'Variable',
                'sed': 'Variable'}
    
    def gen_flat(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
        """
        Generate a random flat light curve.
        
        Args:
            redshift (float): ignored
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
            sed_filename (str): ignored
            cosmo (astropy.cosmology): ignored

        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object
              - 'obj_type' contains a string for the type of object. Will always be 'Flat' here
              - 'sed' contains the filename of the sed used. Will always be 'Flat' here      
        """
        output_data_cols = ['NITE', 'BAND', 'MAG']
        central_mag = random.uniform(12.0, 23.0)
        mags = {band: mag for band, mag in zip(self.bands, central_mag + np.random.uniform(low=-2.0, high=2.0, size=len(self.bands)))}
        output_data = []
        for band in self.bands:
            for nite in nite_dict[band]:
                output_data.append([nite, band, mags[band]])

        return {'lc': pd.DataFrame(data=output_data, columns=output_data_cols),
                'obj_type': 'Flat',
                'sed': 'Flat'}

    def gen_static(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
        """
        Make a static source capable of having time-series data by introducing a mag=99 source
        on each NITE of the simulation.

        Args:
            redshift (float): ignored 
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
            sed_filename (str): ignored                                                                                                                                                               
            cosmo (astropy.cosmology): ignored                                                                                                                                                                                            
        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object
              - 'obj_type' contains a string for the type of object. Will always be 'Static' here
              - 'sed' contains the filename of the sed used. Will always be 'Flat' here     
        """
        output_data_cols = ['NITE', 'BAND', 'MAG']
        central_mag = 99.0
        mags = {band: central_mag for band in self.bands}
        output_data = []
        for band in self.bands:
            for nite in nite_dict[band]:
                output_data.append([nite, band, mags[band]])

        return {'lc': pd.DataFrame(data=output_data, columns=output_data_cols),
                'obj_type': 'Static',
                'sed': 'Static'}


        
    def gen_variablenoise(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
        """ 
        Generate a variable light curve with small random noise

        Args:
            redshift (float): ignored
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
            sed_filename (str): ignored
            cosmo (astropy.cosmology): ignored

        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object
              - 'obj_type' contains a string for the type of object. Will always be 'VariableNoise' here
              - 'sed' contains the filename of the sed used. Will always be 'VariableNoise' here              
        """
        noiseless_lc_dict = self.gen_variable(redshift, nite_dict)
        noise = np.random.normal(loc=0, scale=0.25, size=noiseless_lc_dict['lc'].shape[0])
        noiseless_lc_dict['lc']['MAG'] = noiseless_lc_dict['lc']['MAG'].values + noise
        noiseless_lc_dict['obj_type'] = 'VariableNoise'
        noiseless_lc_dict['sed'] = 'VariableNoise'
        return noiseless_lc_dict

    
    def gen_flatnoise(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
        """
        Generate a flat light curve will small random noise

        Args:
            redshift (float): ignored
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
            sed_filename (str): ignored
            cosmo (astropy.cosmology): ignored

        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object
              - 'obj_type' contains a string for the type of object. Will always be 'FlatNoise' here
              - 'sed' contains the filename of the sed used. Will always be 'FlatNoise' here              
        """
        noiseless_lc_dict = self.gen_flat(redshift, nite_dict)
        noise = np.random.normal(loc=0, scale=0.25, size=noiseless_lc_dict['lc'].shape[0])
        noiseless_lc_dict['lc']['MAG'] = noiseless_lc_dict['lc']['MAG'].values + noise
        noiseless_lc_dict['obj_type'] = 'FlatNoise'
        noiseless_lc_dict['sed'] = 'FlatNoise'
        return noiseless_lc_dict
        
    def gen_user(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
        """
        Generate a light curve from a user-specidied SED

        Args:
            redshift (float): the redshift of the source
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey 
            sed (None or pandas.DataFrame, optional, default=None): a dataframe containing the sed of the SN 
            sed_filename (str): filename containing the time-series sed you want to use 
            cosmo (astropy.cosmology): an astropy.cosmology instance used for distance calculations

        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object
              - 'obj_type' contains a string for the type of object. Will always be <sed_filename> here 
              - 'sed' contains the filename of the sed used  
        """
        if sed is None:
            if sed_filename.startswith('seds/user/'):
                attr_name = sed_filename.split('.')[0]
            else:
                attr_name = 'seds/user/' + sed_filename.split('.')[0]
                sed_filename = 'seds/user/' + sed_filename
                
            if hasattr(self, attr_name):
                sed = getattr(self, attr_name)
            else:
                sed = self._read_sed(sed_filename)
                setattr(self, attr_name, sed)

        return self.gen_lc_from_sed(redshift, nite_dict, sed, sed_filename, sed_filename, cosmo=cosmo)

    def gen_kn(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
        """
        Generate a GW170817-like light curve.

        Args:
            redshift (float): the redshift of the source
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey 
            sed (None or pandas.DataFrame, optional, default=None): a dataframe containing the sed of the SN 
            sed_filename (str): filename containing the time-series sed you want to use 
            cosmo (astropy.cosmology): an astropy.cosmology instance used for distance calculations

        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object
              - 'obj_type' contains a string for the type of object. Will always be KN here 
              - 'sed' contains the filename of the sed used  
        """

        sed_filename = 'seds/kn/kn.SED'
        if sed is None:
            attr_name = sed_filename.split('.')[0]
            if hasattr(self, attr_name):
                sed = getattr(self, attr_name)
            else:
                sed = self._read_sed(sed_filename)
                setattr(self, attr_name, sed)
                
        return self.gen_lc_from_sed(redshift, nite_dict, sed, 'KN', sed_filename, cosmo=cosmo)
    
    def gen_ia(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
        """
        Generate a SN-Ia light curve.

        Args:
            redshift (float): the redshift of the source
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey 
            sed (None or pandas.DataFrame, optional, default=None): a dataframe containing the sed of the SN 
            sed_filename (str): filename containing the time-series sed you want to use 
            cosmo (astropy.cosmology): an astropy.cosmology instance used for distance calculations

        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object
              - 'obj_type' contains a string for the type of object. Will always be Ia here 
              - 'sed' contains the filename of the sed used  
        """
        
        # Read rest-frame sed if not supplied as argument
        if sed is None:
            if sed_filename is None:
                sed_filename = random.choice(self.ia_sed_files)
            
            if sed_filename.startswith('seds/ia/'):
                attr_name = sed_filename.split('.')[0]
            else:
                attr_name = 'seds/ia/' + sed_filename.split('.')[0]
                sed_filename = 'seds/ia/' + sed_filename

            if hasattr(self, attr_name):
                sed = getattr(self, attr_name)
            else:
                sed = self._read_sed(sed_filename)
                setattr(self, attr_name, sed)
                
        # Trigger the lc generation function on this sed
        return self.gen_lc_from_sed(redshift, nite_dict, sed, 'Ia', sed_filename, cosmo=cosmo)
    
    def gen_cc(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
        """
        Generate a SN-CC light curve
        
        Args:
            redshift (float): the redshift of the source
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey 
            sed (None or pandas.DataFrame, optional, default=None): a dataframe containing the sed of the SN 
            sed_filename (str): filename containing the time-series sed you want to use 
            cosmo (astropy.cosmology): an astropy.cosmology instance used for distance calculations

        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object
              - 'obj_type' contains a string for the type of object. Will be 'II', 'Ibc, etc. 
              - 'sed' contains the filename of the sed used  
        """

        # If sed not specified, choose sed based on weight map
        if sed is None:
            if sed_filename is None:
                sed_filename = random.choices(self.cc_sed_files, weights=self.cc_weights, k=1)[0]

            if sed_filename.startswith('seds/cc/'):
                attr_name = sed_filename.split('.')[0]
            else:
                attr_name = 'seds/cc/' + sed_filename.split('.')[0]
                sed_filename = 'seds/cc/' + sed_filename
                    
            if hasattr(self, attr_name):
                sed = getattr(self, attr_name)
            else:
                sed = self._read_sed(sed_filename)
                setattr(self, attr_name, sed)
        
        # Get the type of SN-CC
        obj_type = self.cc_info_df['SNTYPE'].values[self.cc_info_df['SED'].values == sed_filename.split('/')[-1].split('.')[0]][0]
        
        # Trigger the lc generation function on this sed
        return self.gen_lc_from_sed(redshift, nite_dict, sed, obj_type, sed_filename, cosmo=cosmo)
                
    def gen_lc_from_sed(self, redshift, nite_dict, sed, obj_type, sed_filename, cosmo=None):
        """
        Generate a light curve based on a time-series sed.
        
        Args:
            redshift (float): the redshift of the source
            nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey 
            sed (None or pandas.DataFrame, optional, default=None): a dataframe containing the sed of the SN 
            sed_filename (str): filename containing the time-series sed you want to use 
            cosmo (astropy.cosmology): an astropy.cosmology instance used for distance calculations

        Returns:
            lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
              - 'lc' contains a dataframe of the light from the object
              - 'obj_type' contains a string for the type of object. 
              - 'sed' contains the filename of the sed used  
        """
        
        # Adjust nites
        nites = {}
        sed_nites = np.unique(sed['NITE'].values)
        for band, cad_nites in nite_dict.items():
            useable_nites = []
            for nite in cad_nites:
                if nite not in sed_nites:
                    useable_nites.append(self._get_closest_nite(sed_nites, nite))
                else:
                    useable_nites.append(nite)
            nites[band] = useable_nites
            
        # Redshift the sed frequencies and wavelengths
        sed['WAVELENGTH_OBS'] = (1.0 + redshift) * sed['WAVELENGTH_REST'].values
        sed['FREQUENCY_OBS'] = sed['FREQUENCY_REST'].values * (1.0 + redshift)
        
        # Calculate distance modulus
        if not cosmo:
            cosmo = FlatLambdaCDM(H0=69.3 * u.km / (u.Mpc * u.s), 
                                  Om0=0.286, Tcmb0=2.725 * u.K, Neff=3.04, Ob0=0.0463)
        distance_modulus = self._get_distance_modulus(redshift, cosmo=cosmo)
        
        # Calculate k-correction at peak
        k_corrections = self._get_kcorrections(sed, sed_filename, redshift)
        
        # On each nite, in each band, calculate the absolute mag
        output_data = []
        output_data_cols = ['NITE', 'BAND', 'MAG']
        
        for band, k_correction in zip(self.bands, k_corrections):
            
            for nite in nites[band]:
                nite_sed = sed[sed['NITE'].values == nite].copy().reset_index(drop=True)
                
                # Flux is zero if requested nite is noe in sed
                if len(nite_sed) == 0:
                    output_data.append([nite, band, 99.0])
                    continue
                
                # Apply factors to calculate absolute mag
                nite_sed['FLUX'] = (cosmo.luminosity_distance(redshift).value * 10 ** 6 / 10) ** 2 / (1 + redshift) * nite_sed['FLUX'].values
                nite_sed['FREQUENCY_REST'] = nite_sed['FREQUENCY_REST'].values / (1. + redshift)
            
                # Calculate the apparent magnitude
                absolute_ab_mag = self._integrate_through_band(nite_sed, band, redshift, frame='REST') / self.norm_dict[band]
                output_data.append([nite, band, -2.5 * np.log10(absolute_ab_mag) + distance_modulus + k_correction])
                
        return {'lc': pd.DataFrame(data=output_data, columns=output_data_cols).replace(np.nan, 99.0, inplace=False),
                'obj_type': obj_type,
                'sed': sed_filename}

Methods

def gen_cc(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None)

Generate a SN-CC light curve

Args

redshift : float
the redshift of the source
nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
sed : None or pandas.DataFrame, optional, default=None
a dataframe containing the sed of the SN
sed_filename : str
filename containing the time-series sed you want to use
cosmo : astropy.cosmology
an astropy.cosmology instance used for distance calculations

Returns

lc_dict
a dictionary with keys ['lc, 'obj_type', 'sed'] - 'lc' contains a dataframe of the light from the object - 'obj_type' contains a string for the type of object. Will be 'II', 'Ibc, etc. - 'sed' contains the filename of the sed used
Expand source code
def gen_cc(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
    """
    Generate a SN-CC light curve
    
    Args:
        redshift (float): the redshift of the source
        nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey 
        sed (None or pandas.DataFrame, optional, default=None): a dataframe containing the sed of the SN 
        sed_filename (str): filename containing the time-series sed you want to use 
        cosmo (astropy.cosmology): an astropy.cosmology instance used for distance calculations

    Returns:
        lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
          - 'lc' contains a dataframe of the light from the object
          - 'obj_type' contains a string for the type of object. Will be 'II', 'Ibc, etc. 
          - 'sed' contains the filename of the sed used  
    """

    # If sed not specified, choose sed based on weight map
    if sed is None:
        if sed_filename is None:
            sed_filename = random.choices(self.cc_sed_files, weights=self.cc_weights, k=1)[0]

        if sed_filename.startswith('seds/cc/'):
            attr_name = sed_filename.split('.')[0]
        else:
            attr_name = 'seds/cc/' + sed_filename.split('.')[0]
            sed_filename = 'seds/cc/' + sed_filename
                
        if hasattr(self, attr_name):
            sed = getattr(self, attr_name)
        else:
            sed = self._read_sed(sed_filename)
            setattr(self, attr_name, sed)
    
    # Get the type of SN-CC
    obj_type = self.cc_info_df['SNTYPE'].values[self.cc_info_df['SED'].values == sed_filename.split('/')[-1].split('.')[0]][0]
    
    # Trigger the lc generation function on this sed
    return self.gen_lc_from_sed(redshift, nite_dict, sed, obj_type, sed_filename, cosmo=cosmo)
def gen_flat(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None)

Generate a random flat light curve.

Args

redshift : float
ignored
nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
sed_filename : str
ignored
cosmo : astropy.cosmology
ignored

Returns

lc_dict
a dictionary with keys ['lc, 'obj_type', 'sed'] - 'lc' contains a dataframe of the light from the object - 'obj_type' contains a string for the type of object. Will always be 'Flat' here - 'sed' contains the filename of the sed used. Will always be 'Flat' here
Expand source code
def gen_flat(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
    """
    Generate a random flat light curve.
    
    Args:
        redshift (float): ignored
        nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
        sed_filename (str): ignored
        cosmo (astropy.cosmology): ignored

    Returns:
        lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
          - 'lc' contains a dataframe of the light from the object
          - 'obj_type' contains a string for the type of object. Will always be 'Flat' here
          - 'sed' contains the filename of the sed used. Will always be 'Flat' here      
    """
    output_data_cols = ['NITE', 'BAND', 'MAG']
    central_mag = random.uniform(12.0, 23.0)
    mags = {band: mag for band, mag in zip(self.bands, central_mag + np.random.uniform(low=-2.0, high=2.0, size=len(self.bands)))}
    output_data = []
    for band in self.bands:
        for nite in nite_dict[band]:
            output_data.append([nite, band, mags[band]])

    return {'lc': pd.DataFrame(data=output_data, columns=output_data_cols),
            'obj_type': 'Flat',
            'sed': 'Flat'}
def gen_flatnoise(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None)

Generate a flat light curve will small random noise

Args

redshift : float
ignored
nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
sed_filename : str
ignored
cosmo : astropy.cosmology
ignored

Returns

lc_dict
a dictionary with keys ['lc, 'obj_type', 'sed'] - 'lc' contains a dataframe of the light from the object - 'obj_type' contains a string for the type of object. Will always be 'FlatNoise' here - 'sed' contains the filename of the sed used. Will always be 'FlatNoise' here
Expand source code
def gen_flatnoise(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
    """
    Generate a flat light curve will small random noise

    Args:
        redshift (float): ignored
        nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
        sed_filename (str): ignored
        cosmo (astropy.cosmology): ignored

    Returns:
        lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
          - 'lc' contains a dataframe of the light from the object
          - 'obj_type' contains a string for the type of object. Will always be 'FlatNoise' here
          - 'sed' contains the filename of the sed used. Will always be 'FlatNoise' here              
    """
    noiseless_lc_dict = self.gen_flat(redshift, nite_dict)
    noise = np.random.normal(loc=0, scale=0.25, size=noiseless_lc_dict['lc'].shape[0])
    noiseless_lc_dict['lc']['MAG'] = noiseless_lc_dict['lc']['MAG'].values + noise
    noiseless_lc_dict['obj_type'] = 'FlatNoise'
    noiseless_lc_dict['sed'] = 'FlatNoise'
    return noiseless_lc_dict
def gen_ia(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None)

Generate a SN-Ia light curve.

Args

redshift : float
the redshift of the source
nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
sed : None or pandas.DataFrame, optional, default=None
a dataframe containing the sed of the SN
sed_filename : str
filename containing the time-series sed you want to use
cosmo : astropy.cosmology
an astropy.cosmology instance used for distance calculations

Returns

lc_dict
a dictionary with keys ['lc, 'obj_type', 'sed'] - 'lc' contains a dataframe of the light from the object - 'obj_type' contains a string for the type of object. Will always be Ia here - 'sed' contains the filename of the sed used
Expand source code
def gen_ia(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
    """
    Generate a SN-Ia light curve.

    Args:
        redshift (float): the redshift of the source
        nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey 
        sed (None or pandas.DataFrame, optional, default=None): a dataframe containing the sed of the SN 
        sed_filename (str): filename containing the time-series sed you want to use 
        cosmo (astropy.cosmology): an astropy.cosmology instance used for distance calculations

    Returns:
        lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
          - 'lc' contains a dataframe of the light from the object
          - 'obj_type' contains a string for the type of object. Will always be Ia here 
          - 'sed' contains the filename of the sed used  
    """
    
    # Read rest-frame sed if not supplied as argument
    if sed is None:
        if sed_filename is None:
            sed_filename = random.choice(self.ia_sed_files)
        
        if sed_filename.startswith('seds/ia/'):
            attr_name = sed_filename.split('.')[0]
        else:
            attr_name = 'seds/ia/' + sed_filename.split('.')[0]
            sed_filename = 'seds/ia/' + sed_filename

        if hasattr(self, attr_name):
            sed = getattr(self, attr_name)
        else:
            sed = self._read_sed(sed_filename)
            setattr(self, attr_name, sed)
            
    # Trigger the lc generation function on this sed
    return self.gen_lc_from_sed(redshift, nite_dict, sed, 'Ia', sed_filename, cosmo=cosmo)
def gen_kn(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None)

Generate a GW170817-like light curve.

Args

redshift : float
the redshift of the source
nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
sed : None or pandas.DataFrame, optional, default=None
a dataframe containing the sed of the SN
sed_filename : str
filename containing the time-series sed you want to use
cosmo : astropy.cosmology
an astropy.cosmology instance used for distance calculations

Returns

lc_dict
a dictionary with keys ['lc, 'obj_type', 'sed'] - 'lc' contains a dataframe of the light from the object - 'obj_type' contains a string for the type of object. Will always be KN here - 'sed' contains the filename of the sed used
Expand source code
def gen_kn(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
    """
    Generate a GW170817-like light curve.

    Args:
        redshift (float): the redshift of the source
        nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey 
        sed (None or pandas.DataFrame, optional, default=None): a dataframe containing the sed of the SN 
        sed_filename (str): filename containing the time-series sed you want to use 
        cosmo (astropy.cosmology): an astropy.cosmology instance used for distance calculations

    Returns:
        lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
          - 'lc' contains a dataframe of the light from the object
          - 'obj_type' contains a string for the type of object. Will always be KN here 
          - 'sed' contains the filename of the sed used  
    """

    sed_filename = 'seds/kn/kn.SED'
    if sed is None:
        attr_name = sed_filename.split('.')[0]
        if hasattr(self, attr_name):
            sed = getattr(self, attr_name)
        else:
            sed = self._read_sed(sed_filename)
            setattr(self, attr_name, sed)
            
    return self.gen_lc_from_sed(redshift, nite_dict, sed, 'KN', sed_filename, cosmo=cosmo)
def gen_lc_from_sed(self, redshift, nite_dict, sed, obj_type, sed_filename, cosmo=None)

Generate a light curve based on a time-series sed.

Args

redshift : float
the redshift of the source
nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
sed : None or pandas.DataFrame, optional, default=None
a dataframe containing the sed of the SN
sed_filename : str
filename containing the time-series sed you want to use
cosmo : astropy.cosmology
an astropy.cosmology instance used for distance calculations

Returns

lc_dict
a dictionary with keys ['lc, 'obj_type', 'sed'] - 'lc' contains a dataframe of the light from the object - 'obj_type' contains a string for the type of object. - 'sed' contains the filename of the sed used
Expand source code
def gen_lc_from_sed(self, redshift, nite_dict, sed, obj_type, sed_filename, cosmo=None):
    """
    Generate a light curve based on a time-series sed.
    
    Args:
        redshift (float): the redshift of the source
        nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey 
        sed (None or pandas.DataFrame, optional, default=None): a dataframe containing the sed of the SN 
        sed_filename (str): filename containing the time-series sed you want to use 
        cosmo (astropy.cosmology): an astropy.cosmology instance used for distance calculations

    Returns:
        lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
          - 'lc' contains a dataframe of the light from the object
          - 'obj_type' contains a string for the type of object. 
          - 'sed' contains the filename of the sed used  
    """
    
    # Adjust nites
    nites = {}
    sed_nites = np.unique(sed['NITE'].values)
    for band, cad_nites in nite_dict.items():
        useable_nites = []
        for nite in cad_nites:
            if nite not in sed_nites:
                useable_nites.append(self._get_closest_nite(sed_nites, nite))
            else:
                useable_nites.append(nite)
        nites[band] = useable_nites
        
    # Redshift the sed frequencies and wavelengths
    sed['WAVELENGTH_OBS'] = (1.0 + redshift) * sed['WAVELENGTH_REST'].values
    sed['FREQUENCY_OBS'] = sed['FREQUENCY_REST'].values * (1.0 + redshift)
    
    # Calculate distance modulus
    if not cosmo:
        cosmo = FlatLambdaCDM(H0=69.3 * u.km / (u.Mpc * u.s), 
                              Om0=0.286, Tcmb0=2.725 * u.K, Neff=3.04, Ob0=0.0463)
    distance_modulus = self._get_distance_modulus(redshift, cosmo=cosmo)
    
    # Calculate k-correction at peak
    k_corrections = self._get_kcorrections(sed, sed_filename, redshift)
    
    # On each nite, in each band, calculate the absolute mag
    output_data = []
    output_data_cols = ['NITE', 'BAND', 'MAG']
    
    for band, k_correction in zip(self.bands, k_corrections):
        
        for nite in nites[band]:
            nite_sed = sed[sed['NITE'].values == nite].copy().reset_index(drop=True)
            
            # Flux is zero if requested nite is noe in sed
            if len(nite_sed) == 0:
                output_data.append([nite, band, 99.0])
                continue
            
            # Apply factors to calculate absolute mag
            nite_sed['FLUX'] = (cosmo.luminosity_distance(redshift).value * 10 ** 6 / 10) ** 2 / (1 + redshift) * nite_sed['FLUX'].values
            nite_sed['FREQUENCY_REST'] = nite_sed['FREQUENCY_REST'].values / (1. + redshift)
        
            # Calculate the apparent magnitude
            absolute_ab_mag = self._integrate_through_band(nite_sed, band, redshift, frame='REST') / self.norm_dict[band]
            output_data.append([nite, band, -2.5 * np.log10(absolute_ab_mag) + distance_modulus + k_correction])
            
    return {'lc': pd.DataFrame(data=output_data, columns=output_data_cols).replace(np.nan, 99.0, inplace=False),
            'obj_type': obj_type,
            'sed': sed_filename}
def gen_static(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None)

Make a static source capable of having time-series data by introducing a mag=99 source on each NITE of the simulation.

Args

redshift : float
ignored
nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
sed_filename : str
ignored
cosmo : astropy.cosmology
ignored

Returns

lc_dict
a dictionary with keys ['lc, 'obj_type', 'sed'] - 'lc' contains a dataframe of the light from the object - 'obj_type' contains a string for the type of object. Will always be 'Static' here - 'sed' contains the filename of the sed used. Will always be 'Flat' here
Expand source code
def gen_static(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
    """
    Make a static source capable of having time-series data by introducing a mag=99 source
    on each NITE of the simulation.

    Args:
        redshift (float): ignored 
        nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
        sed_filename (str): ignored                                                                                                                                                               
        cosmo (astropy.cosmology): ignored                                                                                                                                                                                            
    Returns:
        lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
          - 'lc' contains a dataframe of the light from the object
          - 'obj_type' contains a string for the type of object. Will always be 'Static' here
          - 'sed' contains the filename of the sed used. Will always be 'Flat' here     
    """
    output_data_cols = ['NITE', 'BAND', 'MAG']
    central_mag = 99.0
    mags = {band: central_mag for band in self.bands}
    output_data = []
    for band in self.bands:
        for nite in nite_dict[band]:
            output_data.append([nite, band, mags[band]])

    return {'lc': pd.DataFrame(data=output_data, columns=output_data_cols),
            'obj_type': 'Static',
            'sed': 'Static'}
def gen_user(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None)

Generate a light curve from a user-specidied SED

Args

redshift : float
the redshift of the source
nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
sed : None or pandas.DataFrame, optional, default=None
a dataframe containing the sed of the SN
sed_filename : str
filename containing the time-series sed you want to use
cosmo : astropy.cosmology
an astropy.cosmology instance used for distance calculations

Returns

lc_dict
a dictionary with keys ['lc, 'obj_type', 'sed'] - 'lc' contains a dataframe of the light from the object - 'obj_type' contains a string for the type of object. Will always be here - 'sed' contains the filename of the sed used
Expand source code
def gen_user(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
    """
    Generate a light curve from a user-specidied SED

    Args:
        redshift (float): the redshift of the source
        nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey 
        sed (None or pandas.DataFrame, optional, default=None): a dataframe containing the sed of the SN 
        sed_filename (str): filename containing the time-series sed you want to use 
        cosmo (astropy.cosmology): an astropy.cosmology instance used for distance calculations

    Returns:
        lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
          - 'lc' contains a dataframe of the light from the object
          - 'obj_type' contains a string for the type of object. Will always be <sed_filename> here 
          - 'sed' contains the filename of the sed used  
    """
    if sed is None:
        if sed_filename.startswith('seds/user/'):
            attr_name = sed_filename.split('.')[0]
        else:
            attr_name = 'seds/user/' + sed_filename.split('.')[0]
            sed_filename = 'seds/user/' + sed_filename
            
        if hasattr(self, attr_name):
            sed = getattr(self, attr_name)
        else:
            sed = self._read_sed(sed_filename)
            setattr(self, attr_name, sed)

    return self.gen_lc_from_sed(redshift, nite_dict, sed, sed_filename, sed_filename, cosmo=cosmo)
def gen_variable(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None)

Generate a random variable light curve

Args

redshift : float
ignored
nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
sed_filename : str
ignored
cosmo : astropy.cosmology
ignored

Returns

lc_dict
a dictionary with keys ['lc, 'obj_type', 'sed'] - 'lc' contains a dataframe of the light from the object
- 'obj_type' contains a string for the type of object. Will always be 'Variable' here - 'sed' contains the filename of the sed used. Will always be 'Variable' here
Expand source code
def gen_variable(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
    """
    Generate a random variable light curve

    Args:
        redshift (float): ignored 
        nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey    
        sed_filename (str): ignored
        cosmo (astropy.cosmology): ignored

    Returns:
        lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
          - 'lc' contains a dataframe of the light from the object     
          - 'obj_type' contains a string for the type of object. Will always be 'Variable' here
          - 'sed' contains the filename of the sed used. Will always be 'Variable' here  
    """
    output_data_cols = ['NITE', 'BAND', 'MAG']
    output_data = []
    central_mag = random.uniform(12.0, 23.0)
    colors = {band: mag for band, mag in zip(self.bands, np.random.uniform(low=-2.0, high=2.0, size=len(self.bands)))}
    for band in self.bands:
        for nite in nite_dict[band]:
            central_mag = random.uniform(central_mag - 1.0, central_mag + 1.0)
            output_data.append([nite, band, central_mag + colors[band]])

    return {'lc': pd.DataFrame(data=output_data, columns=output_data_cols),
            'obj_type': 'Variable',
            'sed': 'Variable'}
def gen_variablenoise(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None)

Generate a variable light curve with small random noise

Args

redshift : float
ignored
nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
sed_filename : str
ignored
cosmo : astropy.cosmology
ignored

Returns

lc_dict
a dictionary with keys ['lc, 'obj_type', 'sed'] - 'lc' contains a dataframe of the light from the object - 'obj_type' contains a string for the type of object. Will always be 'VariableNoise' here - 'sed' contains the filename of the sed used. Will always be 'VariableNoise' here
Expand source code
def gen_variablenoise(self, redshift, nite_dict, sed=None, sed_filename=None, cosmo=None):
    """ 
    Generate a variable light curve with small random noise

    Args:
        redshift (float): ignored
        nite_dict (dict[str: List[int]]): (band, list of night relative to peak you want to obtain a magnitude for) pair for each band in survey
        sed_filename (str): ignored
        cosmo (astropy.cosmology): ignored

    Returns:
        lc_dict: a dictionary with keys ['lc, 'obj_type', 'sed']
          - 'lc' contains a dataframe of the light from the object
          - 'obj_type' contains a string for the type of object. Will always be 'VariableNoise' here
          - 'sed' contains the filename of the sed used. Will always be 'VariableNoise' here              
    """
    noiseless_lc_dict = self.gen_variable(redshift, nite_dict)
    noise = np.random.normal(loc=0, scale=0.25, size=noiseless_lc_dict['lc'].shape[0])
    noiseless_lc_dict['lc']['MAG'] = noiseless_lc_dict['lc']['MAG'].values + noise
    noiseless_lc_dict['obj_type'] = 'VariableNoise'
    noiseless_lc_dict['sed'] = 'VariableNoise'
    return noiseless_lc_dict