Source code for sunpy.map.sources.soho

"""SOHO Map subclass definitions"""

import numpy as np

import astropy.units as u
from astropy.coordinates import CartesianRepresentation, HeliocentricMeanEcliptic
from astropy.visualization import PowerStretch
from astropy.visualization.mpl_normalize import ImageNormalize

from sunpy import log
from sunpy.map.mapbase import GenericMap, SpatialPair
from sunpy.map.sources.source_type import source_stretch
from sunpy.time import parse_time

__all__ = ['EITMap', 'LASCOMap', 'MDIMap', 'MDISynopticMap']


[docs] class EITMap(GenericMap): """ SOHO EIT Image Map. SOHO EIT is an extreme ultraviolet (EUV) imager able to image the solar transition region and inner corona in four selected bandpasses, 171 (Fe IX/X), 195 (Fe XII), 284 (Fe XV), and 304 (He II) Angstrom. SOHO was launched on 2 December 2 1995 into a sun-synchronous orbit and primary mission operations for SOHO EIT ended at the end of July 2010. References ---------- * `SOHO Mission Page <https://sohowww.nascom.nasa.gov/>`_ * `SOHO EIT Instrument Page <https://umbra.nascom.nasa.gov/eit/>`_ * `SOHO EIT User Guide <https://umbra.nascom.nasa.gov/eit/eit_guide/>`_ """ def __init__(self, data, header, **kwargs): super().__init__(data, header, **kwargs) self._nickname = self.detector self.plot_settings['cmap'] = self._get_cmap_name() self.plot_settings['norm'] = ImageNormalize( stretch=source_stretch(self.meta, PowerStretch(0.5)), clip=False) @property def date(self): # Old EIT data has date-obs in format of dd-JAN-yy so we use date_obs where available return self._get_date('date_obs') or super().date @property def spatial_units(self): """ If not present in CUNIT{1,2} keywords, defaults to arcsec. """ return SpatialPair(u.Unit(self.meta.get('cunit1', 'arcsec')), u.Unit(self.meta.get('cunit2', 'arcsec'))) @property def waveunit(self): """ If WAVEUNIT FITS keyword isn't present, defaults to Angstrom. """ unit = self.meta.get("waveunit", "Angstrom") or "Angstrom" return u.Unit(unit) @property def detector(self): return "EIT" @property def rsun_obs(self): return u.Quantity(self.meta['solar_r'] * self.meta['cdelt1'], 'arcsec') @property def _supported_observer_coordinates(self): return [(('hec_x', 'hec_y', 'hec_z'), {'x': self.meta.get('hec_x'), 'y': self.meta.get('hec_y'), 'z': self.meta.get('hec_z'), 'unit': u.km, 'representation_type': CartesianRepresentation, 'frame': HeliocentricMeanEcliptic}) ] + super()._supported_observer_coordinates
[docs] @classmethod def is_datasource_for(cls, data, header, **kwargs): """Determines if header corresponds to an EIT image""" return header.get('instrume') == 'EIT'
[docs] class LASCOMap(GenericMap): """ SOHO LASCO Image Map The Large Angle and Spectrometric COronagraph (LASCO) is a set of three Lyot-type coronagraphs (C1, C2, and C3) that image the solar corona from 1.1 to 32 solar radii. The C1 images rom 1.1 to 3 solar radii. The C2 telescope images the corona from 2 to 6 solar radii, overlapping the outer field-of-view of C1 from 2 to 3 solar radii. The C3 telescope extends the field-of-view to 32 solar radii. SOHO was launched on 2 December 2 1995 into a sun-synchronous orbit. References ---------- * `SOHO Mission Page <https://sohowww.nascom.nasa.gov/>`_ """ def __init__(self, data, header, **kwargs): super().__init__(data, header, **kwargs) self.plot_settings['cmap'] = f'soholasco{self.detector[1]!s}' self.plot_settings['norm'] = ImageNormalize( stretch=source_stretch(self.meta, PowerStretch(0.5)), clip=False) @property def spatial_units(self): return SpatialPair(u.Unit(self.meta.get('cunit1').lower()), u.Unit(self.meta.get('cunit2').lower())) @property def rotation_matrix(self): # For Helioviewer images, clear rotation metadata, as these have already been rotated. # Also check that all CROTAn keywords exist to make sure that it's an untouched # Helioviewer file. if ('helioviewer' in self.meta and 'crota' in self.meta and 'crota1' in self.meta and 'crota2' in self.meta): log.debug("LASCOMap: Ignoring CROTAn keywords " "because the map has already been rotated by Helioviewer") return np.identity(2) else: return super().rotation_matrix @property def date(self): if date := self.meta.get('date-obs', self.meta.get('date_obs')): # If the header has already been fixed, no need to concatenate if (time := self.meta.get('time-obs', self.meta.get('time_obs'))) and 'T' not in date: date = f"{date}T{time}" date = parse_time(date) return date or super().date def _set_date(self, date): if 'time-obs' in self.meta: time_key = 'time-obs' del self.meta['time-obs'] if 'time_obs' in self.meta: time_key = 'time_obs' del self.meta['time_obs'] date_key = 'date-obs' if 'date-obs' in self.meta else 'date_obs' if time_key in self.meta: self.meta[date_key], self.meta[time_key] = parse_time(date).utc.isot.split('T') else: self.meta[date_key] = parse_time(date).utc.isot @property def nickname(self): filter = self.meta.get('filter', '') return f'{self.instrument}-{self.detector} {filter}' @nickname.setter def nickname(self, value): raise AttributeError("Cannot manually set nickname for LASCOMap") @property def measurement(self): # TODO: This needs to do more than white-light. Should give B, pB, etc. return "white-light" @property def unit(self): bunit = self.meta.get('bunit', None) if bunit is not None and bunit == 0: # The HV JP2 files given to us have a 0 value BUNIT return u.dimensionless_unscaled return super().unit
[docs] @classmethod def is_datasource_for(cls, data, header, **kwargs): """Determines if header corresponds to an LASCO image.""" return header.get('instrume') == 'LASCO'
[docs] class MDIMap(GenericMap): """ SOHO MDI Image Map The Michelson Doppler Imager (MDI) is a white light refracting telescope which feeds sunlight through a series of filters onto a CCD camera. Two tunable Michelson interformeters define a 94 mAngstrom bandpass that can be tuned across the Ni 6768 Angstrom solar absorption line. MDI measures line-of-sight motion (Dopplergrams), magnetic field (magnetograms), and brightness images of the full solar disk at several resolutions (4 arc-second to very low resolution) and a fixed selected region in higher resolution (1.2 arc-second). SOHO was launched on 2 December 2 1995 into a sun-synchronous orbit and SOHO MDI ceased normal science observations on 12 April 2011. References ---------- * `SOHO Mission Page <https://sohowww.nascom.nasa.gov/>`_ * `SOHO MDI Instrument Page <http://soi.stanford.edu>`_ * `SOHO MDI Fits Header keywords <http://soi.stanford.edu/sssc/doc/keywords.html>`_ * `SOHO MDI Instrument Paper <https://doi.org/10.1007/978-94-009-0191-9_5>`_ """ def __init__(self, data, header, **kwargs): super().__init__(data, header, **kwargs) if self.unit is not None and self.unit.is_equivalent(u.T): # Magnetic field maps, not intensity maps self._set_symmetric_vmin_vmax() @property def _date_obs(self): if 'T' in self.meta.get('date-obs', ''): # Helioviewer MDI files have the full date in DATE_OBS, but we still # want to let normal FITS files use DATE-OBS return parse_time(self.meta['date-obs']) elif 'date_obs' in self.meta: return parse_time(self.meta['date_obs']) @property def unit(self): bunit = self.meta.get('bunit', None) if bunit is not None and bunit.lower() == 'arbitrary intensity units': return u.dimensionless_unscaled return super().unit @property def spatial_units(self): """ If not present in CUNIT{1,2} keywords, defaults to arcsec. """ return SpatialPair(u.Unit(self.meta.get('cunit1', 'arcsec')), u.Unit(self.meta.get('cunit2', 'arcsec'))) @staticmethod def _is_mdi_map(header): return header.get('instrume') == 'MDI' or header.get('camera') == 'MDI' @staticmethod def _is_synoptic_map(header): return 'Synoptic Chart' in header.get('CONTENT', '') @property def _supported_observer_coordinates(self): return [(('obs_l0', 'obs_b0', 'obs_dist'), {'lon': self.meta.get('obs_l0'), 'lat': self.meta.get('obs_b0'), 'radius': self.meta.get('obs_dist'), 'unit': (u.deg, u.deg, u.AU), 'frame': "heliographic_carrington"}), ] + super()._supported_observer_coordinates @property def instrument(self): return "MDI" @property def waveunit(self): """ Always assumed to be Angstrom. """ return "Angstrom" @property def measurement(self): """ Returns the measurement type. """ return self.meta.get('CONTENT', '')
[docs] @classmethod def is_datasource_for(cls, data, header, **kwargs): """Determines if header corresponds to an MDI image""" return cls._is_mdi_map(header) and not cls._is_synoptic_map(header)
[docs] class MDISynopticMap(MDIMap): """ SOHO MDI synoptic magnetogram Map. See the docstring of `MDIMap` for information on the MDI instrument. """ @property def date(self): """ Image observation time. This is taken from the 'DATE-OBS' or 'T_OBS' keywords. """ return self._get_date('date-obs') or self._get_date('t_obs') or super().date def _set_date(self, date): self.meta['date-obs'] = self.meta['t_obs'] = parse_time(date).utc.isot @property def spatial_units(self): cunit1 = self.meta['cunit1'] if cunit1 == 'Degree': cunit1 = 'deg' cunit2 = self.meta['cunit2'] if cunit2 == 'Sine Latitude': cunit2 = 'deg' return SpatialPair(u.Unit(cunit1), u.Unit(cunit2)) @property def unit(self): bunit = self.meta.get('bunit', None) if bunit is None: return # Maxwells aren't in the IAU unit style manual and therefore not a valid FITS unit # The mapbase unit property forces this validation, so we must override it to prevent it. return u.Unit(bunit) @property def scale(self): if self.meta['cunit2'] == 'Sine Latitude': # Since, this map uses the cylindrical equal-area (CEA) projection, # the spacing should be modified to 180/pi times the original value # Reference: Section 5.5, Thompson 2006 return SpatialPair(np.abs(self.meta['cdelt1']) * self.spatial_units[0] / u.pixel, 180 / np.pi * self.meta['cdelt2'] * u.deg / u.pixel)
[docs] @classmethod def is_datasource_for(cls, data, header, **kwargs): """Determines if header corresponds to an MDI image""" return cls._is_mdi_map(header) and cls._is_synoptic_map(header)