Beispiel #1
0
 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
Beispiel #2
0
    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
Beispiel #3
0
    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)
Beispiel #4
0
 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)
Beispiel #5
0
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)
Beispiel #6
0
    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)
Beispiel #7
0
    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
Beispiel #8
0
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)
Beispiel #9
0
    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
Beispiel #10
0
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)
Beispiel #11
0
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)
Beispiel #12
0
    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
Beispiel #13
0
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)