def stetson_kindex(fmags, ferrs): ''' This calculates the Stetson K index (robust measure of the kurtosis). Requires finite mags and errs. ''' # use a fill in value for the errors if they're none if ferrs is None: ferrs = npfull_like(mags, 0.005) ndet = len(fmags) if ndet > 9: # get the median and ndet medmag = npmedian(fmags) # get the stetson index elements delta_prefactor = (ndet / (ndet - 1)) sigma_i = delta_prefactor * (fmags - medmag) / ferrs stetsonk = (npsum(npabs(sigma_i)) / (npsqrt(npsum(sigma_i * sigma_i))) * (ndet**(-0.5))) return stetsonk else: LOGERROR('not enough detections in this magseries ' 'to calculate stetson K index') return npnan
def stetson_kindex(fmags, ferrs): '''This calculates the Stetson K index (a robust measure of the kurtosis). Parameters ---------- fmags,ferrs : np.array The input mag/flux time-series to process. Must have no non-finite elems. Returns ------- float The Stetson K variability index. ''' # use a fill in value for the errors if they're none if ferrs is None: ferrs = npfull_like(fmags, 0.005) ndet = len(fmags) if ndet > 9: # get the median and ndet medmag = npmedian(fmags) # get the stetson index elements delta_prefactor = (ndet/(ndet - 1)) sigma_i = delta_prefactor*(fmags - medmag)/ferrs stetsonk = ( npsum(npabs(sigma_i))/(npsqrt(npsum(sigma_i*sigma_i))) * (ndet**(-0.5)) ) return stetsonk else: LOGERROR('not enough detections in this magseries ' 'to calculate stetson K index') return npnan
def invgauss_eclipses_func(ebparams, times, mags, errs): '''This returns a double eclipse shaped function. Suitable for first order modeling of eclipsing binaries. ebparams = [period (time), epoch (time), pdepth (mags), pduration (phase), psdepthratio, secondaryphase] period is the period in days epoch is the time of minimum in JD pdepth is the depth of the primary eclipse - for magnitudes -> transitdepth should be < 0 - for fluxes -> transitdepth should be > 0 pduration is the length of the primary eclipse in phase psdepthratio is the ratio in the eclipse depths: depth_secondary/depth_primary. This is generally the same as the ratio of the Teffs of the two stars. secondaryphase is the phase at which the minimum of the secondary eclipse is located. This effectively parameterizes eccentricity. All of these will then have fitted values after the fit is done. ''' (period, epoch, pdepth, pduration, depthratio, secondaryphase) = ebparams # generate the phases iphase = (times - epoch) / period iphase = iphase - npfloor(iphase) phasesortind = npargsort(iphase) phase = iphase[phasesortind] ptimes = times[phasesortind] pmags = mags[phasesortind] perrs = errs[phasesortind] zerolevel = npmedian(pmags) modelmags = npfull_like(phase, zerolevel) primaryecl_amp = -pdepth secondaryecl_amp = -pdepth * depthratio primaryecl_std = pduration / 5.0 # we use 5-sigma as full-width -> duration secondaryecl_std = pduration / 5.0 # secondary eclipse has the same duration halfduration = pduration / 2.0 # phase indices primary_eclipse_ingress = ((phase >= (1.0 - halfduration)) & (phase <= 1.0)) primary_eclipse_egress = ((phase >= 0.0) & (phase <= halfduration)) secondary_eclipse_phase = ((phase >= (secondaryphase - halfduration)) & (phase <= (secondaryphase + halfduration))) # put in the eclipses modelmags[primary_eclipse_ingress] = (zerolevel + _gaussian( phase[primary_eclipse_ingress], primaryecl_amp, 1.0, primaryecl_std)) modelmags[primary_eclipse_egress] = (zerolevel + _gaussian( phase[primary_eclipse_egress], primaryecl_amp, 0.0, primaryecl_std)) modelmags[secondary_eclipse_phase] = ( zerolevel + _gaussian(phase[secondary_eclipse_phase], secondaryecl_amp, secondaryphase, secondaryecl_std)) return modelmags, phase, ptimes, pmags, perrs
def bls_snr(blsdict, times, mags, errs, magsarefluxes=False, sigclip=10.0, perioddeltapercent=10, npeaks=None, assumeserialbls=False, verbose=True): '''Calculates the signal to noise ratio for each best peak in the BLS periodogram. This calculates two values of SNR: SNR = transit model depth / RMS of light curve with transit model subtracted altSNR = transit model depth / RMS of light curve inside transit blsdict is the output of either bls_parallel_pfind or bls_serial_pfind. times, mags, errs are ndarrays containing the magnitude series. perioddeltapercent controls the period interval used by a bls_serial_pfind run around each peak period to figure out the transit depth, duration, and ingress/egress bins for eventual calculation of the SNR of the peak. npeaks controls how many of the periods in blsdict['nbestperiods'] to find the SNR for. If it's None, then this will calculate the SNR for all of them. If it's an integer between 1 and len(blsdict['nbestperiods']), will calculate for only the specified number of peak periods, starting from the best period. If assumeserialbls is True, will not rerun bls_serial_pfind to figure out the transit depth, duration, and ingress/egress bins for eventual calculation of the SNR of the peak. This is normally False because we assume that the user will be using bls_parallel_pfind, which works on chunks of frequency space so returns multiple values of transit depth, duration, ingress/egress bin specific to those chunks. These may not be valid for the global best peaks in the periodogram, so we need to rerun bls_serial_pfind around each peak in blsdict['nbestperiods'] to get correct values for these. FIXME: for now, we're only doing simple RMS. Need to calculate red and white-noise RMS as outlined below: - calculate the white noise rms and the red noise rms of the residual. - the white noise rms is just the rms of the residual - the red noise rms = sqrt(binnedrms^2 - expectedbinnedrms^2) - calculate the SNR using: sqrt(delta^2 / ((sigma_w ^2 / nt) + (sigma_r ^2 / Nt)))) where: delta = transit depth sigma_w = white noise rms sigma_r = red noise rms nt = number of in-transit points Nt = number of distinct transits sampled ''' # figure out how many periods to work on if (npeaks and (0 < npeaks < len(blsdict['nbestperiods']))): nperiods = npeaks else: if verbose: LOGWARNING('npeaks not specified or invalid, ' 'getting SNR for all %s BLS peaks' % len(blsdict['nbestperiods'])) nperiods = len(blsdict['nbestperiods']) nbestperiods = blsdict['nbestperiods'][:nperiods] # get rid of nans first and sigclip stimes, smags, serrs = sigclip_magseries(times, mags, errs, magsarefluxes=magsarefluxes, sigclip=sigclip) # make sure there are enough points to calculate a spectrum if len(stimes) > 9 and len(smags) > 9 and len(serrs) > 9: nbestsnrs = [] nbestasnrs = [] transitdepth, transitduration = [], [] # get these later whitenoise, rednoise = [], [] nphasebins, transingressbin, transegressbin = [], [], [] # keep these around for diagnostics allsubtractedmags = [] allphasedmags = [] allphases = [] allblsmodels = [] for ind, period in enumerate(nbestperiods): # get the period interval startp = period - perioddeltapercent * period / 100.0 endp = period + perioddeltapercent * period / 100.0 # see if we need to rerun bls_serial_pfind if not assumeserialbls: # run bls_serial_pfind with the kwargs copied over from the # initial run. replace only the startp, endp, and verbose kwarg # values prevkwargs = blsdict['kwargs'].copy() prevkwargs['verbose'] = verbose prevkwargs['startp'] = startp prevkwargs['endp'] = endp blsres = bls_serial_pfind(times, mags, errs, **prevkwargs) else: blsres = blsdict thistransdepth = blsres['blsresult']['transdepth'] thistransduration = blsres['blsresult']['transduration'] thisbestperiod = blsres['bestperiod'] thistransingressbin = blsres['blsresult']['transingressbin'] thistransegressbin = blsres['blsresult']['transegressbin'] thisnphasebins = blsdict['kwargs']['nphasebins'] # get the minimum light epoch using a spline fit try: spfit = spline_fit_magseries(times, mags, errs, thisbestperiod, magsarefluxes=magsarefluxes, verbose=verbose) thisminepoch = spfit['fitinfo']['fitepoch'] except ValueError: LOGEXCEPTION('could not fit a spline to find a minimum of ' 'the phased LC, trying SavGol fit instead...') # fit a Savitsky-Golay instead and get its minimum savfit = savgol_fit_magseries(times, mags, errs, thisbestperiod, magsarefluxes=magsarefluxes, verbose=verbose) thisminepoch = savfit['fitinfo']['fitepoch'] if isinstance(thisminepoch, np.ndarray): if verbose: LOGWARNING('minimum epoch is actually an array:\n' '%s\n' 'instead of a float, ' 'are there duplicate time values ' 'in the original input? ' 'will use the first value in this array.' % repr(thisminepoch)) thisminepoch = thisminepoch[0] # phase using this epoch phased_magseries = phase_magseries_with_errs(stimes, smags, serrs, thisbestperiod, thisminepoch, wrap=False, sort=True) tphase = phased_magseries['phase'] tmags = phased_magseries['mags'] terrs = phased_magseries['errs'] # use the transit depth and duration to subtract the BLS transit # model from the phased mag series. we're centered about 0.0 as the # phase of the transit minimum so we need to look at stuff from # [0.0, transitphase] and [1.0-transitphase, 1.0] transitphase = thistransduration / 2.0 transitindices = ((tphase < transitphase) | (tphase > (1.0 - transitphase))) # this is the BLS model # constant = median(tmags) outside transit # constant = thistransitdepth inside transit blsmodel = npfull_like(tmags, npmedian(tmags)) if magsarefluxes: blsmodel[transitindices] = (blsmodel[transitindices] + thistransdepth) else: blsmodel[transitindices] = (blsmodel[transitindices] - thistransdepth) # this is the residual of mags - model subtractedmags = tmags - blsmodel # calculate the rms of this residual subtractedrms = npstd(subtractedmags) # the SNR is the transit depth divided by the rms of the residual thissnr = npabs(thistransdepth / subtractedrms) # alt SNR = expected transit depth / rms of timeseries in transit altsnr = npabs(thistransdepth / npstd(tmags[transitindices])) # tell user about stuff if verbose = True if verbose: LOGINFO('peak %s: new best period: %.6f, ' 'fit center of transit: %.5f' % (ind + 1, thisbestperiod, thisminepoch)) LOGINFO('transit ingress phase = %.3f to %.3f' % (1.0 - transitphase, 1.0)) LOGINFO('transit egress phase = %.3f to %.3f' % (0.0, transitphase)) LOGINFO('npoints in transit: %s' % tmags[transitindices].size) LOGINFO('transit depth (delta): %.5f, ' 'frac transit length (q): %.3f, ' ' SNR: %.3f, altSNR: %.3f' % (thistransdepth, thistransduration, thissnr, altsnr)) # update the lists with results from this peak nbestsnrs.append(thissnr) nbestasnrs.append(altsnr) transitdepth.append(thistransdepth) transitduration.append(thistransduration) transingressbin.append(thistransingressbin) transegressbin.append(thistransegressbin) nphasebins.append(thisnphasebins) # update the diagnostics allsubtractedmags.append(subtractedmags) allphasedmags.append(tmags) allphases.append(tphase) allblsmodels.append(blsmodel) # update these when we figure out how to do it # nphasebins.append(thisnphasebins) # transingressbin.append(thisingressbin) # transegressbin.append(thisegressbin) # done with working on each peak # if there aren't enough points in the mag series, bail out else: LOGERROR('no good detections for these times and mags, skipping...') nbestsnrs, whitenoise, rednoise = None, None, None transitdepth, transitduration = None, None nphasebins, transingressbin, transegressbin = None, None, None allsubtractedmags, allphases, allphasedmags = None, None, None return { 'npeaks': npeaks, 'period': nbestperiods, 'snr': nbestsnrs, 'altsnr': nbestasnrs, 'whitenoise': whitenoise, 'rednoise': rednoise, 'transitdepth': transitdepth, 'transitduration': transitduration, 'nphasebins': nphasebins, 'transingressbin': transingressbin, 'transegressbin': transegressbin, 'allblsmodels': allblsmodels, 'allsubtractedmags': allsubtractedmags, 'allphasedmags': allphasedmags, 'allphases': allphases }
def spline_fit_magseries(times, mags, errs, period, knotfraction=0.01, maxknots=30, sigclip=30.0, plotfit=False, ignoreinitfail=False, magsarefluxes=False, verbose=True): '''This fits a univariate cubic spline to the phased light curve. This fit may be better than the Fourier fit for sharply variable objects, like EBs, so can be used to distinguish them from other types of variables. The knot fraction is the number of internal knots to use for the spline. A value of 0.01 (or 1%) of the total number of non-nan observations appears to work quite well, without over-fitting. maxknots controls the maximum number of knots that will be allowed. magsarefluxes is a boolean value for setting the ylabel and ylimits of plots for either magnitudes (False) or flux units (i.e. normalized to 1, in which case magsarefluxes should be set to True). Returns the chisq of the fit, as well as the reduced chisq. FIXME: check this equation below to see if it's right. reduced_chisq = fit_chisq/(len(pmags) - len(knots) - 1) ''' # this is required to fit the spline correctly if errs is None: errs = npfull_like(mags, 0.005) # sigclip the magnitude time series stimes, smags, serrs = sigclip_magseries(times, mags, errs, sigclip=sigclip, magsarefluxes=magsarefluxes) # get rid of zero errs nzind = npnonzero(serrs) stimes, smags, serrs = stimes[nzind], smags[nzind], serrs[nzind] # phase the mag series phase, pmags, perrs, ptimes, mintime = (_get_phased_quantities( stimes, smags, serrs, period)) # now figure out the number of knots up to max knots (=100) nobs = len(phase) nknots = int(npfloor(knotfraction * nobs)) nknots = maxknots if nknots > maxknots else nknots splineknots = nplinspace(phase[0] + 0.01, phase[-1] - 0.01, num=nknots) # generate and fit the spline spl = LSQUnivariateSpline(phase, pmags, t=splineknots, w=1.0 / perrs) # calculate the spline fit to the actual phases, the chisq and red-chisq fitmags = spl(phase) fitchisq = npsum(((fitmags - pmags) * (fitmags - pmags)) / (perrs * perrs)) fitredchisq = fitchisq / (len(pmags) - nknots - 1) if verbose: LOGINFO('spline fit done. nknots = %s, ' 'chisq = %.5f, reduced chisq = %.5f' % (nknots, fitchisq, fitredchisq)) # figure out the time of light curve minimum (i.e. the fit epoch) # this is when the fit mag is maximum (i.e. the faintest) # or if magsarefluxes = True, then this is when fit flux is minimum if not magsarefluxes: fitmagminind = npwhere(fitmags == npmax(fitmags)) else: fitmagminind = npwhere(fitmags == npmin(fitmags)) magseriesepoch = ptimes[fitmagminind] # assemble the returndict returndict = { 'fittype': 'spline', 'fitinfo': { 'nknots': nknots, 'fitmags': fitmags, 'fitepoch': magseriesepoch }, 'fitchisq': fitchisq, 'fitredchisq': fitredchisq, 'fitplotfile': None, 'magseries': { 'times': ptimes, 'phase': phase, 'mags': pmags, 'errs': perrs, 'magsarefluxes': magsarefluxes }, } # make the fit plot if required if plotfit and isinstance(plotfit, str): _make_fit_plot(phase, pmags, perrs, fitmags, period, mintime, magseriesepoch, plotfit, magsarefluxes=magsarefluxes) returndict['fitplotfile'] = plotfit return returndict
def spline_fit_magseries(times, mags, errs, period, knotfraction=0.01, maxknots=30, sigclip=30.0, plotfit=False, ignoreinitfail=False, magsarefluxes=False, verbose=True): '''This fits a univariate cubic spline to the phased light curve. This fit may be better than the Fourier fit for sharply variable objects, like EBs, so can be used to distinguish them from other types of variables. Parameters ---------- times,mags,errs : np.array The input mag/flux time-series to fit a spline to. period : float The period to use for the spline fit. knotfraction : float The knot fraction is the number of internal knots to use for the spline. A value of 0.01 (or 1%) of the total number of non-nan observations appears to work quite well, without over-fitting. maxknots controls the maximum number of knots that will be allowed. maxknots : int The maximum number of knots that will be used even if `knotfraction` gives a value to use larger than `maxknots`. This helps dealing with over-fitting to short time-scale variations. sigclip : float or int or sequence of two floats/ints or None If a single float or int, a symmetric sigma-clip will be performed using the number provided as the sigma-multiplier to cut out from the input time-series. If a list of two ints/floats is provided, the function will perform an 'asymmetric' sigma-clip. The first element in this list is the sigma value to use for fainter flux/mag values; the second element in this list is the sigma value to use for brighter flux/mag values. For example, `sigclip=[10., 3.]`, will sigclip out greater than 10-sigma dimmings and greater than 3-sigma brightenings. Here the meaning of "dimming" and "brightening" is set by *physics* (not the magnitude system), which is why the `magsarefluxes` kwarg must be correctly set. If `sigclip` is None, no sigma-clipping will be performed, and the time-series (with non-finite elems removed) will be passed through to the output. magsarefluxes : bool If True, will treat the input values of `mags` as fluxes for purposes of plotting the fit and sig-clipping. plotfit : str or False If this is a string, this function will make a plot for the fit to the mag/flux time-series and writes the plot to the path specified here. ignoreinitfail : bool If this is True, ignores the initial failure to find a set of optimized Fourier parameters using the global optimization function and proceeds to do a least-squares fit anyway. verbose : bool If True, will indicate progress and warn of any problems. Returns ------- dict This function returns a dict containing the model fit parameters, the minimized chi-sq value and the reduced chi-sq value. The form of this dict is mostly standardized across all functions in this module:: { 'fittype':'spline', 'fitinfo':{ 'nknots': the number of knots used for the fit 'fitmags': the model fit mags, 'fitepoch': the epoch of minimum light for the fit, }, 'fitchisq': the minimized value of the fit's chi-sq, 'fitredchisq':the reduced chi-sq value, 'fitplotfile': the output fit plot if fitplot is not None, 'magseries':{ 'times':input times in phase order of the model, 'phase':the phases of the model mags, 'mags':input mags/fluxes in the phase order of the model, 'errs':errs in the phase order of the model, 'magsarefluxes':input value of magsarefluxes kwarg } } ''' # this is required to fit the spline correctly if errs is None: errs = npfull_like(mags, 0.005) # sigclip the magnitude time series stimes, smags, serrs = sigclip_magseries(times, mags, errs, sigclip=sigclip, magsarefluxes=magsarefluxes) # get rid of zero errs nzind = npnonzero(serrs) stimes, smags, serrs = stimes[nzind], smags[nzind], serrs[nzind] # phase the mag series phase, pmags, perrs, ptimes, mintime = ( get_phased_quantities(stimes, smags, serrs, period) ) # now figure out the number of knots up to max knots (=100) nobs = len(phase) nknots = int(npfloor(knotfraction*nobs)) nknots = maxknots if nknots > maxknots else nknots splineknots = nplinspace(phase[0] + 0.01, phase[-1] - 0.01, num=nknots) # NOTE: newer scipy needs x to be strictly increasing. this means we should # filter out anything that doesn't have np.diff(phase) > 0.0 # FIXME: this needs to be tested phase_diffs_ind = npdiff(phase) > 0.0 incphase_ind = npconcatenate((nparray([True]), phase_diffs_ind)) phase, pmags, perrs = (phase[incphase_ind], pmags[incphase_ind], perrs[incphase_ind]) # generate and fit the spline spl = LSQUnivariateSpline(phase, pmags, t=splineknots, w=1.0/perrs) # calculate the spline fit to the actual phases, the chisq and red-chisq fitmags = spl(phase) fitchisq = npsum( ((fitmags - pmags)*(fitmags - pmags)) / (perrs*perrs) ) fitredchisq = fitchisq/(len(pmags) - nknots - 1) if verbose: LOGINFO( 'spline fit done. nknots = %s, ' 'chisq = %.5f, reduced chisq = %.5f' % (nknots, fitchisq, fitredchisq) ) # figure out the time of light curve minimum (i.e. the fit epoch) # this is when the fit mag is maximum (i.e. the faintest) # or if magsarefluxes = True, then this is when fit flux is minimum if not magsarefluxes: fitmagminind = npwhere(fitmags == npmax(fitmags)) else: fitmagminind = npwhere(fitmags == npmin(fitmags)) if len(fitmagminind[0]) > 1: fitmagminind = (fitmagminind[0][0],) magseriesepoch = ptimes[fitmagminind] # assemble the returndict returndict = { 'fittype':'spline', 'fitinfo':{ 'nknots':nknots, 'fitmags':fitmags, 'fitepoch':magseriesepoch }, 'fitchisq':fitchisq, 'fitredchisq':fitredchisq, 'fitplotfile':None, 'magseries':{ 'times':ptimes, 'phase':phase, 'mags':pmags, 'errs':perrs, 'magsarefluxes':magsarefluxes }, } # make the fit plot if required if plotfit and isinstance(plotfit, str): make_fit_plot(phase, pmags, perrs, fitmags, period, mintime, magseriesepoch, plotfit, magsarefluxes=magsarefluxes) returndict['fitplotfile'] = plotfit return returndict
def flare_model(flareparams, times, mags, errs): '''This is a flare model function, similar to Kowalski+ 2011. Model params ------------ flareparams is a list: [amplitude, flare_peak_time, rise_gaussian_stdev, decay_time_constant] where: amplitude: the maximum flare amplitude in mags or flux. If flux, then amplitude should be positive. If mags, amplitude should be negative. flare_peak_time: time at which the flare maximum happens rise_gaussian_stdev: the stdev of the gaussian describing the rise of the flare decay_time_constant: the time constant of the exponential fall of the flare Other args ---------- times: a numpy array of times mags: a numpy array of magnitudes or fluxes. the flare will simply be added to mags at the appropriate times errs: a numpy array of measurement errors for each mag/flux measurement ''' (amplitude, flare_peak_time, rise_gaussian_stdev, decay_time_constant) = flareparams zerolevel = npmedian(mags) modelmags = npfull_like(times, zerolevel) # before peak gaussian rise... modelmags[times < flare_peak_time] = ( mags[times < flare_peak_time] + amplitude * np.exp( -((times[times < flare_peak_time] - flare_peak_time) * (times[times < flare_peak_time] - flare_peak_time)) / (2.0*rise_gaussian_stdev*rise_gaussian_stdev) ) ) # after peak exponential decay... modelmags[times > flare_peak_time] = ( mags[times > flare_peak_time] + amplitude * np.exp( -((times[times > flare_peak_time] - flare_peak_time)) / (decay_time_constant) ) ) return modelmags, times, mags, errs
def trapezoid_transit_func(transitparams, times, mags, errs): '''This returns a trapezoid transit-shaped function. Suitable for first order modeling of transit signals. transitparams = [transitperiod (time), transitepoch (time), transitdepth (flux or mags), transitduration (phase), ingressduration (phase)] All of these will then have fitted values after the fit is done. for magnitudes -> transitdepth should be < 0 for fluxes -> transitdepth should be > 0 ''' (transitperiod, transitepoch, transitdepth, transitduration, ingressduration) = transitparams # generate the phases iphase = (times - transitepoch) / transitperiod iphase = iphase - npfloor(iphase) phasesortind = npargsort(iphase) phase = iphase[phasesortind] ptimes = times[phasesortind] pmags = mags[phasesortind] perrs = errs[phasesortind] zerolevel = npmedian(pmags) modelmags = npfull_like(phase, zerolevel) halftransitduration = transitduration / 2.0 bottomlevel = zerolevel - transitdepth slope = transitdepth / ingressduration # the four contact points of the eclipse firstcontact = 1.0 - halftransitduration secondcontact = firstcontact + ingressduration thirdcontact = halftransitduration - ingressduration fourthcontact = halftransitduration ## the phase indices ## # during ingress ingressind = (phase > firstcontact) & (phase < secondcontact) # at transit bottom bottomind = (phase > secondcontact) | (phase < thirdcontact) # during egress egressind = (phase > thirdcontact) & (phase < fourthcontact) # set the mags modelmags[ingressind] = zerolevel - slope * (phase[ingressind] - firstcontact) modelmags[bottomind] = bottomlevel modelmags[egressind] = bottomlevel + slope * (phase[egressind] - thirdcontact) return modelmags, phase, ptimes, pmags, perrs