Source code for sunpy.map.header_helper
import numpy as np
import astropy.units as u
import astropy.wcs
from astropy.coordinates import SkyCoord
from sunpy import log
from sunpy.coordinates import frames, sun
from sunpy.util import MetaDict
__all__ = ['meta_keywords', 'make_fitswcs_header', 'get_observer_meta', 'make_heliographic_header']
[docs]def meta_keywords():
"""
Returns the metadata keywords that are used when creating a `sunpy.map.GenericMap`.
Examples
--------
Returns a dictionary of all meta keywords that are used in a `sunpy.map.GenericMap` header:
>>> import sunpy.map
>>> sunpy.map.meta_keywords()
{'cunit1': 'Units of the coordinate increments along naxis1 e.g. arcsec **required',
'cunit2': 'Units of the coordinate increments along naxis2 e.g. arcsec **required',
...
"""
return _map_meta_keywords
[docs]@u.quantity_input(equivalencies=u.spectral())
def make_fitswcs_header(data,
coordinate,
reference_pixel: u.pix = None,
scale: u.arcsec/u.pix = None,
rotation_angle: u.deg = None,
rotation_matrix=None,
instrument=None,
telescope=None,
observatory=None,
wavelength: u.angstrom = None,
exposure: u.s = None,
projection_code="TAN",
unit=None):
"""
Function to create a FITS-WCS header from a coordinate object
(`~astropy.coordinates.SkyCoord`) that is required to
create a `~sunpy.map.GenericMap`.
Parameters
----------
data : `~numpy.ndarray`, `~astropy.units.Quantity`, or `tuple`
Array data of Map for which a header is required, or the shape of the
data array (in numpy order, i.e. ``(y_size, x_size)``).
coordinate : `~astropy.coordinates.SkyCoord` or `~astropy.coordinates.BaseCoordinateFrame`
The coordinate of the reference pixel.
reference_pixel : `~astropy.units.Quantity`, optional
Reference pixel along each axis. These are expected to be Cartestian ordered, i.e
the first index is the x axis, second index is the y axis. Defaults to
the center of data array, ``(data.shape[1] - 1)/2., (data.shape[0] - 1)/2.)``,
this argument is zero indexed (Python convention) not 1 indexed (FITS
convention).
scale : `~astropy.units.Quantity` of size 2, optional
Pixel scaling along x and y axis (i.e. the spatial scale of the pixels (dx, dy)). These are
expected to be Cartestian ordered, i.e [dx, dy].
Defaults to ``([1., 1.] arcsec/pixel)``.
rotation_angle : `~astropy.units.Quantity`, optional
Coordinate system rotation angle, will be converted to a rotation
matrix and stored in the ``PCi_j`` matrix. Can not be specified with
``rotation_matrix``. Defaults to no rotation.
rotation_matrix : `~numpy.ndarray` of dimensions 2x2, optional
Matrix describing the rotation required to align solar North with
the top of the image in FITS ``PCi_j`` convention. Can not be specified
with ``rotation_angle``.
instrument : `~str`, optional
Name of the instrument of the observation.
telescope : `~str`, optional
Name of the telescope of the observation.
observatory : `~str`, optional
Name of the observatory of the observation.
wavelength : `~astropy.units.Quantity`, optional
Wavelength of the observation as an astropy quanitity, e.g. 171*u.angstrom.
From this keyword, the meta keywords ``wavelnth`` and ``waveunit`` will be populated.
exposure : `~astropy.units.Quantity`, optional
Exposure time of the observation
projection_code : `str`, optional
The FITS standard projection code for the new header.
unit : `~astropy.units.Unit`, optional
Units of the array data of the Map. This will populate the the ``'bunit'`` meta keyword.
If ``data`` is a `~astropy.units.Quantity`, the unit specified here will take precedence
over the unit information attached to ``data``.
Returns
-------
`~sunpy.util.MetaDict`
The header information required for making a `sunpy.map.GenericMap`.
Notes
-----
The observer coordinate is taken from the observer property of the ``reference_pixel``
argument.
Examples
--------
>>> import sunpy.map
>>> from sunpy.coordinates import frames
>>> from astropy.coordinates import SkyCoord
>>> import astropy.units as u
>>> import numpy as np
>>> data = np.random.rand(1024, 1024)
>>> my_coord = SkyCoord(0*u.arcsec, 0*u.arcsec, obstime="2017-08-01",
... observer = 'earth', frame=frames.Helioprojective)
>>> my_header = sunpy.map.make_fitswcs_header(data, my_coord)
>>> my_map = sunpy.map.Map(data, my_header)
"""
coordinate = _validate_coordinate(coordinate)
if hasattr(data, "shape"):
shape = data.shape
else:
shape = data
if hasattr(data, "unit"):
if unit is None:
unit = data.unit
else:
log.info("Overwriting data's current unit with specified unit.")
meta_wcs = _get_wcs_meta(coordinate, projection_code)
meta_wcs = _set_instrument_meta(meta_wcs, instrument, telescope, observatory, wavelength, exposure, unit)
meta_wcs = _set_transform_params(meta_wcs, coordinate, reference_pixel, scale, shape)
meta_wcs = _set_rotation_params(meta_wcs, rotation_angle, rotation_matrix)
if getattr(coordinate, 'observer', None) is not None:
# Have to check for str, as doing == on a SkyCoord and str raises an error
if isinstance(coordinate.observer, str) and coordinate.observer == 'self':
dsun_obs = coordinate.radius
else:
dsun_obs = coordinate.observer.radius
meta_wcs['rsun_obs'] = sun._angular_radius(coordinate.rsun, dsun_obs).to_value(u.arcsec)
meta_dict = MetaDict(meta_wcs)
return meta_dict
def _validate_coordinate(coordinate):
if not isinstance(coordinate, (SkyCoord, frames.BaseCoordinateFrame)):
raise ValueError("coordinate needs to be a coordinate frame or an SkyCoord instance.")
if isinstance(coordinate, SkyCoord):
coordinate = coordinate.frame
if coordinate.obstime is None:
raise ValueError("The coordinate needs an observation time, `obstime`.")
if isinstance(coordinate, frames.Heliocentric):
raise ValueError("This function does not currently support heliocentric coordinates.")
return coordinate
def _set_rotation_params(meta_wcs, rotation_angle, rotation_matrix):
if rotation_angle is not None and rotation_matrix is not None:
raise ValueError("Can not specify both rotation angle and rotation matrix.")
if rotation_angle is None and rotation_matrix is None:
rotation_angle = 0 * u.deg
if rotation_angle is not None:
lam = meta_wcs['cdelt2'] / meta_wcs['cdelt1']
p = np.deg2rad(rotation_angle)
rotation_matrix = np.array([[np.cos(p), -1 * lam * np.sin(p)],
[1/lam * np.sin(p), np.cos(p)]])
if rotation_matrix is not None:
(meta_wcs['PC1_1'], meta_wcs['PC1_2'],
meta_wcs['PC2_1'], meta_wcs['PC2_2']) = (rotation_matrix[0, 0], rotation_matrix[0, 1],
rotation_matrix[1, 0], rotation_matrix[1, 1])
return meta_wcs
def _set_transform_params(meta_wcs, coordinate, reference_pixel, scale, shape):
meta_wcs['naxis'] = 2
meta_wcs['naxis1'] = shape[1]
meta_wcs['naxis2'] = shape[0]
if reference_pixel is None:
reference_pixel = u.Quantity([(shape[1] - 1)/2.*u.pixel,
(shape[0] - 1)/2.*u.pixel])
if scale is None:
scale = [1., 1.] * (u.arcsec/u.pixel)
meta_wcs['crval1'], meta_wcs['crval2'] = (coordinate.spherical.lon.to_value(meta_wcs['cunit1']),
coordinate.spherical.lat.to_value(meta_wcs['cunit2']))
# When the native latitude of the fiducial point is 0 degrees, which is typical for cylindrical
# projections, the correct value of `lonpole` depends on whether the world latitude of the
# fiducial point is greater than or less than its native latitude.
if coordinate.spherical.lat.to_value(u.deg) < meta_wcs['theta0']:
meta_wcs['LONPOLE'] = 180.
else:
meta_wcs['LONPOLE'] = 0.
del meta_wcs['theta0'] # remove the native latitude of the fiducial point
# Add 1 to go from input 0-based indexing to FITS 1-based indexing
meta_wcs['crpix1'], meta_wcs['crpix2'] = (reference_pixel[0].to_value(u.pixel) + 1,
reference_pixel[1].to_value(u.pixel) + 1)
meta_wcs['cdelt1'] = scale[0].to_value(meta_wcs['cunit1']/u.pixel)
meta_wcs['cdelt2'] = scale[1].to_value(meta_wcs['cunit2']/u.pixel)
return meta_wcs
def _get_wcs_meta(coordinate, projection_code):
"""
Function to get WCS meta from the SkyCoord using
`astropy.wcs.utils.celestial_frame_to_wcs`
Parameters
----------
coordinate : `~astropy.coordinates.BaseCoordinateFrame`
Returns
-------
`dict`
Containing the WCS meta information
* ctype1, ctype2
* cunit1, cunit2
* date_obs
* observer auxiliary information, if set on `coordinate`
"""
coord_meta = {}
skycoord_wcs = astropy.wcs.utils.celestial_frame_to_wcs(coordinate, projection_code)
cunit1, cunit2 = skycoord_wcs.wcs.cunit
coord_meta = dict(skycoord_wcs.to_header())
coord_meta['cunit1'], coord_meta['cunit2'] = cunit1.to_string("fits"), cunit2.to_string("fits")
coord_meta['theta0'] = skycoord_wcs.wcs.theta0 # add the native latitude of the fiducial point
return coord_meta
[docs]@u.quantity_input
def get_observer_meta(observer, rsun: (u.Mm, None) = None):
"""
Function to get observer meta from coordinate frame.
Parameters
----------
observer : `~astropy.coordinates.BaseCoordinateFrame`
The coordinate of the observer, must be transformable to Heliographic
Stonyhurst.
rsun : `astropy.units.Quantity`, optional
The radius of the Sun. If ``None``, the RSUN_OBS and RSUN_REF keys are
not set.
Returns
-------
coord_meta : `dict`
WCS metadata, with the keys ``['hgln_obs', 'hglt_obs', 'dsun_obs']``,
and additionally if ``rsun`` is given ``['rsun_obs', 'rsun_ref']``.
"""
observer = observer.transform_to(frames.HeliographicStonyhurst(obstime=observer.obstime))
coord_meta = {}
coord_meta['hgln_obs'] = observer.lon.to_value(u.deg)
coord_meta['hglt_obs'] = observer.lat.to_value(u.deg)
coord_meta['dsun_obs'] = observer.radius.to_value(u.m)
if rsun is not None:
coord_meta['rsun_ref'] = rsun.to_value(u.m)
coord_meta['rsun_obs'] = sun._angular_radius(rsun, observer.radius).to_value(u.arcsec)
return coord_meta
def _set_instrument_meta(meta_wcs, instrument, telescope, observatory, wavelength, exposure, unit):
"""
Function to correctly name keywords from keyword arguments
"""
if instrument is not None:
meta_wcs['instrume'] = str(instrument)
if telescope is not None:
meta_wcs['telescop'] = str(telescope)
if observatory is not None:
meta_wcs['obsrvtry'] = str(observatory)
if wavelength is not None:
meta_wcs['wavelnth'] = wavelength.to_value()
meta_wcs['waveunit'] = wavelength.unit.to_string("fits")
if exposure is not None:
meta_wcs['exptime'] = exposure.to_value(u.s)
if unit is not None:
meta_wcs['bunit'] = unit.to_string("fits")
return meta_wcs
_map_meta_keywords = {
'cunit1':
'Units of the coordinate increments along naxis1 e.g. arcsec **required',
'cunit2':
'Units of the coordinate increments along naxis2 e.g. arcsec **required',
'crval1':
'Coordinate value at reference point on naxis1 **required',
'crval2':
'Coordinate value at reference point on naxis2 **required',
'cdelt1':
'Spatial scale of pixels for naxis1, i.e. coordinate increment at reference point',
'cdelt2':
'Spatial scale of pixels for naxis2, i.e. coordinate increment at reference point',
'crpix1':
'Pixel coordinate at reference point naxis1',
'crpix2':
'Pixel coordinate at reference point naxis2',
'ctype1':
'Coordinate type projection along naxis1 of data e.g. HPLT-TAN',
'ctype2':
'Coordinate type projection along naxis2 of data e.g. HPLN-TAN',
'hgln_obs':
'Heliographic longitude of observation',
'hglt_obs':
'Heliographic latitude of observation',
'dsun_obs':
'distance to Sun from observation in metres',
'rsun_obs':
'radius of Sun in meters from observation',
'date-obs':
'date of observation e.g. 2013-10-28 00:00',
'date_obs':
'date of observation e.g. 2013-10-28 00:00',
'rsun_ref':
'reference radius of Sun in meters',
'solar_r':
'radius of Sun in meters from observation',
'radius':
'radius of Sun in meters from observation',
'crln_obs':
'Carrington longitude of observation',
'crlt_obs':
'Heliographic latitude of observation',
'solar_b0':
'Solar B0 angle',
'detector':
'name of detector e.g. AIA',
'exptime':
'exposure time of observation, in seconds e.g 2',
'instrume':
'name of instrument',
'wavelnth':
'wavelength of observation',
'waveunit':
'unit for which observation is taken e.g. angstom',
'obsrvtry':
'name of observatory of observation',
'telescop':
'name of telescope of observation',
'lvl_num':
'FITS processing level',
'crota2':
'Rotation of the horizontal and vertical axes in degrees',
'PC1_1':
'Matrix element PCi_j describing the rotation required to align solar North with the top of the image.',
'PC1_2':
'Matrix element PCi_j describing the rotation required to align solar North with the top of the image.',
'PC2_1':
'Matrix element PCi_j describing the rotation required to align solar North with the top of the image.',
'PC2_2':
'Matrix element PCi_j describing the rotation required to align solar North with the top of the image.',
'CD1_1':
'Matrix element CDi_j describing the rotation required to align solar North with the top of the image.',
'CD1_2':
'Matrix element CDi_j describing the rotation required to align solar North with the top of the image.',
'CD2_1':
'Matrix element CDi_j describing the rotation required to align solar North with the top of the image.',
'CD2_2':
'Matrix element CDi_j describing the rotation required to align solar North with the top of the image.'
}
[docs]def make_heliographic_header(date, observer_coordinate, shape, *, frame, projection_code="CAR"):
"""
Construct a FITS-WCS header for a full-Sun heliographic (Carrington or Stonyhurst) coordinate frame.
The date-time and observer coordinate of the new coordinate frame
are taken from the input map. The resulting WCS covers the full surface
of the Sun, and has a reference coordinate at (0, 0) degrees Longitude/Latitude.
Parameters
----------
date :
Date for the output header.
observer_coordinate :
Observer coordinate for the output header.
shape : [int, int]
Output map shape, number of pixels in (latitude, longitude).
frame : {'carrington', 'stonyhurst'}
Coordinate frame.
projection_code : {'CAR', 'CEA'}
Projection to use for the latitude coordinate.
Returns
-------
`~sunpy.util.MetaDict`
See Also
--------
sunpy.map.header_helper.make_fitswcs_header : A more generic header helper that can be used if more customisation is required.
Examples
--------
>>> from sunpy.map.header_helper import make_heliographic_header
>>> from sunpy.coordinates import get_earth
>>>
>>> date = '2020-01-01 12:00:00'
>>> observer = get_earth(date)
>>> header = make_heliographic_header(date, observer, [90, 180], frame='carrington')
>>> header
MetaDict([('wcsaxes', 2), ('crpix1', 90.5), ('crpix2', 45.5), ('cdelt1', 2.0), ('cdelt2', 2.0), ('cunit1', 'deg'), ('cunit2', 'deg'), ('ctype1', 'CRLN-CAR'), ('ctype2', 'CRLT-CAR'), ('crval1', 0.0), ('crval2', 0.0), ('lonpole', 0.0), ('latpole', 90.0), ('mjdref', 0.0), ('date-obs', '2020-01-01T12:00:00.000'), ('rsun_ref', 695700000.0), ('dsun_obs', 147096975776.97), ('hgln_obs', 0.0), ('hglt_obs', -3.0011725838606), ('naxis', 2), ('naxis1', 180), ('naxis2', 90), ('pc1_1', 1.0), ('pc1_2', -0.0), ('pc2_1', 0.0), ('pc2_2', 1.0), ('rsun_obs', 975.5398432033492)])
.. minigallery:: sunpy.map.make_heliographic_header
"""
valid_codes = {"CAR", "CEA"}
if projection_code not in valid_codes:
raise ValueError(f"projection_code must be one of {valid_codes}")
valid_frames = {'carrington', 'stonyhurst'}
if frame not in valid_frames:
raise ValueError(f"frame must be one of {valid_frames}")
frame_out = SkyCoord(
0 * u.deg,
0 * u.deg,
frame=f"heliographic_{frame}",
obstime=date,
observer=observer_coordinate,
rsun=getattr(observer_coordinate, "rsun", None),
)
if projection_code == "CAR":
scale = [360 / int(shape[1]), 180 / int(shape[0])] * u.deg / u.pix
elif projection_code == "CEA":
# Using the cylindrical equal-area (CEA) projection,
# scale needs to be to 180/pi times the sin(latitude) spacing
# See Section 5.5, Thompson 2006
scale = [
360 / int(shape[1]),
(180 / np.pi) / (int(shape[0]) / 2)
] * u.deg / u.pix
header = make_fitswcs_header(shape, frame_out, scale=scale, projection_code=projection_code)
return header