def _make_crossspectrum(self, lc1, lc2): # make sure the inputs work! if not isinstance(lc1, lightcurve.Lightcurve): raise TypeError("lc1 must be a lightcurve.Lightcurve object") if not isinstance(lc2, lightcurve.Lightcurve): raise TypeError("lc2 must be a lightcurve.Lightcurve object") # Then check that GTIs make sense if self.gti is None: self.gti = cross_two_gtis(lc1.gti, lc2.gti) check_gtis(self.gti) if self.gti.shape[0] != 1: raise TypeError("Non-averaged Cross Spectra need " "a single Good Time Interval") lc1 = lc1.split_by_gti()[0] lc2 = lc2.split_by_gti()[0] # total number of photons is the sum of the # counts in the light curve self.nphots1 = np.float64(np.sum(lc1.counts)) self.nphots2 = np.float64(np.sum(lc2.counts)) self.meancounts1 = np.mean(lc1.counts) self.meancounts2 = np.mean(lc2.counts) # the number of data points in the light curve if lc1.n != lc2.n: raise StingrayError("Light curves do not have same number " "of time bins per segment.") if lc1.dt != lc2.dt: raise StingrayError("Light curves do not have " "same time binning dt.") self.n = lc1.n # the frequency resolution self.df = 1.0 / lc1.tseg # the number of averaged periodograms in the final output # This should *always* be 1 here self.m = 1 # make the actual Fourier transform and compute cross spectrum self.freq, self.unnorm_power = self._fourier_cross(lc1, lc2) # If co-spectrum is desired, normalize here. Otherwise, get raw back # with the imaginary part still intact. self.power = self._normalize_crossspectrum(self.unnorm_power, lc1.tseg)
def _operation_with_other_lc(self, other, operation): """ Helper method to codify an operation of one light curve with another (e.g. add, subtract, ...). Takes into account the GTIs correctly, and returns a new :class:`Lightcurve` object. Parameters ---------- other : :class:`Lightcurve` object A second light curve object operation : function An operation between the :class:`Lightcurve` object calling this method, and ``other``, operating on the ``counts`` attribute in each :class:`Lightcurve` object Returns ------- lc_new : Lightcurve object The new light curve calculated in ``operation`` """ if self.mjdref != other.mjdref: raise ValueError("MJDref is different in the two light curves") common_gti = cross_two_gtis(self.gti, other.gti) mask_self = create_gti_mask(self.time, common_gti, dt=self.dt) mask_other = create_gti_mask(other.time, common_gti, dt=other.dt) # ValueError is raised by Numpy while asserting np.equal over arrays # with different dimensions. try: diff = np.abs((self.time[mask_self] - other.time[mask_other])) assert np.all(diff < self.dt / 100) except (ValueError, AssertionError): raise ValueError("GTI-filtered time arrays of both light curves " "must be of same dimension and equal.") new_time = self.time[mask_self] new_counts = operation(self.counts[mask_self], other.counts[mask_other]) if self.err_dist.lower() != other.err_dist.lower(): simon("Lightcurves have different statistics!" "We are setting the errors to zero to avoid complications.") new_counts_err = np.zeros_like(new_counts) elif self.err_dist.lower() in valid_statistics: new_counts_err = \ np.sqrt(np.add(self.counts_err[mask_self]**2, other.counts_err[mask_other]**2)) # More conditions can be implemented for other statistics else: raise StingrayError("Statistics not recognized." " Please use one of these: " "{}".format(valid_statistics)) lc_new = Lightcurve(new_time, new_counts, err=new_counts_err, gti=common_gti, mjdref=self.mjdref) return lc_new
def _make_corr(self, lc1, lc2): """ Do some checks on the light curves supplied to the method, and then calculate the time shifts, time lags and cross correlation. Parameters ---------- lc1::class:`stingray.Lightcurve` object The first light curve data. lc2::class:`stingray.Lightcurve` object The second light curve data. """ if not isinstance(lc1, Lightcurve): raise TypeError("lc1 must be a lightcurve.Lightcurve object") if not isinstance(lc2, Lightcurve): raise TypeError("lc2 must be a lightcurve.Lightcurve object") if not np.isclose(lc1.dt, lc2.dt): raise StingrayError("Light curves do not have " "same time binning dt.") else: # ignore very small differences in dt neglected by np.isclose() lc1.dt = lc2.dt self.dt = lc1.dt # self.lc1 and self.lc2 may get assigned values explicitly in which case there is no need to copy data if self.lc1 is None: self.lc1 = lc1 if self.lc2 is None: self.lc2 = lc2 # Subtract means before passing scipy.signal.correlate into correlation lc1_counts = self.lc1.counts - np.mean(self.lc1.counts) lc2_counts = self.lc2.counts - np.mean(self.lc2.counts) # Calculates cross-correlation of two lightcurves self.corr = \ signal.correlate(lc1_counts, lc2_counts, self.mode) self.n = np.size(self.corr) self.time_shift, self.time_lags, self.n = self.cal_timeshift(dt=self.dt) # Normalization that makes the maximum correlation equal to 1, and # maximum anticorrelation -1. if self.norm == "variance": # Note that self.corr is normalized so that the maximum is # proportional to the number of bins in the first input # light curve. Hence, the division by the lc size variance1 = np.var(lc1.counts) - np.mean(lc1.counts_err)**2 variance2 = np.var(lc2.counts) - np.mean(lc2.counts_err)**2 self.corr = self.corr / np.sqrt(variance1 * variance2) / lc1_counts.size
def __sub__(self, other): """ Subtract two light curves element by element having the same time array. This magic method subtracts two Lightcurve objects having the same time array such that the corresponding counts arrays interferes with each other. GTIs are crossed, so that only common intervals are saved. Example ------- >>> time = [10, 20, 30] >>> count1 = [600, 1200, 800] >>> count2 = [300, 100, 400] >>> gti1 = [[0, 20]] >>> gti2 = [[0, 25]] >>> lc1 = Lightcurve(time, count1, gti=gti1) >>> lc2 = Lightcurve(time, count2, gti=gti2) >>> lc = lc1 - lc2 >>> lc.counts array([ 300, 1100, 400]) """ # ValueError is raised by Numpy while asserting np.equal over arrays # with different dimensions. try: assert np.all(np.equal(self.time, other.time)) except (ValueError, AssertionError): raise ValueError("Time arrays of both light curves must be " "of same dimension and equal.") new_counts = np.subtract(self.counts, other.counts) if self.err_dist.lower() != other.err_dist.lower(): simon("Lightcurves have different statistics!" "We are setting the errors to zero to avoid complications.") new_counts_err = np.zeros_like(new_counts) elif self.err_dist.lower() in valid_statistics: new_counts_err = np.sqrt(np.add(self.counts_err**2, other.counts_err**2)) # More conditions can be implemented for other statistics else: raise StingrayError("Statistics not recognized." " Please use one of these: " "{}".format(valid_statistics)) common_gti = cross_two_gtis(self.gti, other.gti) lc_new = Lightcurve(self.time, new_counts, err=new_counts_err, gti=common_gti) return lc_new
def create_gti_from_condition(time, condition, safe_interval=0, dt=None): """Create a GTI list from a time array and a boolean mask (``condition``). Parameters ---------- time : array-like Array containing time stamps condition : array-like An array of bools, of the same length of time. A possible condition can be, e.g., the result of ``lc > 0``. Returns ------- gtis : ``[[gti0_0, gti0_1], [gti1_0, gti1_1], ...]`` The newly created GTIs Other parameters ---------------- safe_interval : float or ``[float, float]`` A safe interval to exclude at both ends (if single float) or the start and the end (if pair of values) of GTIs. dt : float The width (in sec) of each bin of the time array. Can be irregular. """ if len(time) != len(condition): raise StingrayError('The length of the condition and ' 'time arrays must be the same.') idxs = contiguous_regions(condition) if not isinstance(safe_interval, collections.Iterable): safe_interval = [safe_interval, safe_interval] dt = assign_value_if_none(dt, np.zeros_like(time) + (time[1] - time[0]) / 2) gtis = [] for idx in idxs: logging.debug(idx) startidx = idx[0] stopidx = idx[1] - 1 t0 = time[startidx] - dt[startidx] + safe_interval[0] t1 = time[stopidx] + dt[stopidx] - safe_interval[1] if t1 - t0 < 0: continue gtis.append([t0, t1]) return np.array(gtis)
def cal_timeshift(self, dt=1.0): """ Calculate the cross correlation against all possible time lags, both positive and negative. Parameters ---------- dt: float, optional, default ``1.0`` Time resolution of the light curve, should be passed when object is populated with correlation data and no information about light curve can be extracted. Used to calculate ``time_lags``. Returns ------- self.time_shift: float Value of the time lag that gives maximum value of correlation between two light curves. self.time_lags: numpy.ndarray An array of ``time_lags`` calculated from correlation data """ if self.dt is None: self.dt = dt if self.corr is None: if (self.lc1 is None or self.lc2 is None) and (self.cross is None): raise StingrayError( 'Please provide either two lightcurve objects or \ a [average]crossspectrum object to calculate correlation and time_shift' ) else: # This will cover very rare case of assigning self.lc1 and lc2 # or self.cross and also self.corr = ``None``. # In this case, correlation is calculated using self.lc1 # and self.lc2 and using that correlation data, # time_shift is calculated. if self.cross is not None: self._make_cross_corr(self.cross) else: self._make_corr(self.lc1, self.lc2) self.n = len(self.corr) dur = int(self.n / 2) # Correlation against all possible lags, positive as well as negative lags are stored x_lags = np.linspace(-dur, dur, self.n) self.time_lags = x_lags * self.dt # time_shift is the time lag for max. correlation self.time_shift = self.time_lags[np.argmax(self.corr)] return self.time_shift, self.time_lags, self.n
def _make_corr(self, lc1, lc2): """ Do some checks on the light curves supplied to the method, and then calculate the time shifts, time lags and cross correlation. Parameters ---------- lc1::class:`stingray.Lightcurve` object The first light curve data. lc2::class:`stingray.Lightcurve` object The second light curve data. """ if not isinstance(lc1, lightcurve.Lightcurve): raise TypeError("lc1 must be a lightcurve.Lightcurve object") if not isinstance(lc2, lightcurve.Lightcurve): raise TypeError("lc2 must be a lightcurve.Lightcurve object") if not np.isclose(lc1.dt, lc2.dt): raise StingrayError("Light curves do not have " "same time binning dt.") else: # ignore very small differences in dt neglected by np.isclose() lc1.dt = lc2.dt self.dt = lc1.dt # self.lc1 and self.lc2 may get assigned values explicitly in which case there is no need to copy data if self.lc1 is None: self.lc1 = lc1 if self.lc2 is None: self.lc2 = lc2 # Subtract means before passing scipy.signal.correlate into correlation lc1_counts = self.lc1.counts - np.mean(self.lc1.counts) lc2_counts = self.lc2.counts - np.mean(self.lc2.counts) # Calculates cross-correlation of two lightcurves self.corr = signal.correlate(lc1_counts, lc2_counts, self.mode) self.n = len(self.corr) self.time_shift, self.time_lags, self.n = self.cal_timeshift( dt=self.dt)
def _operation_with_other_lc(self, other, operation): if self.mjdref != other.mjdref: raise ValueError("MJDref is different in the two light curves") common_gti = cross_two_gtis(self.gti, other.gti) mask_self = create_gti_mask(self.time, common_gti) mask_other = create_gti_mask(other.time, common_gti) # ValueError is raised by Numpy while asserting np.equal over arrays # with different dimensions. try: assert np.all( np.equal(self.time[mask_self], other.time[mask_other])) except (ValueError, AssertionError): raise ValueError("GTI-filtered time arrays of both light curves " "must be of same dimension and equal.") new_time = self.time[mask_self] new_counts = operation(self.counts[mask_self], other.counts[mask_other]) if self.err_dist.lower() != other.err_dist.lower(): simon("Lightcurves have different statistics!" "We are setting the errors to zero to avoid complications.") new_counts_err = np.zeros_like(new_counts) elif self.err_dist.lower() in valid_statistics: new_counts_err = np.sqrt( np.add(self.counts_err[mask_self]**2, other.counts_err[mask_other]**2)) # More conditions can be implemented for other statistics else: raise StingrayError("Statistics not recognized." " Please use one of these: " "{}".format(valid_statistics)) lc_new = Lightcurve(new_time, new_counts, err=new_counts_err, gti=common_gti, mjdref=self.mjdref) return lc_new
def cal_timeshift(self, dt=1.0): """ Creates Crosscorrelation Object. Parameters ---------- dt: float , optional, default 1.0 Time resolution of lightcurve, should be passed when object is populated with correlation data and no information about light curve can be extracted. Used to calculate time_lags. Returns ---------- self.time_shift: float Value of time lag that gives maximum value of correlation between two lightcurves. self.time_lags: numpy.ndarray An array of time_lags calculated from correlation data """ if self.dt is None: self.dt = dt if self.corr is None: if self.lc1 is None or self.lc2 is None: raise StingrayError( 'lc1 and lc2 should be provided to calculate correlation and time_shift' ) else: # This will cover very rare case of assigning self.lc1 and self.lc2 and self.corr = None. # In this case, correlation is calculated using self.lc1 and self.lc2 and using that correlation data, # time_shift is calculated. self._make_corr(self.lc1, self.lc2) return self.time_shift, self.time_lags, self.n self.n = len(self.corr) dur = int(self.n / 2) # Correlation against all possible lags, positive as well as negative lags are stored x_lags = np.linspace(-dur, dur, self.n) self.time_lags = x_lags * self.dt # time_shift is the time lag for max. correlation self.time_shift = self.time_lags[np.argmax(self.corr)] return self.time_shift, self.time_lags, self.n
def _make_crossspectrum(self, lc1, lc2): # make sure the inputs work! if not isinstance(lc1, Lightcurve): raise TypeError("lc1 must be a lightcurve.Lightcurve object") if not isinstance(lc2, Lightcurve): raise TypeError("lc2 must be a lightcurve.Lightcurve object") # Then check that GTIs make sense if self.gti is None: self.gti = cross_two_gtis(lc1.gti, lc2.gti) check_gtis(self.gti) if self.gti.shape[0] != 1: raise TypeError("Non-averaged Cross Spectra need " "a single Good Time Interval") lc1 = lc1.split_by_gti()[0] lc2 = lc2.split_by_gti()[0] # total number of photons is the sum of the # counts in the light curve self.nphots1 = np.float64(np.sum(lc1.counts)) self.nphots2 = np.float64(np.sum(lc2.counts)) self.meancounts1 = lc1.meancounts self.meancounts2 = lc2.meancounts # the number of data points in the light curve if lc1.n != lc2.n: raise StingrayError("Light curves do not have same number " "of time bins per segment.") if lc1.dt != lc2.dt: raise StingrayError("Light curves do not have " "same time binning dt.") self.n = lc1.n # the frequency resolution self.df = 1.0 / lc1.tseg # the number of averaged periodograms in the final output # This should *always* be 1 here self.m = 1 # make the actual Fourier transform and compute cross spectrum self.freq, self.unnorm_power = self._fourier_cross(lc1, lc2) # If co-spectrum is desired, normalize here. Otherwise, get raw back # with the imaginary part still intact. self.power = self._normalize_crossspectrum(self.unnorm_power, lc1.tseg) if lc1.err_dist.lower() != lc2.err_dist.lower(): simon("Your lightcurves have different statistics." "The errors in the Crossspectrum will be incorrect.") elif lc1.err_dist.lower() != "poisson": simon("Looks like your lightcurve statistic is not poisson." "The errors in the Powerspectrum will be incorrect.") if self.__class__.__name__ in [ 'Powerspectrum', 'AveragedPowerspectrum' ]: self.power_err = self.power / np.sqrt(self.m) elif self.__class__.__name__ in [ 'Crossspectrum', 'AveragedCrossspectrum' ]: # This is clearly a wild approximation. simon("Errorbars on cross spectra are not thoroughly tested. " "Please report any inconsistencies.") unnorm_power_err = np.sqrt(2) / np.sqrt(self.m) # Leahy-like unnorm_power_err /= (2 / np.sqrt(self.nphots1 * self.nphots2)) unnorm_power_err += np.zeros_like(self.power) self.power_err = \ self._normalize_crossspectrum(unnorm_power_err, lc1.tseg) else: self.power_err = np.zeros(len(self.power))
def join(self, other): """ Join two lightcurves into a single object. The new Lightcurve object will contain time stamps from both the objects. The count per bin in the resulting object will be the individual count per bin, or the average in case of overlapping time arrays of both lightcurve objects. Good Time intervals are also joined. Note : Time array of both lightcurves should not overlap each other. Parameters ---------- other : Lightcurve object The other Lightcurve object which is supposed to be joined with. Returns ------- lc_new : Lightcurve object The resulting lightcurve object. Example ------- >>> time1 = [5, 10, 15] >>> count1 = [300, 100, 400] >>> time2 = [20, 25, 30] >>> count2 = [600, 1200, 800] >>> lc1 = Lightcurve(time1, count1) >>> lc2 = Lightcurve(time2, count2) >>> lc = lc1.join(lc2) >>> lc.time array([ 5, 10, 15, 20, 25, 30]) >>> lc.counts array([ 300, 100, 400, 600, 1200, 800]) """ if self.mjdref != other.mjdref: raise ValueError("MJDref is different in the two light curves") if self.dt != other.dt: utils.simon("The two light curves have different bin widths.") if( self.tstart < other.tstart ): first_lc = self second_lc = other else: first_lc = other second_lc = self if len(np.intersect1d(self.time, other.time) > 0): utils.simon("The two light curves have overlapping time ranges. " "In the common time range, the resulting count will " "be the average of the counts in the two light " "curves. If you wish to sum, use `lc_sum = lc1 + " "lc2`.") valid_err = False if self.err_dist.lower() != other.err_dist.lower(): simon("Lightcurves have different statistics!" "We are setting the errors to zero.") elif self.err_dist.lower() in valid_statistics: valid_err = True # More conditions can be implemented for other statistics else: raise StingrayError("Statistics not recognized." " Please use one of these: " "{}".format(valid_statistics)) from collections import Counter counts = Counter() counts_err = Counter() for i, time in enumerate(first_lc.time): counts[time] = first_lc.counts[i] counts_err[time] = first_lc.counts_err[i] for i, time in enumerate(second_lc.time): if (counts.get(time) != None): #Common time counts[time] = (counts[time] + second_lc.counts[i]) / 2 #avg counts_err[time] = np.sqrt(( ((counts_err[time]**2) + (second_lc.counts_err[i] **2)) / 2)) else: counts[time] = second_lc.counts[i] counts_err[time] = second_lc.counts_err[i] new_time = list(counts.keys()) new_counts = list(counts.values()) if(valid_err): new_counts_err = list(counts_err.values()) else: new_counts_err = np.zeros_like(new_counts) del[counts, counts_err] else: new_time = np.concatenate([first_lc.time, second_lc.time]) new_counts = np.concatenate([first_lc.counts, second_lc.counts]) new_counts_err = np.concatenate([first_lc.counts_err, second_lc.counts_err]) new_time = np.asarray(new_time) new_counts = np.asarray(new_counts) new_counts_err = np.asarray(new_counts_err) gti = join_gtis(self.gti, other.gti) lc_new = Lightcurve(new_time, new_counts, err=new_counts_err, gti=gti, mjdref=self.mjdref, dt=self.dt) return lc_new
def __init__(self, time, counts, err=None, input_counts=True, gti=None, err_dist='poisson', mjdref=0, dt=None): """ Make a light curve object from an array of time stamps and an array of counts. Parameters ---------- time: iterable A list or array of time stamps for a light curve counts: iterable, optional, default None A list or array of the counts in each bin corresponding to the bins defined in `time` (note: use `input_counts=False` to input the count range, i.e. counts/second, otherwise use counts/bin). err: iterable, optional, default None: A list or array of the uncertainties in each bin corresponding to the bins defined in `time` (note: use `input_counts=False` to input the count rage, i.e. counts/second, otherwise use counts/bin). If None, we assume the data is poisson distributed and calculate the error from the average of the lower and upper 1-sigma confidence intervals for the Poissonian distribution with mean equal to `counts`. input_counts: bool, optional, default True If True, the code assumes that the input data in 'counts' is in units of counts/bin. If False, it assumes the data in 'counts' is in counts/second. gti: 2-d float array, default None [[gti0_0, gti0_1], [gti1_0, gti1_1], ...] Good Time Intervals. They are *not* applied to the data by default. They will be used by other methods to have an indication of the "safe" time intervals to use during analysis. err_dist: str, optional, default=None Statistic of the Lightcurve, it is used to calculate the uncertainties and other statistical values apropriately. Default makes no assumptions and keep errors equal to zero. mjdref: float MJD reference (useful in most high-energy mission data) Attributes ---------- time: numpy.ndarray The array of midpoints of time bins. bin_lo: The array of lower time stamp of time bins. bin_hi: The array of higher time stamp of time bins. counts: numpy.ndarray The counts per bin corresponding to the bins in `time`. counts_err: numpy.ndarray The uncertainties corresponding to `counts` countrate: numpy.ndarray The counts per second in each of the bins defined in `time`. countrate_err: numpy.ndarray The uncertainties corresponding to `countrate` meanrate: float The mean count rate of the light curve. meancounts: float The mean counts of the light curve. n: int The number of data points in the light curve. dt: float The time resolution of the light curve. mjdref: float MJD reference date (tstart / 86400 gives the date in MJD at the start of the observation) tseg: float The total duration of the light curve. tstart: float The start time of the light curve. gti: 2-d float array [[gti0_0, gti0_1], [gti1_0, gti1_1], ...] Good Time Intervals. They indicate the "safe" time intervals to be used during the analysis of the light curve. err_dist: string Statistic of the Lightcurve, it is used to calculate the uncertainties and other statistical values apropriately. It propagates to Spectrum classes. """ if not np.all(np.isfinite(time)): raise ValueError("There are inf or NaN values in " "your time array!") if not np.all(np.isfinite(counts)): raise ValueError("There are inf or NaN values in " "your counts array!") if len(time) != len(counts): raise StingrayError("time and counts array are not " "of the same length!") if len(time) <= 1: raise StingrayError("A single or no data points can not create " "a lightcurve!") if err is not None: if not np.all(np.isfinite(err)): raise ValueError("There are inf or NaN values in " "your err array") else: if err_dist.lower() not in valid_statistics: # err_dist set can be increased with other statistics raise StingrayError("Statistic not recognized." "Please select one of these: ", "{}".format(valid_statistics)) if err_dist.lower() == 'poisson': # Instead of the simple square root, we use confidence # intervals (should be valid for low fluxes too) err_low, err_high = poisson_conf_interval(np.asarray(counts), interval='frequentist-confidence', sigma=1) # calculate approximately symmetric uncertainties err_low -= np.asarray(counts) err_high -= np.asarray(counts) err = (np.absolute(err_low) + np.absolute(err_high))/2.0 # other estimators can be implemented for other statistics else: simon("Stingray only uses poisson err_dist at the moment, " "We are setting your errors to zero. " "Sorry for the inconvenience.") err = np.zeros_like(counts) self.mjdref = mjdref self.time = np.asarray(time) if dt is None: self.dt = np.median(self.time[1:] - self.time[:-1]) else: self.dt = dt self.bin_lo = self.time - 0.5 * self.dt self.bin_hi = self.time + 0.5 * self.dt self.err_dist = err_dist self.tstart = self.time[0] - 0.5*self.dt self.tseg = self.time[-1] - self.time[0] + self.dt self.gti = \ np.asarray(assign_value_if_none(gti, [[self.tstart, self.tstart + self.tseg]])) check_gtis(self.gti) good = create_gti_mask(self.time, self.gti) self.time = self.time[good] if input_counts: self.counts = np.asarray(counts)[good] self.countrate = self.counts / self.dt self.counts_err = np.asarray(err)[good] self.countrate_err = np.asarray(err)[good] / self.dt else: self.countrate = np.asarray(counts)[good] self.counts = self.countrate * self.dt self.counts_err = np.asarray(err)[good] * self.dt self.countrate_err = np.asarray(err)[good] self.meanrate = np.mean(self.countrate) self.meancounts = np.mean(self.counts) self.n = self.counts.shape[0] # Issue a warning if the input time iterable isn't regularly spaced, # i.e. the bin sizes aren't equal throughout. dt_array = np.diff(self.time) if not (np.allclose(dt_array, np.repeat(self.dt, dt_array.shape[0]))): simon("Bin sizes in input time array aren't equal throughout! " "This could cause problems with Fourier transforms. " "Please make the input time evenly sampled.")
def __init__(self, time, counts, input_counts=True, gti=None): """ Make a light curve object from an array of time stamps and an array of counts. Parameters ---------- time: iterable A list or array of time stamps for a light curve counts: iterable, optional, default None A list or array of the counts in each bin corresponding to the bins defined in `time` (note: **not** the count rate, i.e. counts/second, but the counts/bin). input_counts: bool, optional, default True If True, the code assumes that the input data in 'counts' is in units of counts/bin. If False, it assumes the data in 'counts' is in counts/second. gti: 2-d float array, default None [[gti0_0, gti0_1], [gti1_0, gti1_1], ...] Good Time Intervals. They are *not* applied to the data by default. They will be used by other methods to have an indication of the "safe" time intervals to use during analysis. Attributes ---------- time: numpy.ndarray The array of midpoints of time bins. bin_lo: The array of lower time stamp of time bins. bin_hi: The array of higher time stamp of time bins. counts: numpy.ndarray The counts per bin corresponding to the bins in `time`. countrate: numpy.ndarray The counts per second in each of the bins defined in `time`. meanrate: float The mean count rate of the light curve. meancounts: float The mean counts of the light curve. n: int The number of data points in the light curve. dt: float The time resolution of the light curve. tseg: float The total duration of the light curve. tstart: float The start time of the light curve. gti: 2-d float array [[gti0_0, gti0_1], [gti1_0, gti1_1], ...] Good Time Intervals. They indicate the "safe" time intervals to be used during the analysis of the light curve. """ if not np.all(np.isfinite(time)): raise ValueError("There are inf or NaN values in " "your time array!") if not np.all(np.isfinite(counts)): raise ValueError("There are inf or NaN values in " "your counts array!") if len(time) != len(counts): raise StingrayError("time and counts array are not " "of the same length!") if len(time) <= 1: raise StingrayError("A single or no data points can not create " "a lightcurve!") self.time = np.asarray(time) self.dt = time[1] - time[0] self.bin_lo = self.time - 0.5 * self.dt self.bin_hi = self.time + 0.5 * self.dt if input_counts: self.counts = np.asarray(counts) self.countrate = self.counts / self.dt else: self.countrate = np.asarray(counts) self.counts = self.countrate * self.dt self.meanrate = np.mean(self.countrate) self.meancounts = np.mean(self.counts) self.n = self.counts.shape[0] # Issue a warning if the input time iterable isn't regularly spaced, # i.e. the bin sizes aren't equal throughout. dt_array = np.diff(self.time) if not (np.allclose(dt_array, np.repeat(self.dt, dt_array.shape[0]))): simon("Bin sizes in input time array aren't equal throughout! " "This could cause problems with Fourier transforms. " "Please make the input time evenly sampled.") self.tseg = self.time[-1] - self.time[0] + self.dt self.tstart = self.time[0] - 0.5 * self.dt self.gti = \ np.asarray(assign_value_if_none(gti, [[self.tstart, self.tstart + self.tseg]])) check_gtis(self.gti)
def _make_crossspectrum(self, lc1, lc2): """ Auxiliary method computing the normalized cross spectrum from two light curves. This includes checking for the presence of and applying Good Time Intervals, computing the unnormalized Fourier cross-amplitude, and then renormalizing using the required normalization. Also computes an uncertainty estimate on the cross spectral powers. Parameters ---------- lc1, lc2 : :class:`stingray.Lightcurve` objects Two light curves used for computing the cross spectrum. """ # make sure the inputs work! if not isinstance(lc1, Lightcurve): raise TypeError("lc1 must be a lightcurve.Lightcurve object") if not isinstance(lc2, Lightcurve): raise TypeError("lc2 must be a lightcurve.Lightcurve object") if self.lc2.mjdref != self.lc1.mjdref: raise ValueError("MJDref is different in the two light curves") # Then check that GTIs make sense if self.gti is None: self.gti = cross_two_gtis(lc1.gti, lc2.gti) check_gtis(self.gti) if self.gti.shape[0] != 1: raise TypeError("Non-averaged Cross Spectra need " "a single Good Time Interval") lc1 = lc1.split_by_gti()[0] lc2 = lc2.split_by_gti()[0] # total number of photons is the sum of the # counts in the light curve self.nphots1 = np.float64(np.sum(lc1.counts)) self.nphots2 = np.float64(np.sum(lc2.counts)) self.meancounts1 = lc1.meancounts self.meancounts2 = lc2.meancounts # the number of data points in the light curve if lc1.n != lc2.n: raise StingrayError("Light curves do not have same number " "of time bins per segment.") # If dt differs slightly, its propagated error must not be more than # 1/100th of the bin if not np.isclose(lc1.dt, lc2.dt, rtol=0.01 * lc1.dt / lc1.tseg): raise StingrayError("Light curves do not have same time binning " "dt.") # In case a small difference exists, ignore it lc1.dt = lc2.dt self.n = lc1.n # the frequency resolution self.df = 1.0 / lc1.tseg # the number of averaged periodograms in the final output # This should *always* be 1 here self.m = 1 # make the actual Fourier transform and compute cross spectrum self.freq, self.unnorm_power = self._fourier_cross(lc1, lc2) # If co-spectrum is desired, normalize here. Otherwise, get raw back # with the imaginary part still intact. self.power = self._normalize_crossspectrum(self.unnorm_power, lc1.tseg) if lc1.err_dist.lower() != lc2.err_dist.lower(): simon("Your lightcurves have different statistics." "The errors in the Crossspectrum will be incorrect.") elif lc1.err_dist.lower() != "poisson": simon("Looks like your lightcurve statistic is not poisson." "The errors in the Powerspectrum will be incorrect.") if self.__class__.__name__ in ['Powerspectrum', 'AveragedPowerspectrum']: self.power_err = self.power / np.sqrt(self.m) elif self.__class__.__name__ in ['Crossspectrum', 'AveragedCrossspectrum']: # This is clearly a wild approximation. simon("Errorbars on cross spectra are not thoroughly tested. " "Please report any inconsistencies.") unnorm_power_err = np.sqrt(2) / np.sqrt(self.m) # Leahy-like unnorm_power_err /= (2 / np.sqrt(self.nphots1 * self.nphots2)) unnorm_power_err += np.zeros_like(self.power) self.power_err = \ self._normalize_crossspectrum(unnorm_power_err, lc1.tseg) else: self.power_err = np.zeros(len(self.power))
def join(self, other): """ Join two lightcurves into a single object. The new Lightcurve object will contain time stamps from both the objects. The count per bin in the resulting object will be the individual count per bin, or the average in case of overlapping time arrays of both lightcurve objects. Good Time intervals are also joined. Note : Time array of both lightcurves should not overlap each other. Parameters ---------- other : Lightcurve object The other Lightcurve object which is supposed to be joined with. Returns ------- lc_new : Lightcurve object The resulting lightcurve object. Example ------- >>> time1 = [5, 10, 15] >>> count1 = [300, 100, 400] >>> time2 = [20, 25, 30] >>> count2 = [600, 1200, 800] >>> lc1 = Lightcurve(time1, count1) >>> lc2 = Lightcurve(time2, count2) >>> lc = lc1.join(lc2) >>> lc.time array([ 5, 10, 15, 20, 25, 30]) >>> lc.counts array([ 300, 100, 400, 600, 1200, 800]) """ if self.dt != other.dt: utils.simon("The two light curves have different bin widths.") if self.tstart <= other.tstart: new_time = np.unique(np.concatenate([self.time, other.time])) else: new_time = np.unique(np.concatenate([other.time, self.time])) if len(new_time) != len(self.time) + len(other.time): utils.simon("The two light curves have overlapping time ranges. " "In the common time range, the resulting count will " "be the average of the counts in the two light " "curves. If you wish to sum, use `lc_sum = lc1 + " "lc2`.") new_counts = [] new_counts_err = [] # For every time stamp, get the individual time counts and add them. for time in new_time: try: count1 = self.counts[np.where(self.time == time)[0][0]] count1_err = self.counts_err[np.where(self.time == time)[0][0]] except IndexError: count1 = None count1_err = None try: count2 = other.counts[np.where(other.time == time)[0][0]] count2_err = other.counts_err[np.where(other.time == time)[0][0]] except IndexError: count2 = None count2_err = None if count1 is not None: if count2 is not None: # Average the overlapping counts new_counts.append((count1 + count2) / 2) if self.err_dist.lower() != other.err_dist.lower(): simon("Lightcurves have different statistics!" "We are setting the errors to zero.") new_counts_err = np.zeros_like(new_counts) elif self.err_dist.lower() in valid_statistics: new_counts_err.append(np.sqrt(((count1_err**2) + (count2_err**2)) / 2)) # More conditions can be implemented for other statistics else: raise StingrayError("Statistics not recognized." " Please use one of these: " "{}".format(valid_statistics)) else: new_counts.append(count1) new_counts_err.append(count1_err) else: new_counts.append(count2) new_counts_err.append(count2_err) new_counts = np.asarray(new_counts) new_counts_err = np.asarray(new_counts_err) gti = join_gtis(self.gti, other.gti) lc_new = Lightcurve(new_time, new_counts, err=new_counts_err, gti=gti) return lc_new
def __init__(self, time, counts, err=None, input_counts=True, gti=None, err_dist='poisson', mjdref=0, dt=None): if not np.all(np.isfinite(time)): raise ValueError("There are inf or NaN values in " "your time array!") if not np.all(np.isfinite(counts)): raise ValueError("There are inf or NaN values in " "your counts array!") if len(time) != len(counts): raise StingrayError("time and counts array are not " "of the same length!") if len(time) <= 1: raise StingrayError("A single or no data points can not create " "a lightcurve!") if err is not None: if not np.all(np.isfinite(err)): raise ValueError("There are inf or NaN values in " "your err array") else: if err_dist.lower() not in valid_statistics: # err_dist set can be increased with other statistics raise StingrayError("Statistic not recognized." "Please select one of these: ", "{}".format(valid_statistics)) if err_dist.lower() == 'poisson': # Instead of the simple square root, we use confidence # intervals (should be valid for low fluxes too) err = poisson_symmetrical_errors(counts) else: simon("Stingray only uses poisson err_dist at the moment, " "We are setting your errors to zero. " "Sorry for the inconvenience.") err = np.zeros_like(counts) self.mjdref = mjdref self.time = np.asarray(time) dt_array = np.diff(np.sort(self.time)) dt_array_unsorted = np.diff(self.time) unsorted = np.any(dt_array_unsorted < 0) if dt is None: if unsorted: logging.warning("The light curve is unordered! This may cause " "unexpected behaviour in some methods! Use " "sort() to order the light curve in time and " "check that the time resolution `dt` is " "calculated correctly!") self.dt = np.median(dt_array) else: self.dt = dt self.bin_lo = self.time - 0.5 * self.dt self.bin_hi = self.time + 0.5 * self.dt self.err_dist = err_dist if unsorted: self.tstart = np.min(self.time) - 0.5 * self.dt self.tseg = np.max(self.time) - np.min(self.time) + self.dt else: self.tstart = self.time[0] - 0.5 * self.dt self.tseg = self.time[-1] - self.time[0] + self.dt self.gti = \ np.asarray(assign_value_if_none(gti, [[self.tstart, self.tstart + self.tseg]])) check_gtis(self.gti) good = create_gti_mask(self.time, self.gti, dt=self.dt) self.time = self.time[good] if input_counts: self.counts = np.asarray(counts)[good] self.countrate = self.counts / self.dt self.counts_err = np.asarray(err)[good] self.countrate_err = np.asarray(err)[good] / self.dt else: self.countrate = np.asarray(counts)[good] self.counts = self.countrate * self.dt self.counts_err = np.asarray(err)[good] * self.dt self.countrate_err = np.asarray(err)[good] self.meanrate = np.mean(self.countrate) self.meancounts = np.mean(self.counts) self.n = self.counts.shape[0] # Issue a warning if the input time iterable isn't regularly spaced, # i.e. the bin sizes aren't equal throughout. dt_array = [] for g in self.gti: mask = create_gti_mask(self.time, [g], dt=self.dt) t = self.time[mask] dt_array.extend(np.diff(t)) dt_array = np.asarray(dt_array) if not (np.allclose(dt_array, np.repeat(self.dt, dt_array.shape[0]))): simon("Bin sizes in input time array aren't equal throughout! " "This could cause problems with Fourier transforms. " "Please make the input time evenly sampled.")