def read(self, filename, format_='pickle'): """ Read a :class:`Lightcurve` object from file. Currently supported formats are * pickle (not recommended for long-term storage) * HDF5 * ASCII Parameters ---------- filename: str Path and file name for the file to be read. format\_: str Available options are 'pickle', 'hdf5', 'ascii' Returns -------- lc : ``astropy.table`` or ``dict`` or :class:`Lightcurve` object * If ``format\_`` is ``ascii``: ``astropy.table`` is returned. * If ``format\_`` is ``hdf5``: dictionary with key-value pairs is returned. * If ``format\_`` is ``pickle``: :class:`Lightcurve` object is returned. """ if format_ == 'ascii' or format_ == 'hdf5': return io.read(filename, format_) elif format_ == 'pickle': self = io.read(filename, format_) else: utils.simon("Format not understood.")
def write(self, filename, format_='pickle', **kwargs): """ Write a :class:`Lightcurve` object to file. Currently supported formats are * pickle (not recommended for long-term storage) * HDF5 * ASCII Parameters ---------- filename: str Path and file name for the output file. format\_: str Available options are 'pickle', 'hdf5', 'ascii' """ if format_ == 'ascii': io.write(np.array([self.time, self.counts]).T, filename, format_, fmt=["%s", "%s"]) elif format_ == 'pickle': io.write(self, filename, format_) elif format_ == 'hdf5': io.write(self, filename, format_) else: utils.simon("Format not understood.")
def _save_hdf5_object(object, filename): """ Save a class object in hdf5 format. Parameters ---------- object: class instance A class object whose attributes would be saved in a dictionary format. filename: str The file name to save to """ items = vars(object) attrs = [name for name in items] with h5py.File(filename, 'w') as hf: for attr in attrs: data = items[attr] # If data is a single number, store as an attribute. if _isattribute(data): if isinstance(data, np.longdouble): data = np.double(data) utils.simon("Casting data as double instead of longdouble.") hf.attrs[attr] = data # If data is a numpy array, create a dataset. else: if isinstance(data[0], np.longdouble): data = np.double(data) utils.simon("Casting data as double instead of longdouble.") hf.create_dataset(attr, data=data)
def write(input_, filename, format_='pickle', **kwargs): """ Pickle a class instance. For parameters depending on `format_`, see individual function definitions. Parameters ---------- object: a class instance filename: str name of the file to be created. format_: str pickle, hdf5, ascii ... """ if format_ == 'pickle': _save_pickle_object(input_, filename) elif format_ == 'hdf5': if _H5PY_INSTALLED: _save_hdf5_object(input_, filename) else: utils.simon('h5py not installed, using pickle instead to save object.') _save_pickle_object(input_, filename.split('.')[0]+'.pickle') elif format_ == 'ascii': _save_ascii_object(input_, filename, **kwargs) else: utils.simon('Format not understood.')
def _spectrum_function(self): lag_spec = np.zeros(len(self.energy_intervals)) lag_spec_err = np.zeros_like(lag_spec) for i, eint in enumerate(self.energy_intervals): base_lc, ref_lc = self._construct_lightcurves(eint) try: xspect = AveragedCrossspectrum(base_lc, ref_lc, segment_size=self.segment_size) except AssertionError as e: # Avoid assertions in AveragedCrossspectrum. simon("AssertionError: " + str(e)) else: good = (xspect.freq >= self.freq_interval[0]) & \ (xspect.freq < self.freq_interval[1]) lag, lag_err = xspect.time_lag() good_lag, good_lag_err = lag[good], lag_err[good] coh, coh_err = xspect.coherence() lag_spec[i] = np.mean(good_lag) coh_check = coh > 1.2 / (1 + 0.2 * xspect.m) if not np.all(coh_check[good]): simon("Coherence is not ideal over the specified energy " "range. Lag values and uncertainties might be " "underestimated. See Epitropakis and Papadakis, " "A\&A 591, 1113, 2016") # Root squared sum of errors of the spectrum # Verified! lag_spec_err[i] = \ np.sqrt(np.sum(good_lag_err**2) / len(good_lag)) return lag_spec, lag_spec_err
def _spectrum_function(self): rms_spec = np.zeros(len(self.energy_intervals)) rms_spec_err = np.zeros_like(rms_spec) for i, eint in enumerate(self.energy_intervals): base_lc, ref_lc = self._construct_lightcurves(eint, exclude=False) try: xspect = AveragedCrossspectrum(base_lc, ref_lc, segment_size=self.segment_size, norm='frac') except AssertionError as e: # Avoid "Mean count rate is <= 0. Something went wrong" assertion. simon("AssertionError: " + str(e)) else: good = (xspect.freq >= self.freq_interval[0]) & \ (xspect.freq < self.freq_interval[1]) rms_spec[i] = np.sqrt(np.sum(xspect.power[good] * xspect.df)) # Root squared sum of errors of the spectrum root_sq_err_sum = \ np.sqrt(np.sum((xspect.power_err[good] * xspect.df) ** 2)) # But the rms is the squared root. So, # Error propagation rms_spec_err[i] = 1 / (2 * rms_spec[i]) * root_sq_err_sum return rms_spec, rms_spec_err
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 _spectrum_function(self): rms_spec = np.zeros(len(self.energy_intervals)) rms_spec_err = np.zeros_like(rms_spec) for i, eint in enumerate(self.energy_intervals): base_lc, ref_lc = self._construct_lightcurves(eint, exclude=False) try: xspect = AveragedCrossspectrum(base_lc, ref_lc, segment_size=self.segment_size, norm='frac') except AssertionError as e: # Avoid "Mean count rate is <= 0. Something went wrong" assertion. simon("AssertionError: " + str(e)) else: good = (xspect.freq >= self.freq_interval[0]) & \ (xspect.freq < self.freq_interval[1]) rms_spec[i] = np.sqrt(np.sum(xspect.power[good]*xspect.df)) # Root squared sum of errors of the spectrum root_sq_err_sum = np.sqrt(np.sum((xspect.power_err[good]*xspect.df)**2)) # But the rms is the squared root. So, # Error propagation rms_spec_err[i] = 1 / (2 * rms_spec[i]) * root_sq_err_sum return rms_spec, rms_spec_err
def _construct_energy_covar(self, energy_events, energy_covar, xs_var=None): """Form the actual output covaraince dictionary and array.""" self._init_energy_covar(energy_events, energy_covar) if not self.avg_covar: xs_var = dict() for energy in energy_covar.keys(): lc, lc_ref = self._create_lc_and_lc_ref(energy, energy_events) covar = self._compute_covariance(lc, lc_ref) energy_covar[energy] = covar if not self.avg_covar: self.covar_error[energy] = self._calculate_covariance_error( lc, lc_ref) # Excess variance in ref band xs_var[energy] = self._calculate_excess_variance(lc_ref) for key, value in energy_covar.items(): if not xs_var[key] > 0: utils.simon("The excess variance in the reference band is " "negative. This implies that the reference " "band was badly chosen. Beware that the " "covariance spectra will have NaNs!") if not self.avg_covar: self.unnorm_covar = np.vstack(energy_covar.items()) energy_covar[key] = value / (xs_var[key])**0.5 self.covar = np.vstack(energy_covar.items()) self.covar_error = np.vstack(self.covar_error.items())
def read(filename, format_='pickle', **kwargs): """ Return a pickled class instance. Parameters ---------- filename: str name of the file to be retrieved. format_: str pickle, hdf5, ascii ... """ if format_ == 'pickle': return _retrieve_pickle_object(filename) elif format_ == 'hdf5': if _H5PY_INSTALLED: return _retrieve_hdf5_object(filename) else: utils.simon('h5py not installed, cannot read an hdf5 object.') elif format_ == 'ascii': return _retrieve_ascii_object(filename, **kwargs) else: utils.simon('Format not understood.')
def _spectrum_function(self): lag_spec = np.zeros(len(self.energy_intervals)) lag_spec_err = np.zeros_like(lag_spec) for i, eint in enumerate(self.energy_intervals): base_lc, ref_lc = self._construct_lightcurves(eint) xspect = AveragedCrossspectrum(base_lc, ref_lc, segment_size=self.segment_size) good = (xspect.freq >= self.freq_interval[0]) & \ (xspect.freq < self.freq_interval[1]) lag, lag_err = xspect.time_lag() good_lag, good_lag_err = lag[good], lag_err[good] coh, coh_err = xspect.coherence() lag_spec[i] = np.mean(good_lag) coh_check = coh > 1.2 / (1 + 0.2 * xspect.m) if not np.all(coh_check[good]): simon("Coherence is not ideal over the specified energy range." " Lag values and uncertainties might be underestimated. " "See Epitropakis and Papadakis, A\&A 591, 1113, 2016") # Root squared sum of errors of the spectrum # Verified! lag_spec_err[i] = np.sqrt(np.sum(good_lag_err**2) / len(good_lag)) return lag_spec, lag_spec_err
def savefig(filename, **kwargs): """ Save a figure plotted by Matplotlib. Note : This function is supposed to be used after the ``plot`` function. Otherwise it will save a blank image with no plot. Parameters ---------- filename : str The name of the image file. Extension must be specified in the file name. For example filename with `.png` extension will give a rasterized image while `.pdf` extension will give a vectorized output. kwargs : keyword arguments Keyword arguments to be passed to ``savefig`` function of ``matplotlib.pyplot``. For example use `bbox_inches='tight'` to remove the undesirable whitepace around the image. """ try: import matplotlib.pyplot as plt except ImportError: raise ImportError("Matplotlib required for savefig()") if not plt.fignum_exists(1): utils.simon("use ``plot`` function to plot the image first and " "then use ``savefig`` to save the figure.") plt.savefig(filename, **kwargs)
def savefig(filename, **kwargs): """ Save a figure plotted by ``matplotlib``. Note : This function is supposed to be used after the ``plot`` function. Otherwise it will save a blank image with no plot. Parameters ---------- filename : str The name of the image file. Extension must be specified in the file name. For example filename with `.png` extension will give a rasterized image while ``.pdf`` extension will give a vectorized output. kwargs : keyword arguments Keyword arguments to be passed to ``savefig`` function of ``matplotlib.pyplot``. For example use `bbox_inches='tight'` to remove the undesirable whitepace around the image. """ try: import matplotlib.pyplot as plt except ImportError: raise ImportError("Matplotlib required for savefig()") if not plt.fignum_exists(1): utils.simon("use ``plot`` function to plot the image first and " "then use ``savefig`` to save the figure.") plt.savefig(filename, **kwargs)
def read(self, filename, format_='pickle'): """ Imports LightCurve object. Parameters ---------- filename: str Name of the LightCurve object to be read. format\_: str Available options are 'pickle', 'hdf5', 'ascii' Returns -------- If format\_ is 'ascii': astropy.table is returned. If format\_ is 'hdf5': dictionary with key-value pairs is returned. If format\_ is 'pickle': class object is set. """ if format_ == 'ascii' or format_ == 'hdf5': return io.read(filename, format_) elif format_ == 'pickle': self = io.read(filename, format_) else: utils.simon("Format not understood.")
def write(self, filename, format_='pickle', **kwargs): """ Exports LightCurve object. Parameters ---------- filename: str Name of the LightCurve object to be created. format\_: str Available options are 'pickle', 'hdf5', 'ascii' """ if format_ == 'ascii': io.write(np.array([self.time, self.counts]).T, filename, format_, fmt=["%s", "%s"]) elif format_ == 'pickle': io.write(self, filename, format_) elif format_ == 'hdf5': io.write(self, filename, format_) else: utils.simon("Format not understood.")
def _construct_covar(self): """ Helper method to construct the covariance attribute and fill it with values. """ self.avg_covar = True start_time = self.lcs[0].time[0] covar = np.zeros(len(self.lcs)) covar_err = np.zeros(len(self.lcs)) xs_var = np.zeros(len(self.lcs)) for i in range(len(self.lcs)): lc = self.lcs[i] if np.size(self.ref_band_lcs) == 1: lc_ref = self.ref_band_lcs else: lc_ref = self.ref_band_lcs[i] tstart = start_time tend = start_time + self.segment_size cv = 0.0 cv_err = 0.0 xs = 0.0 self.nbins = int((tend - tstart)/self.segment_size) for k in range(self.nbins): start_ind = lc.time.searchsorted(tstart) end_ind = lc.time.searchsorted(tend) lc_seg = lc.truncate(start=start_ind, stop=end_ind) lc_ref_seg = lc_ref.truncate(start=start_ind, stop=end_ind) cv += self._compute_covariance(lc_seg, lc_ref_seg) cv_err += self._calculate_covariance_error(lc_seg, lc_ref_seg) xs += self._calculate_excess_variance(lc_ref_seg) if not xs > 0: utils.simon("The excess variance in the reference band is " "negative. This implies that the reference " "band was badly chosen. Beware that the " "covariance spectra will have NaNs!") tstart += self.segment_size tend += self.segment_size covar[i] = cv/self.nbins covar_err[i] = cv_err/self.nbins xs_var[i] = xs/self.nbins self.unnorm_covar = covar energy_covar = covar / xs_var**0.5 self.covar = energy_covar self.covar_error = covar_err return
def coherence(self): """ Compute an averaged Coherence function of cross spectrum by computing coherence function of each segment and averaging them. The return type is a tuple with first element as the coherence function and the second element as the corresponding uncertainty[1] associated with it. Note : The uncertainty in coherence function is strictly valid for Gaussian statistics only. Returns ------- tuple : tuple of np.ndarray Tuple of coherence function and uncertainty. References ---------- .. [1] http://iopscience.iop.org/article/10.1086/310430/pdf """ if self.m < 50: simon("Number of segments used in averaging is " "significantly low. The result might not follow the " "expected statistical distributions.") # Calculate average coherence unnorm_power_avg = np.zeros_like(self.cs_all[0].unnorm_power) for cs in self.cs_all: unnorm_power_avg += cs.unnorm_power unnorm_power_avg /= self.m num = np.absolute(unnorm_power_avg)**2 # this computes the averaged power spectrum, but using the # cross spectrum code to avoid circular imports aps1 = AveragedCrossspectrum(self.lc1, self.lc1, segment_size=self.segment_size) aps2 = AveragedCrossspectrum(self.lc2, self.lc2, segment_size=self.segment_size) unnorm_powers_avg_1 = np.zeros_like(aps1.cs_all[0].unnorm_power.real) for ps in aps1.cs_all: unnorm_powers_avg_1 += ps.unnorm_power.real unnorm_powers_avg_1 /= aps1.m unnorm_powers_avg_2 = np.zeros_like(aps2.cs_all[0].unnorm_power.real) for ps in aps2.cs_all: unnorm_powers_avg_2 += ps.unnorm_power.real unnorm_powers_avg_2 /= aps2.m coh = num / (unnorm_powers_avg_1 * unnorm_powers_avg_2) # Calculate uncertainty uncertainty = (2**0.5 * coh * (1 - coh)) / (np.abs(coh) * self.m**0.5) return (coh, uncertainty)
def _construct_covar(self): """ Helper method to construct the covariance attribute and fill it with values. """ self.avg_covar = True start_time = self.lcs[0].time[0] covar = np.zeros(len(self.lcs)) covar_err = np.zeros(len(self.lcs)) xs_var = np.zeros(len(self.lcs)) for i in range(len(self.lcs)): lc = self.lcs[i] if np.size(self.ref_band_lcs) == 1: lc_ref = self.ref_band_lcs else: lc_ref = self.ref_band_lcs[i] tstart = start_time tend = start_time + self.segment_size cv = 0.0 cv_err = 0.0 xs = 0.0 self.nbins = int((tend - tstart) / self.segment_size) for k in range(self.nbins): start_ind = lc.time.searchsorted(tstart) end_ind = lc.time.searchsorted(tend) lc_seg = lc.truncate(start=start_ind, stop=end_ind) lc_ref_seg = lc_ref.truncate(start=start_ind, stop=end_ind) cv += self._compute_covariance(lc_seg, lc_ref_seg) cv_err += self._calculate_covariance_error(lc_seg, lc_ref_seg) xs += self._calculate_excess_variance(lc_ref_seg) if not xs > 0: utils.simon("The excess variance in the reference band is " "negative. This implies that the reference " "band was badly chosen. Beware that the " "covariance spectra will have NaNs!") tstart += self.segment_size tend += self.segment_size covar[i] = cv / self.nbins covar_err[i] = cv_err / self.nbins xs_var[i] = xs / self.nbins self.unnorm_covar = covar energy_covar = covar / xs_var**0.5 self.covar = energy_covar self.covar_error = covar_err return
def classical_pvalue(power, nspec): """ Compute the probability of detecting the current power under the assumption that there is no periodic oscillation in the data. This computes the single-trial p-value that the power was observed under the null hypothesis that there is no signal in the data. Important: the underlying assumptions that make this calculation valid are: (1) the powers in the power spectrum follow a chi-square distribution (2) the power spectrum is normalized according to Leahy (1984), such that the powers have a mean of 2 and a variance of 4 (3) there is only white noise in the light curve. That is, there is no aperiodic variability that would change the overall shape of the power spectrum. Also note that the p-value is for a *single trial*, i.e. the power currently being tested. If more than one power or more than one power spectrum are being tested, the resulting p-value must be corrected for the number of trials (Bonferroni correction). Mathematical formulation in Groth, 1975. Original implementation in IDL by Anna L. Watts. Parameters ---------- power : float The squared Fourier amplitude of a spectrum to be evaluated nspec : int The number of spectra or frequency bins averaged in `power`. This matters because averaging spectra or frequency bins increases the signal-to-noise ratio, i.e. makes the statistical distributions of the noise narrower, such that a smaller power might be very significant in averaged spectra even though it would not be in a single power spectrum. """ assert np.isfinite(power), "power must be a finite floating point number!" assert power > 0, "power must be a positive real number!" assert np.isfinite(nspec), "nspec must be a finite integer number" assert nspec >= 1, "nspec must be larger or equal to 1" assert np.isclose(nspec % 1, 0), "nspec must be an integer number!" ## If the power is really big, it's safe to say it's significant, ## and the p-value will be nearly zero if power * nspec > 30000: simon("Probability of no signal too miniscule to calculate.") return 0.0 else: pval = _pavnosigfun(power, nspec) return pval
def classical_pvalue(power, nspec): """ Compute the probability of detecting the current power under the assumption that there is no periodic oscillation in the data. This computes the single-trial p-value that the power was observed under the null hypothesis that there is no signal in the data. Important: the underlying assumptions that make this calculation valid are: (1) the powers in the power spectrum follow a chi-square distribution (2) the power spectrum is normalized according to Leahy (1984), such that the powers have a mean of 2 and a variance of 4 (3) there is only white noise in the light curve. That is, there is no aperiodic variability that would change the overall shape of the power spectrum. Also note that the p-value is for a *single trial*, i.e. the power currently being tested. If more than one power or more than one power spectrum are being tested, the resulting p-value must be corrected for the number of trials (Bonferroni correction). Mathematical formulation in Groth, 1975. Original implementation in IDL by Anna L. Watts. Parameters ---------- power : float The squared Fourier amplitude of a spectrum to be evaluated nspec : int The number of spectra or frequency bins averaged in `power`. This matters because averaging spectra or frequency bins increases the signal-to-noise ratio, i.e. makes the statistical distributions of the noise narrower, such that a smaller power might be very significant in averaged spectra even though it would not be in a single power spectrum. """ assert np.isfinite(power), "power must be a finite floating point number!" assert power > 0.0, "power must be a positive real number!" assert np.isfinite(nspec), "nspec must be a finite integer number" assert nspec >= 1, "nspec must be larger or equal to 1" assert np.isclose(nspec % 1, 0), "nspec must be an integer number!" ## If the power is really big, it's safe to say it's significant, ## and the p-value will be nearly zero if power*nspec > 30000: simon("Probability of no signal too miniscule to calculate.") return 0.0 else: pval = _pavnosigfun(power, nspec) return pval
def coherence(self): """ Compute an averaged Coherence function of cross spectrum by computing coherence function of each segment and averaging them. The return type is a tuple with first element as the coherence function and the second element as the corresponding uncertainty[1] associated with it. Note : The uncertainty in coherence function is strictly valid for Gaussian statistics only. Returns ------- tuple : tuple of np.ndarray Tuple of coherence function and uncertainty. References ---------- .. [1] http://iopscience.iop.org/article/10.1086/310430/pdf """ if self.m < 50: utils.simon("Number of segments used in averaging is " "significantly low. The result might not follow the " "expected statistical distributions.") # Calculate average coherence unnorm_power_avg = np.zeros_like(self.cs_all[0].unnorm_power) for cs in self.cs_all: unnorm_power_avg += cs.unnorm_power unnorm_power_avg /= self.m num = np.abs(unnorm_power_avg)**2 # this computes the averaged power spectrum, but using the # cross spectrum code to avoid circular imports aps1 = AveragedCrossspectrum(self.lc1, self.lc1, segment_size=self.segment_size) aps2 = AveragedCrossspectrum(self.lc2, self.lc2, segment_size=self.segment_size) unnorm_powers_avg_1 = np.zeros_like(aps1.cs_all[0].unnorm_power) for ps in aps1.cs_all: unnorm_powers_avg_1 += ps.unnorm_power unnorm_powers_avg_1 /= aps1.m unnorm_powers_avg_2 = np.zeros_like(aps2.cs_all[0].unnorm_power) for ps in aps2.cs_all: unnorm_powers_avg_2 += ps.unnorm_power unnorm_powers_avg_2 /= aps2.m coh = num / (unnorm_powers_avg_1 * unnorm_powers_avg_2) # Calculate uncertainty uncertainty = (2**0.5 * coh * (1 - coh)) / (np.abs(coh) * self.m**0.5) return (coh, uncertainty)
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 coherence(self): """Averaged Coherence function. Coherence is defined in Vaughan and Nowak, 1996 [vaughan-1996]. It is a Fourier frequency dependent measure of the linear correlation between time series measured simultaneously in two energy channels. Compute an averaged Coherence function of cross spectrum by computing coherence function of each segment and averaging them. The return type is a tuple with first element as the coherence function and the second element as the corresponding uncertainty associated with it. Note : The uncertainty in coherence function is strictly valid for Gaussian \ statistics only. Returns ------- (coh, uncertainty) : tuple of np.ndarray Tuple comprising the coherence function and uncertainty. References ---------- .. [vaughan-1996] http://iopscience.iop.org/article/10.1086/310430/pdf """ if np.any(self.m < 50): simon("Number of segments used in averaging is " "significantly low. The result might not follow the " "expected statistical distributions.") # Calculate average coherence unnorm_power_avg = self.unnorm_power num = np.absolute(unnorm_power_avg)**2 # The normalization was 'none'! unnorm_powers_avg_1 = self.pds1.power.real unnorm_powers_avg_2 = self.pds2.power.real coh = num / (unnorm_powers_avg_1 * unnorm_powers_avg_2) coh[~np.isfinite(coh)] = 0.0 # Calculate uncertainty uncertainty = \ (2 ** 0.5 * coh * (1 - coh)) / (np.abs(coh) * self.m ** 0.5) uncertainty[coh == 0] = 0.0 return (coh, uncertainty)
def __init__( self, events, freq_interval, energy_spec, ref_band=None, bin_time=1, use_pi=False, segment_size=None, events2=None, return_complex=False, ): self.events1 = events self.events2 = assign_value_if_none(events2, events) self._analyze_inputs() # This will be set to True in ComplexCovariance self.return_complex = return_complex self.freq_interval = freq_interval self.use_pi = use_pi self.bin_time = bin_time if isinstance(energy_spec, tuple): energies = _decode_energy_specification(energy_spec) else: energies = np.asarray(energy_spec) self.energy_intervals = list(zip(energies[0:-1], energies[1:])) self.ref_band = np.asarray(assign_value_if_none(ref_band, [0, np.inf])) if len(self.ref_band.shape) <= 1: self.ref_band = np.asarray([self.ref_band]) self.segment_size = self.delta_nu = None if segment_size is not None: self.segment_size = segment_size self.delta_nu = 1 / self.segment_size self._create_empty_spectrum() if len(events.time) == 0: simon("There are no events in your event list!" + "Can't make a spectrum!") else: self._spectrum_function()
def coherence(self): """ Compute an averaged Coherence function of cross spectrum by computing coherence function of each segment and averaging them. The return type is a tuple with first element as the coherence function and the second element as the corresponding uncertainty[1] associated with it. Note : The uncertainty in coherence function is strictly valid for Gaussian statistics only. Returns ------- tuple : tuple of np.ndarray Tuple of coherence function and uncertainty. References ---------- .. [1] http://iopscience.iop.org/article/10.1086/310430/pdf """ if np.any(self.m < 50): simon("Number of segments used in averaging is " "significantly low. The result might not follow the " "expected statistical distributions.") # Calculate average coherence unnorm_power_avg = np.zeros_like(self.cs_all[0].unnorm_power) for cs in self.cs_all: unnorm_power_avg += cs.unnorm_power unnorm_power_avg /= self.m num = np.absolute(unnorm_power_avg)**2 # The normalization was 'none'! unnorm_powers_avg_1 = self.pds1.power.real unnorm_powers_avg_2 = self.pds2.power.real coh = num / (unnorm_powers_avg_1 * unnorm_powers_avg_2) # Calculate uncertainty uncertainty = (2**0.5 * coh * (1 - coh)) / (np.abs(coh) * self.m**0.5) return (coh, uncertainty)
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 _construct_covar(self): """ Helper method to construct the covariance attribute and fill it with values. """ self.avg_covar = False covar = np.zeros(len(self.lcs)) covar_err = np.zeros(len(self.lcs)) xs_var = np.zeros(len(self.lcs)) for i in range(len(self.lcs)): lc = self.lcs[i] if np.size(self.ref_band_lcs) == 1 or isinstance( self.ref_band_lcs, Lightcurve): lc_ref = self.ref_band_lcs else: lc_ref = self.ref_band_lcs[i] cv = self._compute_covariance(lc, lc_ref) cv_err = self._calculate_covariance_error(lc, lc_ref) covar[i] = cv covar_err[i] = cv_err xs = self._calculate_excess_variance(lc_ref) if not xs > 0: utils.simon("The excess variance in the reference band is " "negative. This implies that the reference " "band was badly chosen. Beware that the " "covariance spectra will have NaNs!") xs_var[i] = xs self.unnorm_covar = covar energy_covar = covar / xs_var**0.5 self.covar = energy_covar self.covar_error = covar_err return
def _construct_covar(self): """ Helper method to construct the covariance attribute and fill it with values. """ self.avg_covar = False covar = np.zeros(len(self.lcs)) covar_err = np.zeros(len(self.lcs)) xs_var = np.zeros(len(self.lcs)) for i in range(len(self.lcs)): lc = self.lcs[i] if np.size(self.ref_band_lcs) == 1 or isinstance(self.ref_band_lcs, Lightcurve): lc_ref = self.ref_band_lcs else: lc_ref = self.ref_band_lcs[i] cv = self._compute_covariance(lc, lc_ref) cv_err = self._calculate_covariance_error(lc, lc_ref) covar[i] = cv covar_err[i] = cv_err xs = self._calculate_excess_variance(lc_ref) if not xs > 0: utils.simon("The excess variance in the reference band is " "negative. This implies that the reference " "band was badly chosen. Beware that the " "covariance spectra will have NaNs!") xs_var[i] = xs self.unnorm_covar = covar energy_covar = covar / xs_var**0.5 self.covar = energy_covar self.covar_error = covar_err return
def coherence(self): """ Compute an averaged Coherence function of cross spectrum by computing coherence function of each segment and averaging them. The return type is a tuple with first element as the coherence function and the second element as the corresponding uncertainty[1] associated with it. Note : The uncertainty in coherence function is strictly valid for Gaussian statistics only. Returns ------- tuple : tuple of np.ndarray Tuple of coherence function and uncertainty. References ---------- .. [1] http://iopscience.iop.org/article/10.1086/310430/pdf """ if np.any(self.m < 50): simon("Number of segments used in averaging is " "significantly low. The result might not follow the " "expected statistical distributions.") # Calculate average coherence unnorm_power_avg = self.unnorm_power num = np.absolute(unnorm_power_avg) ** 2 # The normalization was 'none'! unnorm_powers_avg_1 = self.pds1.power.real unnorm_powers_avg_2 = self.pds2.power.real coh = num / (unnorm_powers_avg_1 * unnorm_powers_avg_2) # Calculate uncertainty uncertainty = (2 ** 0.5 * coh * (1 - coh)) / ( np.abs(coh) * self.m ** 0.5) return (coh, uncertainty)
def read(filename, format_='pickle', **kwargs): """ Return a saved class instance. Parameters ---------- filename: str The name of the file to be retrieved. format_: str The format used to store file. Supported formats are pickle, hdf5, ascii or fits. Returns ------- data : {``object`` | ``astropy.table`` | ``dict``} * If ``format_`` is ``pickle``, an object is returned. * If ``format_`` is ``ascii``, `astropy.table` object is returned. * If ``format_`` is ``hdf5`` or 'fits``, a dictionary object is returned. """ if format_ == 'pickle': return _retrieve_pickle_object(filename) elif format_ == 'hdf5': if _H5PY_INSTALLED: return _retrieve_hdf5_object(filename) else: utils.simon('h5py not installed, cannot read an' 'hdf5 object.') elif format_ == 'ascii': return _retrieve_ascii_object(filename, **kwargs) elif format_ == 'fits': return _retrieve_fits_object(filename, **kwargs) else: utils.simon('Format not understood.')
def _init_vars(self, event_list, dt, band_interest, ref_band_interest, std): """ Check for consistency with input variables and declare public ones. """ if not np.all(np.diff(event_list, axis=0).T[0] >= 0): utils.simon("The event list must be sorted with respect to " "times of arrivals.") event_list = event_list[event_list[:, 0].argsort()] self.event_list = event_list self.event_list_T = event_list.T self._init_special_vars() if ref_band_interest is None: ref_band_interest = (self.min_energy, self.max_energy) assert type(ref_band_interest) in (list, tuple), "Ref Band interest " \ "should be either " \ "tuple or list." assert len(ref_band_interest) == 2, "Band interest should be a tuple" \ " with min and max energy value " \ "for the reference band." self.ref_band_interest = ref_band_interest if band_interest is not None: for element in list(band_interest): assert type(element) in (list, tuple), \ "band_interest should be iterable of either tuple or list." assert len(element) == 2, "Band interest should be a tuple " \ "with min and max energy values." self.band_interest = band_interest self.dt = dt self.std = std
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 read(filename, format_='pickle', **kwargs): """ Return a pickled class instance. Parameters ---------- filename: str The name of the file to be retrieved. format_: str The format used to store file. Supported formats are pickle, hdf5, ascii or fits. Returns ------- If format_ is 'pickle', a class object is returned. If format_ is 'ascii', astropy.table object is returned. If format_ is 'hdf5' or 'fits', a dictionary object is returned. """ if format_ == 'pickle': return _retrieve_pickle_object(filename) elif format_ == 'hdf5': if _H5PY_INSTALLED: return _retrieve_hdf5_object(filename) else: utils.simon('h5py not installed, cannot read an' \ 'hdf5 object.') elif format_ == 'ascii': return _retrieve_ascii_object(filename, **kwargs) elif format_ == 'fits': return _retrieve_fits_object(filename, **kwargs) else: utils.simon('Format not understood.')
def write(input_, filename, format_='pickle', **kwargs): """ Pickle a class instance. For parameters depending on ``format_``, see individual function definitions. Parameters ---------- object: a class instance The object to be stored filename: str The name of the file to be created format_: str The format in which to store file. Formats supported are ``pickle``, ``hdf5``, ``ascii`` or ``fits`` """ if format_ == 'pickle': _save_pickle_object(input_, filename) elif format_ == 'hdf5': if _H5PY_INSTALLED: _save_hdf5_object(input_, filename) else: utils.simon('h5py not installed, using pickle instead' 'to save object.') _save_pickle_object(input_, filename.split('.')[0] + '.pickle') elif format_ == 'ascii': _save_ascii_object(input_, filename, **kwargs) elif format_ == 'fits': _save_fits_object(input_, filename, **kwargs) else: utils.simon('Format not understood.')
def _construct_energy_covar(self, energy_events, energy_covar, xs_var=None): """Form the actual output covariance dictionary and array.""" self._init_energy_covar(energy_events, energy_covar) if not self.avg_covar: xs_var = dict() for energy in energy_covar.keys(): lc, lc_ref = self._create_lc_and_lc_ref(energy, energy_events) covar = self._compute_covariance(lc, lc_ref) energy_covar[energy] = covar if not self.avg_covar: self.covar_error[energy] = self._calculate_covariance_error( lc, lc_ref) # Excess variance in ref band xs_var[energy] = self._calculate_excess_variance(lc_ref) for key, value in energy_covar.items(): if not xs_var[key] > 0: utils.simon("The excess variance in the reference band is " "negative. This implies that the reference " "band was badly chosen. Beware that the " "covariance spectra will have NaNs!") if not self.avg_covar: self.unnorm_covar = np.vstack(energy_covar.items()) energy_covar[key] = value / (xs_var[key])**0.5 self.covar = np.vstack(energy_covar.items()) self.covar_error = np.vstack(self.covar_error.items())
def __init__(self, events, freq_interval, energy_spec, ref_band=None, bin_time=1, use_pi=False, segment_size=None, events2=None): self.events1 = events self.events2 = assign_value_if_none(events2, events) self.freq_interval = freq_interval self.use_pi = use_pi self.bin_time = bin_time if isinstance(energy_spec, tuple): energies = _decode_energy_specification(energy_spec) else: energies = np.asarray(energy_spec) self.energy_intervals = list(zip(energies[0:-1], energies[1:])) self.ref_band = np.asarray(assign_value_if_none(ref_band, [0, np.inf])) if len(self.ref_band.shape) <= 1: self.ref_band = np.asarray([self.ref_band]) self.segment_size = segment_size if len(events.time) == 0: simon("There are no events in your event list!" + "Can't make a spectrum!") self.spectrum = 0 self.spectrum_error = 0 else: self.spectrum, self.spectrum_error = self._spectrum_function()
def plot(self, witherrors=False, labels=None, axis=None, title=None, marker='-', save=False, filename=None): """ Plot the Lightcurve using Matplotlib. Plot the Lightcurve object on a graph ``self.time`` on x-axis and ``self.counts`` on y-axis with ``self.counts_err`` optionaly as error bars. Parameters ---------- witherrors: boolean, default False Whether to plot the Lightcurve with errorbars or not labels : iterable, default None A list of tuple with xlabel and ylabel as strings. axis : list, tuple, string, default None Parameter to set axis properties of Matplotlib figure. For example it can be a list like ``[xmin, xmax, ymin, ymax]`` or any other acceptable argument for `matplotlib.pyplot.axis()` function. title : str, default None The title of the plot. marker : str, default '-' Line style and color of the plot. Line styles and colors are combined in a single format string, as in ``'bo'`` for blue circles. See `matplotlib.pyplot.plot` for more options. save : boolean, optional (default=False) If True, save the figure with specified filename. filename : str File name of the image to save. Depends on the boolean ``save``. """ try: import matplotlib.pyplot as plt except ImportError: raise ImportError("Matplotlib required for plot()") fig = plt.figure() if witherrors: fig = plt.errorbar(self.time, self.counts, yerr=self.counts_err, fmt=marker) else: fig = plt.plot(self.time, self.counts, marker) if labels is not None: try: plt.xlabel(labels[0]) plt.ylabel(labels[1]) except TypeError: utils.simon("``labels`` must be either a list or tuple with " "x and y labels.") raise except IndexError: utils.simon("``labels`` must have two labels for x and y " "axes.") # Not raising here because in case of len(labels)==1, only # x-axis will be labelled. if axis is not None: plt.axis(axis) if title is not None: plt.title(title) if save: if filename is None: plt.savefig('out.png') else: plt.savefig(filename)
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 __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. 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 are 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] 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 __init__(self, data, dt=None, band_interest=None, ref_band_interest=None, std=None): self.dt = dt self.std = std # check whether data is an EventList object: if isinstance(data, EventList): data = np.vstack([data.time, data.energy]).T # check whether the data contains a list of Lightcurve objects if isinstance(data[0], Lightcurve): self.use_lc = True self.lcs = data else: self.use_lc = False # if band_interest is None, extract the energy bins and make an array # with the lower and upper bounds of the energy bins if not band_interest: if not self.use_lc: self._create_band_interest(data) else: self.band_interest = np.vstack([np.arange(len(data)), np.arange(1, len(data)+1, 1)]).T else: if np.size(band_interest) < 2: raise ValueError('band_interest must contain at least 2 values ' '(minimum and maximum values for each band) ' 'and be a 2D array!') self.band_interest = np.atleast_2d(band_interest) if self.use_lc is False and not dt: raise ValueError("If the input data is event data, the dt keyword " "must be set and supply a time resolution for " "creating light curves!") # if we don't have light curves already, make them: if not self.use_lc: if not np.all(np.diff(data, axis=0).T[0] >= 0): utils.simon("The event list must be sorted with respect to " "times of arrivals.") data = data[data[:, 0].argsort()] self.lcs = self._make_lightcurves(data) # check whether band of interest contains a Lightcurve object: if np.size(ref_band_interest) == 1 or isinstance(ref_band_interest, Lightcurve): if isinstance(ref_band_interest, Lightcurve): self.ref_band_lcs = ref_band_interest # ref_band_interest must either be a Lightcurve, or must have # multiple entries elif ref_band_interest is None: if self.use_lc: self.ref_band_lcs = \ self._make_reference_bands_from_lightcurves(ref_band_interest) else: self.ref_band_lcs = \ self._make_reference_bands_from_event_data(data) else: raise ValueError("ref_band_interest must contain either " "a Lightcurve object, a list of Lightcurve " "objects or a tuple of length 2.") else: # check whether ref_band_interest is a list of light curves if isinstance(ref_band_interest[0], Lightcurve): self.ref_band_lcs = ref_band_interest assert len(ref_band_interest) == len(self.lcs), "The list of " \ "reference light " \ "curves must have " \ "the same length as " \ "the list of light curves" \ "of interest." # if not, it must be a tuple, so we're going to make a list of light # curves else: if self.use_lc: self.ref_band_lcs = \ self._make_reference_bands_from_lightcurves(bounds= ref_band_interest) else: self.ref_band_lcs = \ self._make_reference_bands_from_event_data(data) self._construct_covar()
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 = [] # 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]] except IndexError: count1 = None try: count2 = other.counts[np.where(other.time == time)[0][0]] except IndexError: count2 = None if count1 is not None: if count2 is not None: # Average the overlapping counts new_counts.append((count1 + count2) / 2) else: new_counts.append(count1) else: new_counts.append(count2) new_counts = np.asarray(new_counts) gti = join_gtis(self.gti, other.gti) lc_new = Lightcurve(new_time, new_counts, gti=gti) return lc_new
def plot(self, labels=None, axis=None, title=None, marker='-', save=False, filename=None, ax=None): """ Plot the :class:`Crosscorrelation` as function using Matplotlib. Plot the Crosscorrelation object on a graph ``self.time_lags`` on x-axis and ``self.corr`` on y-axis Parameters ---------- labels : iterable, default ``None`` A list of tuple with ``xlabel`` and ``ylabel`` as strings. axis : list, tuple, string, default ``None`` Parameter to set axis properties of ``matplotlib`` figure. For example it can be a list like ``[xmin, xmax, ymin, ymax]`` or any other acceptable argument for ``matplotlib.pyplot.axis()`` function. title : str, default ``None`` The title of the plot. marker : str, default ``-`` Line style and color of the plot. Line styles and colors are combined in a single format string, as in ``'bo'`` for blue circles. See ``matplotlib.pyplot.plot`` for more options. save : boolean, optional (default=False) If True, save the figure with specified filename. filename : str File name of the image to save. Depends on the boolean ``save``. ax : ``matplotlib.Axes`` object An axes object to fill with the cross correlation plot. """ try: import matplotlib.pyplot as plt except ImportError: raise ImportError("Matplotlib required for plot()") if ax is None: fig, ax = plt.subplots(1, 1, figsize=(6, 4)) ax.plot(self.time_lags, self.corr, marker) if labels is not None: try: ax.set_xlabel(labels[0]) ax.set_ylabel(labels[1]) except TypeError: utils.simon("``labels`` must be either a list or tuple with " "x and y labels.") raise except IndexError: utils.simon("``labels`` must have two labels for x and y " "axes.") # Not raising here because in case of len(labels)==1, only # x-axis will be labelled. # axis is a tuple containing formatting information if axis is not None: ax.axis(axis) if title is not None: ax.set_title(title) if save: if filename is None: plt.savefig('corr.pdf', format="pdf") else: plt.savefig(filename) else: plt.show(block=False) return ax
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 __init__(self, data, dt=None, band_interest=None, ref_band_interest=None, std=None): """ Compute a covariance spectrum for the data. The input data can be either in event data or pre-made light curves. Event data can either be in the form of a numpy.ndarray with (time stamp, energy) pairs or a `stingray.events.EventList` object. If light curves are formed ahead of time, then a list of `Lightcurve` objects should be passed to the object, ideally one light curve for each band of interest. For the case where the data is input as a list of `Lightcurve` objects, the reference band(s) should either be (1) a single `Lightcurve` object, (2) a list of `Lightcurve` objects with the reference band for each band of interest pre-made, or (3) `None`, in which case reference bands will formed by combining all light curves *except* for the band of interest. In the case of event data, `band_interest` and `ref_band_interest` can be (multiple) pairs of energies, and the light curves for the bands of interest and reference bands will be produced dynamically. Parameters ---------- data : {numpy.ndarray | EventList object | list of Lightcurve objects} `data` contains the time series data, either in the form of a 2-D array of (time stamp, energy) pairs for event data, or as a list of light curves. Note : The event list must be in sorted order with respect to the times of arrivals. dt : float The time resolution of the Lightcurve formed from the energy bin. Only used if `data` is an event list. band_interest : {None, iterable of tuples} If None, all possible energy values will be assumed to be of interest, and a covariance spectrum in the highest resolution will be produced. Note; If the input is a list of Lightcurve objects, then the user may supply their energy values here, for construction of a reference band. ref_band_interest : {None, tuple, Lightcurve, list of Lightcurves} Defines the reference band to be used for comparison with the bands of interest. If None, all bands *except* the band of interest will be used for each band of interest, respectively. Alternatively, a tuple can be given for event list data, which will extract the reference band (always excluding the band of interest), or one may put in a single Lightcurve object to be used (the same for each band of interest) or a list of Lightcurve objects, one for each band of interest. std : float or np.array or list of numbers The term std is used to calculate the excess variance of a band. If std is set to None, default Poisson case is taken and the std is calculated as `mean(lc)**0.5`. In the case of a single float as input, the same is used as the standard deviation which is also used as the std. And if the std is an iterable of numbers, their mean is used for the same purpose. Attributes ---------- unnorm_covar : np.ndarray An array of arrays with mid point band_interest and their covariance. It is the array-form of the dictionary `energy_covar`. The covariance values are unnormalized. covar : np.ndarray Normalized covariance spectrum. covar_error : np.ndarray Errors of the normalized covariance spectrum. References ---------- [1] Wilkinson, T. and Uttley, P. (2009), Accretion disc variability in the hard state of black hole X-ray binaries. Monthly Notices of the Royal Astronomical Society, 397: 666–676. doi: 10.1111/j.1365-2966.2009.15008.x Examples -------- See https://github.com/StingraySoftware/notebooks repository for detailed notebooks on the code. """ self.dt = dt self.std = std # check whether data is an EventList object: if isinstance(data, EventList): data = np.vstack([data.time, data.energy]).T # check whether the data contains a list of Lightcurve objects if isinstance(data[0], Lightcurve): self.use_lc = True self.lcs = data else: self.use_lc = False # if band_interest is None, extract the energy bins and make an array # with the lower and upper bounds of the energy bins if not band_interest: if not self.use_lc: self._create_band_interest(data) else: self.band_interest = np.vstack([np.arange(len(data)), np.arange(1, len(data)+1, 1)]).T else: if np.size(band_interest) < 2: raise ValueError('band_interest must contain at least 2 values ' '(minimum and maximum values for each band) ' 'and be a 2D array!') self.band_interest = np.atleast_2d(band_interest) if self.use_lc is False and not dt: raise ValueError("If the input data is event data, the dt keyword " "must be set and supply a time resolution for " "creating light curves!") # if we don't have light curves already, make them: if not self.use_lc: if not np.all(np.diff(data, axis=0).T[0] >= 0): utils.simon("The event list must be sorted with respect to " "times of arrivals.") data = data[data[:, 0].argsort()] self.lcs = self._make_lightcurves(data) # check whether band of interest contains a Lightcurve object: if np.size(ref_band_interest) == 1 or isinstance(ref_band_interest, Lightcurve): if isinstance(ref_band_interest, Lightcurve): self.ref_band_lcs = ref_band_interest # ref_band_interest must either be a Lightcurve, or must have # multiple entries elif ref_band_interest is None: if self.use_lc: self.ref_band_lcs = \ self._make_reference_bands_from_lightcurves(ref_band_interest) else: self.ref_band_lcs = \ self._make_reference_bands_from_event_data(data) else: raise ValueError("ref_band_interest must contain either " "a Lightcurve object, a list of Lightcurve " "objects or a tuple of length 2.") else: # check whether ref_band_interest is a list of light curves if isinstance(ref_band_interest[0], Lightcurve): self.ref_band_lcs = ref_band_interest assert len(ref_band_interest) == len(self.lcs), "The list of " \ "reference light " \ "curves must have " \ "the same length as " \ "the list of light curves" \ "of interest." # if not, it must be a tuple, so we're going to make a list of light # curves else: if self.use_lc: self.ref_band_lcs = \ self._make_reference_bands_from_lightcurves(bounds= ref_band_interest) else: self.ref_band_lcs = \ self._make_reference_bands_from_event_data(data) self._construct_covar()
def __init__(self, time, counts, input_counts=True): """ 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. Attributes ---------- time: numpy.ndarray The array of midpoints 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`. ncounts: 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. """ assert np.all(np.isfinite(time)), "There are inf or NaN values in " \ "your time array!" assert np.all(np.isfinite(counts)), "There are inf or NaN values in " \ "your counts array!" assert len(time) == len(counts), "time are counts array are not " \ "of the same length!" assert len(time) > 1, "A single or no data points can not create " \ "a lightcurve!" self.time = np.asarray(time) self.dt = time[1] - time[0] 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.ncounts = 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