def timelag_map(self, channel_a, channel_b, **kwargs): """ Construct map of timelag values that maximize the cross-correlation between two channels in each pixel of an AIA map. """ cc = self.cross_correlation(channel_a, channel_b, **kwargs) bounds = kwargs.get('timelag_bounds', None) if bounds is not None: indices, = np.where( np.logical_and(self.timelags >= bounds[0], self.timelags <= bounds[1])) start = indices[0] stop = indices[-1] + 1 else: start = 0 stop = self.timelags.shape[0] + 1 i_max_cc = cc[start:stop, :, :].argmax(axis=0) max_timelag = self.timelags[start:stop][i_max_cc] meta = self[channel_a].maps[0].meta.copy() del meta['instrume'] del meta['t_obs'] del meta['wavelnth'] meta['bunit'] = 's' meta['comment'] = f'{channel_a}-{channel_b} timelag' plot_settings = { 'cmap': 'RdBu_r', 'vmin': self.timelags[start:stop].value.min(), 'vmax': self.timelags[start:stop].value.max() } plot_settings.update(kwargs.get('plot_settings', {})) timelag_map = GenericMap(max_timelag, meta.copy(), plot_settings=plot_settings.copy()) return timelag_map
def peak_cross_correlation_map(self, channel_a, channel_b, **kwargs): """ Construct map of peak cross-correlation between two channels in each pixel of an AIA map. """ cc = self.cross_correlation(channel_a, channel_b, **kwargs) bounds = kwargs.get('timelag_bounds', None) if bounds is not None: indices, = np.where( np.logical_and(self.timelags >= bounds[0], self.timelags <= bounds[1])) start = indices[0] stop = indices[-1] + 1 else: start = 0 stop = self.timelags.shape[0] + 1 max_cc = cc[start:stop, :, :].max(axis=0) meta = self[channel_a].maps[0].meta.copy() del meta['instrume'] del meta['t_obs'] del meta['wavelnth'] meta['bunit'] = '' meta['comment'] = f'{channel_a}-{channel_b} cross-correlation' plot_settings = {'cmap': 'plasma'} plot_settings.update(kwargs.get('plot_settings', {})) correlation_map = GenericMap(max_cc, meta, plot_settings=plot_settings) return correlation_map
def make_slope_map(self, temperature_bounds=None, em_threshold=None, rsquared_tolerance=0.5): """ Calculate emission measure slope :math:`a` in each pixel Create map of emission measure slopes by fitting :math:`\mathrm{EM}\sim T^a` for a given temperature range. Only those pixels for which the minimum :math:`\mathrm{EM}` across all temperature bins is above some threshold value. .. warning:: This method provides no measure of the goodness of the fit. Some slope values may not provide an accurate fit to the data. Parameters ---------- temperature_bounds : `~astropy.units.Quantity`, optional em_threshold : `~astropy.units.Quantity`, optional Mask emission measure below this value rsquared_tolerance : `float` Throw away slopes with :math:`r^2` below this value """ if temperature_bounds is None: temperature_bounds = u.Quantity((1e6, 4e6), u.K) if em_threshold is None: em_threshold = u.Quantity(1e25, u.cm**(-5)) # cut on temperature temperature_bin_centers = (self.temperature_bin_edges[:-1] + self.temperature_bin_edges[1:])/2. index_temperature_bounds = np.where(np.logical_and( temperature_bin_centers >= temperature_bounds[0], temperature_bin_centers <= temperature_bounds[1])) temperature_fit = temperature_bin_centers[index_temperature_bounds].value # unwrap to 2D and threshold data = self.as_array()*u.Unit(self[0].meta['bunit']) flat_data = data.reshape(np.prod(data.shape[:2]), temperature_bin_centers.shape[0]) index_data_threshold = np.where(np.min( flat_data[:, index_temperature_bounds[0]], axis=1) >= em_threshold) flat_data_threshold = flat_data.value[index_data_threshold[0], :][:, index_temperature_bounds[0]] # very basic but vectorized fitting _, rss_flat, _, _, _ = np.polyfit( np.log10(temperature_fit), np.log10(flat_data_threshold.T), 0, full=True) coefficients, rss, _, _, _ = np.polyfit( np.log10(temperature_fit), np.log10(flat_data_threshold.T), 1, full=True) rsquared = 1. - rss/rss_flat slopes = np.where(rsquared >= rsquared_tolerance, coefficients[0], 0.) # rebuild into a map slopes_flat = np.zeros(flat_data.shape[0]) * np.nan slopes_flat[index_data_threshold[0]] = slopes slopes_2d = np.reshape(slopes_flat, data.shape[:2]) base_meta = self[0].meta.copy() base_meta['temp_a'] = temperature_fit[0] base_meta['temp_b'] = temperature_fit[-1] base_meta['bunit'] = '' base_meta['detector'] = 'EM slope' base_meta['comment'] = 'Linear fit to log-transformed LOS EM' plot_settings = self[0].plot_settings.copy() plot_settings['norm'] = None return GenericMap(slopes_2d, base_meta, plot_settings=plot_settings)
def total_emission(self): """ Sum the emission measure over all temperatures. """ tmp_meta = self[0].meta.copy() tmp_meta['temp_a'] = self.temperature_bin_edges[0] tmp_meta['temp_b'] = self.temperature_bin_edges[-1] return GenericMap(self.as_array().sum(axis=2), tmp_meta, plot_settings=self[0].plot_settings)
def make_los_velocity_map(time: u.s, field, instr, **kwargs): """ Return map of LOS velocity at a given time for a given instrument resolution. """ plot_settings = { 'cmap': cm.get_cmap('bwr'), 'norm': colors.SymLogNorm(10, vmin=-1e8, vmax=1e8) } plot_settings.update(kwargs.get('plot_settings', {})) bins, bin_range = instr.make_detector_array(field) visible = is_visible(instr.total_coordinates, instr.observer_coordinate) hist_coordinates, _, _ = np.histogram2d(instr.total_coordinates.Tx.value, instr.total_coordinates.Ty.value, bins=(bins.x.value, bins.y.value), range=(bin_range.x.value, bin_range.y.value), weights=visible) with h5py.File(instr.counts_file, 'r') as hf: try: i_time = np.where( np.array(hf['time']) * u.Unit(hf['time'].attrs['units']) == time)[0][0] except IndexError: raise IndexError( f'{time} is not a valid time in observing time for {instr.name}' ) v_x = u.Quantity(hf['velocity_x'][i_time, :], hf['velocity_x'].attrs['units']) v_y = u.Quantity(hf['velocity_y'][i_time, :], hf['velocity_y'].attrs['units']) v_z = u.Quantity(hf['velocity_z'][i_time, :], hf['velocity_z'].attrs['units']) v_los = instr.los_velocity(v_x, v_y, v_z) hist, _, _ = np.histogram2d(instr.total_coordinates.Tx.value, instr.total_coordinates.Ty.value, bins=(bins.x.value, bins.y.value), range=(bin_range.x.value, bin_range.y.value), weights=v_los.value * visible) hist /= np.where(hist_coordinates == 0, 1, hist_coordinates) meta = instr.make_fits_header(field, instr.channels[0]) del meta['wavelnth'] del meta['waveunit'] meta['bunit'] = v_los.unit.to_string() meta['detector'] = 'LOS Velocity' meta['comment'] = 'LOS velocity calculated by synthesizAR' return GenericMap(hist.T, meta, plot_settings=plot_settings)
def __init__(self, data, header, temperature_bin_edges: u.K, **kwargs): self.temperature_bin_edges = temperature_bin_edges # sanitize header meta_base = header.copy() meta_base['temp_unit'] = self.temperature_bin_edges.unit.to_string() meta_base['bunit'] = data.unit.to_string() # build map list map_list = [] for i in range(self.temperature_bin_edges.shape[0] - 1): tmp = GenericMap(data[:, :, i], meta_base) tmp.meta['temp_a'] = self.temperature_bin_edges[i].value tmp.meta['temp_b'] = self.temperature_bin_edges[i+1].value tmp.plot_settings.update(kwargs.get('plot_settings', {})) map_list.append(tmp) # call super method super().__init__(map_list)
def param_map_cube(self, parameter, line_guess=None, *extra_lines, **kwargs): """ Returns a MapCube of the given parameter at the given Gaussian values. The parameter can be 'intensity', which returns the amplitudes of the gaussian fits, 'position', which returns the mean of the fits, or 'stddev', which returns the standard deviation of the gaussian. The number of maps on the cube depends on how many lines are supplied - there must be at least one. Parameters ---------- parameter: "intensity", "position", "stddev" The parameter to return from the cube. Defaults to intensity on unrecognized input. This is because intensity is the longest of the three words and we want to make your life as simple as possible. "i", "p", "s" are also acceptable shorthand for this. line_guess and extra_lines: 3-tuples of floats There must be at least one guess, in the format (intensity, position, stddev). The closer these guesses are to the true values the better the fit will be. recalc=False: boolean If True, the gaussian fits will be recalculated, even if there's an existing fit for the given wavelengths already in the memo. This keyword should be set to True if changing the amplitude or width of the fit. **kwargs: dict Extra keyword arguments are ultimately passed on to the astropy fitter. """ param = 0 if parameter.lower()[0] == 'p': param = 1 elif parameter.lower()[0] == 's': param = 2 val_arr = self._param_array(param, line_guess, *extra_lines, **kwargs) maps = [GenericMap(raster, self.meta) for raster in val_arr.T] mapcube = MapCube(maps) return mapcube
def make_temperature_map(time: u.s, field, instr, **kwargs): """ Return map of column-averaged electron temperature at a given time for a given instrument resolution. """ plot_settings = {'cmap': cm.get_cmap('inferno')} plot_settings.update(kwargs.get('plot_settings', {})) bins, bin_range = instr.make_detector_array(field) visible = is_visible(instr.total_coordinates, instr.observer_coordinate) hist_coordinates, _, _ = np.histogram2d(instr.total_coordinates.Tx.value, instr.total_coordinates.Ty.value, bins=(bins.x.value, bins.y.value), range=(bin_range.x.value, bin_range.y.value), weights=visible) with h5py.File(instr.counts_file, 'r') as hf: try: i_time = np.where( u.Quantity(hf['time'], get_keys(hf['time'].attrs), ( 'unit', 'units')) == time)[0][0] except IndexError: raise IndexError( f'{time} is not a valid time in observing time for {instr.name}' ) weights = np.array(hf['electron_temperature'][i_time, :]) units = u.Unit( get_keys(hf['electron_temperature'].attrs, ('unit', 'units'))) hist, _, _ = np.histogram2d(instr.total_coordinates.Tx.value, instr.total_coordinates.Ty.value, bins=(bins.x.value, bins.y.value), range=(bin_range.x.value, bin_range.y.value), weights=weights * visible) hist /= np.where(hist_coordinates == 0, 1, hist_coordinates) meta = instr.make_fits_header(field, instr.channels[0]) del meta['wavelnth'] del meta['waveunit'] meta['bunit'] = units.to_string() meta['detector'] = 'Electron Temperature' meta[ 'comment'] = 'Column-averaged electron temperature calculated by synthesizAR' return GenericMap(hist.T, meta, plot_settings=plot_settings)
def slice_to_map(self, chunk, snd_dim=None, *args, **kwargs): """ Converts a given frequency chunk to a SunPy Map. Extra parameters are passed on to Map. Parameters ---------- chunk: int or astropy quantity or tuple The piece of the cube to convert to a map. If it's a single number, then it will return that single-slice map, otherwise it will aggregate the given range. Depending on the cube, this may correspond to a time or an energy dimension snd_dim: int or astropy quantity or tuple, optional Only used for hypercubes, the wavelength to choose from; works in the same way as chunk. """ if self.axes_wcs.wcs.ctype[1] == 'WAVE' and self.data.ndim == 3: error = "Cannot construct a map with only one spatial dimension" raise cu.CubeError(3, error) if isinstance(chunk, tuple): item = slice(cu.pixelize(chunk[0], self.axes_wcs, -1), cu.pixelize(chunk[1], self.axes_wcs, -1), None) maparray = self.data[item].sum(0) else: maparray = self.data[cu.pixelize(chunk, self.axes_wcs, -1)] if self.data.ndim == 4: if snd_dim is None: error = "snd_dim must be given when slicing hypercubes" raise cu.CubeError(4, error) if isinstance(snd_dim, tuple): item = slice(cu.pixelize(snd_dim[0], self.axes_wcs, -1), cu.pixelize(snd_dim[1], self.axes_wcs, -1), None) maparray = maparray[item].sum(0) else: maparray = maparray[cu.pixelize(snd_dim, self.axes_wcs, -1)] mapheader = MetaDict(self.meta) gmap = GenericMap(data=maparray, header=mapheader, *args, **kwargs) return gmap
def eclipse_image_to_map(filename): """ Given the filename to a photo, convert it to a `sunpy.map.GenericMap` object. Parameters ---------- filename : `str` The filename of the image. Returns ------- sunpymap : `sunpy.map.GenericMap` A SunPy map with valid metadata for the image. """ # load the image data im_rgb = np.flipud(matplotlib.image.imread(filename)) # remove the color information im = np.average(im_rgb, axis=2) # find the sun center and radius im_cx, im_cy, im_radius = find_sun_center_and_radius(im) tags = exifread.process_file(open(filename, 'rb')) time = m.get_image_time(tags) ############################################################################### # With the time and the radius of the solar disk we can calculate the plate # scale. plate_scale = m.get_plate_scale(time, im_radius) ############################################################################### # We can now build a WCS object and a meta dictionary. We then append a few # more meta tags to the meta dictionary. wcs = m.build_wcs(im_cx, im_cy, plate_scale) meta = m.build_meta(wcs, tags) return GenericMap(data=im, header=meta)
def synthetic_magnetogram(bottom_left_coord, top_right_coord, shape: u.pixel, centers, sigmas: u.arcsec, amplitudes: u.Gauss, observer=None): """ Compute synthetic magnetogram using 2D guassian "sunspots" Parameters ---------- bottom_left_coord : `~astropy.coordinates.SkyCoord` Bottom left corner top_right_coord : `~astropy.coordinates.SkyCoord` Top right corner shape : `~astropy.units.Quantity` Dimensionality of the magnetogram centers : `~astropy.coordinates.SkyCoord` Center coordinates of flux concentration sigmas : `~astropy.units.Quantity` Standard deviation of flux concentration with shape `(N,2)`, with `N` the number of flux concentrations amplitudes : `~astropy.units.Quantity` Amplitude of flux concentration with shape `(N,)` observer : `~astropy.coordinates.SkyCoord`, optional Defaults to Earth observer at current time """ time_now = astropy.time.Time.now() if observer is None: observer = sunpy.coordinates.ephemeris.get_earth(time=time_now) # Transform to HPC frame hpc_frame = sunpy.coordinates.Helioprojective(observer=observer, obstime=observer.obstime) bottom_left_coord = bottom_left_coord.transform_to(hpc_frame) top_right_coord = top_right_coord.transform_to(hpc_frame) # Setup array delta_x = (top_right_coord.Tx - bottom_left_coord.Tx).to(u.arcsec) delta_y = (top_right_coord.Ty - bottom_left_coord.Ty).to(u.arcsec) dx = delta_x / shape[0] dy = delta_y / shape[1] data = np.zeros((int(shape[1].value), int(shape[0].value))) xphysical, yphysical = np.meshgrid( np.arange(shape[0].value) * shape.unit * dx, np.arange(shape[1].value) * shape.unit * dy) # Add sunspots centers = centers.transform_to(hpc_frame) for c, s, a in zip(centers, sigmas, amplitudes): xc_2 = (xphysical - (c.Tx - bottom_left_coord.Tx)).to( u.arcsec).value**2.0 yc_2 = (yphysical - (c.Ty - bottom_left_coord.Ty)).to( u.arcsec).value**2.0 data += a.to( u.Gauss).value * np.exp(-xc_2 / (2 * s[0].to(u.arcsec).value**2) - yc_2 / (2 * s[1].to(u.arcsec).value**2)) # Build metadata meta = make_fitswcs_header( data, bottom_left_coord, reference_pixel=(0, 0) * u.pixel, scale=u.Quantity((dx, dy)), instrument='synthetic_magnetic_imager', telescope='synthetic_magnetic_imager', ) meta['bunit'] = 'gauss' return GenericMap(data, meta)
def make_slope_map(self, temperature_bounds=None, em_threshold=None, rsquared_tolerance=0.5, full=False): """ Calculate emission measure slope :math:`a` in each pixel Create map of emission measure slopes by fitting :math:`\mathrm{EM}\sim T^a` for a given temperature range. A slope is masked if a value between the `temperature_bounds` is less than :math:`\mathrm{EM}`. Additionally, the "goodness-of-fit" is evaluated using the correlation coefficient, :math:`r^2=1 - R_1/R_0`, where :math:`R_1` and :math:`R_0` are the residuals from the first and zeroth order polynomial fits, respectively. We mask the slope if :math:`r^2` is less than `rsquared_tolerance`. Parameters ---------- temperature_bounds : `~astropy.units.Quantity`, optional em_threshold : `~astropy.units.Quantity`, optional Mask slope if any emission measure in the fit interval is below this value rsquared_tolerance : `float` Mask any slopes with a correlation coefficient, :math:`r^2`, below this value full : `bool` If True, return maps of the intercept and :math:`r^2` values as well """ if temperature_bounds is None: temperature_bounds = u.Quantity((1e6, 4e6), u.K) if em_threshold is None: em_threshold = u.Quantity(1e25, u.cm**(-5)) # Get temperature fit array index_temperature_bounds = np.where( np.logical_and( self.temperature_bin_centers >= temperature_bounds[0], self.temperature_bin_centers <= temperature_bounds[1])) temperature_fit = np.log10( self.temperature_bin_centers[index_temperature_bounds].to( u.K).value) if temperature_fit.size < 3: warnings.warn( f'Fitting to fewer than 3 points in temperature space: {temperature_fit}' ) # Cut on temperature data = u.Quantity( self.as_array()[:, :, index_temperature_bounds].squeeze(), self[0].meta['bunit']) # Get EM fit array em_fit = np.log10( data.value.reshape((np.prod(data.shape[:2]), ) + data.shape[2:]).T) em_fit[np.logical_or(np.isinf(em_fit), np.isnan(em_fit))] = 0.0 # Filter before fitting # Fit to first-order polynomial coefficients, rss, _, _, _ = np.polyfit( temperature_fit, em_fit, 1, full=True, ) # Create mask from correlation coefficient EM threshold _, rss_flat, _, _, _ = np.polyfit( temperature_fit, em_fit, 0, full=True, ) rss = 0. * rss_flat if rss.size == 0 else rss # returns empty residual when fit is exact rsquared = 1. - rss / rss_flat rsquared_mask = rsquared.reshape(data.shape[:2]) < rsquared_tolerance em_mask = np.any(data < em_threshold, axis=2) # Rebuild into a map base_meta = self[0].meta.copy() base_meta['temp_a'] = 10.**temperature_fit[0] base_meta['temp_b'] = 10.**temperature_fit[-1] base_meta['bunit'] = '' base_meta['detector'] = 'EM slope' base_meta['comment'] = 'Linear fit to log-transformed LOS EM' plot_settings = self[0].plot_settings.copy() plot_settings['norm'] = None m = GenericMap(coefficients[0, :].reshape(data.shape[:2]), base_meta, mask=np.stack((em_mask, rsquared_mask), axis=2).any(axis=2), plot_settings=plot_settings) return (m, coefficients, rsquared) if full else m
def synthetic_magnetogram(bottom_left_coord, top_right_coord, shape: u.pixel, centers, sigmas: u.arcsec, amplitudes: u.Gauss, observer=None): """ Compute synthetic magnetogram using 2D guassian "sunspots" Parameters ---------- bottom_left_coord : `~astropy.coordinates.SkyCoord` Bottom left corner top_right_coord : `~astropy.coordinates.SkyCoord` Top right corner shape : `~astropy.units.Quantity` Dimensionality of the magnetogram centers : `~astropy.coordinates.SkyCoord` Center coordinates of flux concentration sigmas : `~astropy.units.Quantity` Standard deviation of flux concentration with shape `(N,2)`, with `N` the number of flux concentrations amplitudes : `~astropy.units.Quantity` Amplitude of flux concentration with shape `(N,)` observer : `~astropy.coordinates.SkyCoord`, optional Defaults to Earth observer at current time """ time_now = astropy.time.Time.now() if observer is None: observer = sunpy.coordinates.ephemeris.get_earth(time=time_now) # Transform to HPC frame bottom_left_coord = bottom_left_coord.transform_to( sunpy.coordinates.Helioprojective(observer=observer)) top_right_coord = top_right_coord.transform_to( sunpy.coordinates.Helioprojective(observer=observer)) # Setup array delta_x = (top_right_coord.Tx - bottom_left_coord.Tx).to(u.arcsec) delta_y = (top_right_coord.Ty - bottom_left_coord.Ty).to(u.arcsec) dx = delta_x / shape[0] dy = delta_y / shape[1] data = np.zeros((int(shape[1].value), int(shape[0].value))) xphysical, yphysical = np.meshgrid( np.arange(shape[0].value) * shape.unit * dx, np.arange(shape[1].value) * shape.unit * dy) # Add sunspots centers = centers.transform_to( sunpy.coordinates.Helioprojective(observer=observer)) for c, s, a in zip(centers, sigmas, amplitudes): xc_2 = (xphysical - (c.Tx - bottom_left_coord.Tx)).to( u.arcsec).value**2.0 yc_2 = (yphysical - (c.Ty - bottom_left_coord.Ty)).to( u.arcsec).value**2.0 data += a.to( u.Gauss).value * np.exp(-xc_2 / (2 * s[0].to(u.arcsec).value**2) - yc_2 / (2 * s[1].to(u.arcsec).value**2)) # Build metadata meta = MetaDict({ 'telescop': 'synthetic_magnetic_imager', 'instrume': 'synthetic_magnetic_imager', 'detector': 'synthetic_magnetic_imager', 'bunit': 'Gauss', 'ctype1': 'HPLN-TAN', 'ctype2': 'HPLT-TAN', 'hgln_obs': observer.transform_to('heliographic_stonyhurst').lon.to(u.deg).value, 'hglt_obs': observer.transform_to('heliographic_stonyhurst').lat.to(u.deg).value, 'cunit1': 'arcsec', 'cunit2': 'arcsec', 'crpix1': (shape[0].value + 1) / 2., 'crpix2': (shape[1].value + 1) / 2., 'cdelt1': dx.value, 'cdelt2': dy.value, 'crval1': ((bottom_left_coord.Tx + top_right_coord.Tx) / 2.).to(u.arcsec).value, 'crval2': ((bottom_left_coord.Ty + top_right_coord.Ty) / 2.).to(u.arcsec).value, 'dsun_obs': observer.transform_to('heliographic_stonyhurst').radius.to(u.m).value, 'dsun_ref': observer.transform_to('heliographic_stonyhurst').radius.to(u.m).value, 'rsun_ref': const.R_sun.to(u.m).value, 'rsun_obs': ((const.R_sun / observer.transform_to('heliographic_stonyhurst').radius ).decompose() * u.radian).to(u.arcsec).value, 't_obs': time_now.iso, 'date-obs': time_now.iso, }) return GenericMap(data, meta)