Data Classes¶
SpectrogramCube¶
The fundamental data class of the sunraster
package is SpectrogramCube
.
It is designed to handle data representing one or more spectrograms of solar regions.
SpectrogramCube
stores its data as an array whose
transformations between pixel and real world coordinates are described by
a single astropy
WCS (World Coordinate System) object.
(For data that is described by multiple WCS objects, see the
SpectrogramSequence RasterSequence sections below.)
SpectrogramCube
is subclassed from ndcube.NDCube
and so inherits the
same attributes for data, wcs, extra_coords, uncertainty, mask, meta, and unit.
It also inherits much of the same slicing, coordinate transformation and
visualization API and provides some additional convenience properties relevant to
spectrogram data.
Initialization¶
To initialize a basic SpectrogramCube
object, all you need is an
array containing the data and an astropy.wcs.WCS
object
describing the transformation from array-element (or pixel) space to real
world coordinates.
Let’s create a 3D numpy.ndarray
representing a series of spectrograms.
Let the array shape be (3, 4, 5) and let every value be 1.
Let the first axis represent time (and/or space if the spectrogram slit
is rastering across a solar region).
Let the second represent the position along a dispersing slit,
and the third represent the spectral axis.
Although a WCS object can often be easily created by feeding a FITS header into
the astropy.wcs.WCS
class, we will create one manually here to be explicit.
Note that due to (confusing) convention, the order of the axes in the
WCS object is reversed relative to the data array.
>>> import numpy as np
>>> data = np.ones((3, 4, 5))
>>> import astropy.wcs
>>> wcs_input_dict = {
... 'CTYPE1': 'WAVE ', 'CUNIT1': 'Angstrom', 'CDELT1': 0.2, 'CRPIX1': 0, 'CRVAL1': 10, 'NAXIS1': 5,
... 'CTYPE2': 'HPLT-TAN', 'CUNIT2': 'deg', 'CDELT2': 0.5, 'CRPIX2': 2, 'CRVAL2': 0.5, 'NAXIS2': 4,
... 'CTYPE3': 'HPLN-TAN', 'CUNIT3': 'deg', 'CDELT3': 0.4, 'CRPIX3': 2, 'CRVAL3': 1, 'NAXIS3': 3}
>>> input_wcs = astropy.wcs.WCS(wcs_input_dict)
We have defined the first axis to be spatial (helioprojective longitude and latitude)
which implies that this series of spectrograms represents a raster scan across a
solar region. The second axis (position along slit) also has coordinates of
helioprojective longitude and latitude.
Although we often think of the x-dimension as longitude and the
y-dimension as latitude, latitude and longitude are in fact coupled dimensions.
This means that – except in a small number of edge cases – moving along the slit
in y-direction will cause both the latitude AND longitude to change, even if
only slightly. This is important to understand when interacting
with the WCS object, and hence the SpectrogramCube
class.
The 3rd axis (spectral) has coordinates of wavelength.
Now that we have a data array and a corresponding WCS object, we can
create a SpectrogramCube
instance simply by doing:
>>> from sunraster import SpectrogramCube
>>> my_spectrograms = SpectrogramCube(data, input_wcs)
The data array is stored in the my_spectrograms.data
attribute while the
WCS object is stored in the my_spectrograms.wcs
attribute. However, when
manipulating/slicing the data is it better to slice the object as a
whole as all relevant data and metadata is sliced simultaneously.
(See section on Slicing.)
Thanks to the fact that SpectrogramCube
is subclassed from
NDCube
, you can also supply additional data to the instance.
These include: metadata (dict
or dict-like) located in sunraster.SpectrogramCube.meta
;
a data mask (boolean numpy.ndarray
) located in sunraster.SpectrogramCube.mask
for marking reliable and unreliable pixels;
a unit (astropy.units.Unit
or unit str
) located at sunraster.SpectrogramCube.unit
;
and an uncertainty array (numpy.ndarray
) located in uncertainty
describing the uncertainty of each data array value. It is advised that you use
one of astropy’s uncertainty classes to describe your uncertainty.
However, this is not required by SpectrogramCube
.
A simple array will still work but will cause a warning to be raised.
Here is an example of how to instantiate these attributes.
>>> import astropy.units as u
>>> from astropy.nddata import StdDevUncertainty
>>> uncertainties = StdDevUncertainty(np.sqrt(data))
>>> # Create a mask where all pixels are unmasked, i.e. all mask values are False.
>>> mask = np.zeros_like(data, dtype=bool)
>>> meta = {"Description": "This is example SpectrogramCube metadata."}
>>> my_spectrograms = SpectrogramCube(data, input_wcs, uncertainty=uncertainties,
... mask=mask, meta=meta)
Coordinates¶
WCS Coordinates¶
The primary location for coordinate information in a SpectrogramCube
instance is its WCS.
The coordinate values for each axis and pixel can be accessed via the
axis_world_coords
, pixel_to_world
and
world_to_pixel
methods inherited from ndcube.NDCube
.
To learn how to use these coordinate transformation methods, see the
NDCube coordinate transformations documentation
Extra Coordinates¶
SpectrogramCube
can also store array-based real world coordinates
that aren’t described by the WCS object.
These can be accessed via the sunraster.SpectrogramCube.extra_coords
property,
also inherited from NDCube
.
extra_coords
is particularly useful if
the temporal axis is convolved with space, as is the case for raster scans.
Therefore, if the WCS object only supplies (lat, lon) for the x-axis, the
timestamp of each exposure can be attached separately, e.g. as an
astropy.time.Time
object. extra_coords
is not restricted to timestamps. The user can supply any additional coordinate
as an astropy.units.Quantity
or other array-like.
Metadata that has a relationship with an axis but isn’t strictly a coordinate
can also be stored, e.g. the exposure time of each image.
(See Exposure Time Correction for more on SpectrogramCube
’s
handling of exposure times.)
To learn how to attach extra coordinates to a SpectrogramCube
instance
and how to access them once attached, see the
NDCube extra coordinates documentation
Coordinate Properties¶
For convenience, SpectrogramCube
provides shortcuts to the
four primary coordinates that define spectrogram data.
These are sunraster.SpectrogramCube.lon
, sunraster.SpectrogramCube.lat
,
sunraster.SpectrogramCube.spectral
, and sunraster.SpectrogramCube.time
which return the relevant coordinate values of each pixel.
Note that both sunraster.SpectrogramCube.lon
and sunraster.SpectrogramCube.lat
return 2-D data because longitude and latitude are couple dimensions.
These properties inspect the WCS and extra coords objects and locate where and
how the relevant coordinate information is stored.
This is possible only if the coordinate name is supported by sunraster
.
To see these supported names, see
sunraster.SpectrogramCube.SUPPORTED_LONGITUDE_NAMES
,
sunraster.spectrogram.SUPPORTED_LATITUDE_NAMES
,
sunraster.spectrogram.SUPPORTED_SPECTRAL_NAMES
, and
sunraster.spectrogram.SUPPORTED_TIME_NAMES
.
If the coordinate name cannot be found, these properties will raise an error.
If you think additional coordinate names should be supported,
please let us know by raising an issue on our GitHub repo.
In addition to the four primary coordinates, there is also a convenience
for the exposure time, sunraster.SpectrogramCube.exposure_time
.
The supported exposure time coordinate names can be found under
sunraster.spectrogram.SUPPORTED_EXPOSURE_NAMES
.
Dimensions¶
The dimensions
and
array_axis_physical_types
methods
enable users to inspect the shape and WCS axis types of the
SpectrogramCube
instance.
>>> my_spectrograms.dimensions
<Quantity [3., 4., 5.] pix>
>>> my_spectrograms.array_axis_physical_types
[('custom:pos.helioprojective.lat', 'custom:pos.helioprojective.lon'),
('custom:pos.helioprojective.lat', 'custom:pos.helioprojective.lon'),
('em.wl',)]
dimensions
returns a Quantity
giving the length of each dimension in pixel units while
array_axis_physical_types
returns an list of tuples where each tuple contains the types of physical properties
associated with each array axis. Since more than one physical type be associated with
an array axis because they are dependent, e.g. latitude/longitude, or because of the
rastering naturing of the instrument, e.g. latitude/longitude and time, the length of each
tuple can be greater than one.
The axis names are in accordance with the International Virtual Observatory Alliance (IVOA)
UCD1+ controlled vocabulary.
Slicing¶
SpectrogramCube
inherits a powerful and simple slicing API from NDCube
.
It enables users to access sub-regions of their data while simultaneously
slicing all relevent attributes including uncertainty, mask, wcs, extra_coords, etc.
Slicing in pixel space is achieved via the standard Python slicing API while a
separate API is provided for cropping a SpectrogramCube
instance by real
world coordinates.
See the
NDCube slicing documentation
to learn more.
Plotting¶
To quickly and easily visualize spectrograms,
SpectrogramCube
inherits a simple-to-use,
yet powerful plotting method from NDCube
.
It is intended to be a useful quicklook tool and not a
replacement for high quality plots or animations, e.g. for
publications. The plot method can be called very simply.
>>> my_spectrograms.plot()
This method produces different types of visualizations including line plots,
2-D images and 1- and 2-D animations.
Which is displayed depends on the dimensionality of the SpectrogramCube
and the inputs of the user.
For learn more about how to customize plots and animations through the
plot
method, see the
NDCubeSequence plotting documentation.
Exposure Time Correction¶
An important step in analyzing any form of photon-based observations is normalizing the data to the exposure time. This is important both for converting between instrumental and physical units, e.g. DN to energy, and comparing spectral features between exposure, e.g. line intensity.
SpectrogramCube
provides a simple API for performing this correction:
apply_exposure_time_correction
.
It requires that the exposure time is stored the WCS or as a Quantity
in the extra_coords
property.
Let’s recreate our spectrogram object again, but this time with exposure times of
0.5 seconds stored as an extra coordinate and a data unit of counts.
>>> import astropy.units as u
>>> exposure_times = np.ones(data.shape[0])/2 * u.s
>>> extra_coords_input = [("exposure time", 0, exposure_times)]
>>> my_spectrograms = SpectrogramCube(data, input_wcs, uncertainty=uncertainties,
... mask=mask, meta=meta, unit=u.ct,
... extra_coords=extra_coords_input)
Note that the API for supplying extra coordinates is an iterable of
tuples of the form (str
, int
, Quantity
or array-like).
The 0th entry gives the name of the coordinate, the 1st entry gives the data
axis to which the extra coordinate corresponds, and the 2nd entry
gives the value of that coordinate at each pixel along the axis.
Also note that the coordinate array must be the same length as its corresponding
data axis.
See the
NDCube extra coordinates documentation
for more.
Applying the exposure time correction is now simple.
>>> # First check the data unit and average data value before applying correction.
>>> print(my_spectrograms.unit, my_spectrograms.data.mean())
ct 1.0
>>> my_spectrograms = my_spectrograms.apply_exposure_time_correction() # Apply exposure time correction.
>>> # Confirm effect by checking data unit and average data value again.
>>> print(my_spectrograms.unit, my_spectrograms.data.mean())
ct / s 2.0
Notice that the average data value has been doubled and
the data unit is now counts per second.
This method alters not only the data, but also the uncertainty if any is supplied.
apply_exposure_time_correction
does not apply the scaling blindly, but first checks whether there is
a per second (1/s) component in the data unit.
If there is, it assumes that the correction has already been performed
and raises an error.
This helps users more easily keep track of whether they have applied the correction.
However, if for some reason there is a per second component that
doesn’t refer to the exposure time and the user still wants to apply the correction,
they can set the force
kwarg to override the check.
>>> print(my_spectrograms.unit, my_spectrograms.data.mean())
ct / s 2.0
>>> my_spectrograms = my_spectrograms.apply_exposure_time_correction(force=True)
>>> print(my_spectrograms.unit, my_spectrograms.data.mean())
ct / s2 4.0
Should users like to undo the correction, they can set the undo
kwarg.
>>> print(my_spectrograms.unit, my_spectrograms.data.mean())
ct / s2 4.0
>>> my_spectrograms = my_spectrograms.apply_exposure_time_correction(undo=True, force=True)
>>> my_spectrograms = my_spectrograms.apply_exposure_time_correction(undo=True) # Undo correction twice.
>>> print(my_spectrograms.unit, my_spectrograms.data.mean())
ct 1.0
As before, apply_exposure_time_correction
only undoes the
correction if there is a time component in the unit.
And again as before, users can override this check by setting the force
kwarg.
>>> print(my_spectrograms.unit, my_spectrograms.data.mean())
ct 1.0
>>> my_spectrograms = my_spectrograms.apply_exposure_time_correction(undo=True, force=True)
>>> print(my_spectrograms.unit, my_spectrograms.data.mean())
ct s 0.5
SpectrogramSequence¶
In some cases, a series of spectrograms may not be describable by a
single set of WCS transformations.
However, it still may make sense to combine them in order along a dimension.
This is the purpose of the SpectrogramSequence
class.
It stores a sequence of SpectrogramCube
instances and provides
equivalent or analagous APIs so users can interact with the data as if it were
a single data cube.
SpectrogramSequence
inherits from NDCubeSequence
and
so inherits much of the same API.
Initialization¶
To initialize a SpectrogramSequence
, we first need spectrograms
stored in multiple SpectrogramCube
instances.
Let’s create some using what we learned in the SpectrogramCube section and include
timestamps and exposure times as extra coordinates.
>>> from datetime import datetime, timedelta
>>> import numpy as np
>>> import astropy.wcs
>>> import astropy.units as u
>>> from astropy.nddata import StdDevUncertainty
>>> from astropy.time import Time
>>> from sunraster import SpectrogramCube
>>> # Define primary data array and WCS object.
>>> data = np.ones((3, 4, 5))
>>> wcs_input_dict = {
... 'CTYPE1': 'WAVE ', 'CUNIT1': 'Angstrom', 'CDELT1': 0.2, 'CRPIX1': 0, 'CRVAL1': 10, 'NAXIS1': 5,
... 'CTYPE2': 'HPLT-TAN', 'CUNIT2': 'deg', 'CDELT2': 0.5, 'CRPIX2': 2, 'CRVAL2': 0.5, 'NAXIS2': 4,
... 'CTYPE3': 'HPLN-TAN', 'CUNIT3': 'deg', 'CDELT3': 0.4, 'CRPIX3': 2, 'CRVAL3': 1, 'NAXIS3': 3}
>>> input_wcs = astropy.wcs.WCS(wcs_input_dict)
>>> # Define a mask with all pixel unmasked, i.e. mask values = False
>>> mask = np.zeros(data.shape, dtype=bool)
>>> # Define uncertaines for data, 2*data and data/2.
>>> uncertainties = StdDevUncertainty(np.sqrt(data))
>>> uncertainties2 = StdDevUncertainty(np.sqrt(data * 2))
>>> uncertainties05 = StdDevUncertainty(np.sqrt(data * 0.5))
>>> # Define exposure times.
>>> exposure_times = np.ones(data.shape[0])/2 * u.s
>>> axis_length = int(data.shape[0])
>>> # Create 1st cube of spectrograms.
>>> timestamps0 = Time([datetime(2000, 1, 1) + timedelta(minutes=i)
... for i in range(axis_length)], format='datetime', scale='utc')
>>> extra_coords_input0 = [("time", 0, timestamps0), ("exposure time", 0, exposure_times)]
>>> spectrograms0 = SpectrogramCube(data, input_wcs, uncertainty=uncertainties, mask=mask,
... meta=meta, unit=u.ct, extra_coords=extra_coords_input0)
>>> # Create 2nd cube of spectrograms.
>>> timestamps1 = Time([timestamps0[-1].to_datetime() + timedelta(minutes=i)
... for i in range(1, axis_length+1)], format='datetime', scale='utc')
>>> extra_coords_input1 = [("time", 0, timestamps1), ("exposure time", 0, exposure_times)]
>>> spectrograms1 = SpectrogramCube(data*2, input_wcs, uncertainty=uncertainties2, mask=mask,
... meta=meta, unit=u.ct, extra_coords=extra_coords_input1)
>>> # Create 3rd cube of spectrograms.
>>> timestamps2 = Time([timestamps1[-1].to_datetime() + timedelta(minutes=i)
... for i in range(1, axis_length+1)], format='datetime', scale='utc')
>>> extra_coords_input2 = [("time", 0, timestamps2), ("exposure time", 0, exposure_times)]
>>> spectrograms2 = SpectrogramCube(data*0.5, input_wcs, uncertainty=uncertainties05, mask=mask,
... meta=meta, unit=u.ct, extra_coords=extra_coords_input2)
If we choose, we can define some sequence-level metadata in addition to any metadata attached to the individual raster scans:
>>> seq_meta = {"description": "This is a SpectrogramSequence."}
To create a SpectrogramSequence
, simply supply the class with a
list of SpectrogramCube
instances.
>>> from sunraster import SpectrogramSequence
>>> my_sequence = SpectrogramSequence([spectrograms0, spectrograms1, spectrograms2],
... meta=seq_meta)
Dimensions¶
In order to inspect the dimensionlity of our sequence and the physical properties
to which the axes correspond, we can use the
dimensions
and
array_axis_physical_types
properties.
>>> my_sequence.dimensions
(<Quantity 3. pix>, <Quantity 3. pix>, <Quantity 4. pix>, <Quantity 5. pix>)
>>> my_sequence.array_axis_physical_types
[('meta.obs.sequence',),
('custom:pos.helioprojective.lat', 'custom:pos.helioprojective.lon'),
('custom:pos.helioprojective.lat', 'custom:pos.helioprojective.lon'),
('em.wl',)]
Note that this is the same API as SpectrogramCube
except that
sunraster.SpectrogramSequence.dimensions
returns an iterable of
Quantity
objects, one for each axis.
This is because of its inheritance from NDCubeSequence
rather than NDCube
.
Also note that there are now four dimensions, as the sequence is treated
as though it were an additional data axis.
This can be very helpful if you have a series of 2D spectrograms and
want to use the sequence axis to represent time.
sunraster.SpectrogramSequence.array_axis_physical_types
returns a list of tuples of the same IVOA UCD1+ controlled words
used by sunraster.SpectrogramCube.array_axis_physical_types
.
The sequence axis is given the label 'meta.obs.sequence'
.
Coordinates¶
Coordinate Properties¶
Just like SpectrogramCube
, SpectrogramSequence
provides convenience properties to retrieve the real world coordinate values
for each pixel along each axis, namely
sunraster.SpectrogramSequence.lon
, sunraster.SpectrogramSequence.lat
,
sunraster.SpectrogramSequence.spectral
, sunraster.SpectrogramSequence.time
and
sunraster.SpectrogramSequence.exposure_time
.
Since there is no guarantee that SpectrogramCube
’s WCS transformations
are consistent between SpectrogramCube
s, sunraster.SpectrogramCube.lon
and sunraster.SpectrogramCube.lat
return 3-D Quantity
instances
and sunraster.SpectrogramCube.spectral
returns a 2-D Quantity
where the additional dimension represent the coordinates for different
SpectrogramCube
instances.
Exposure Time Correction¶
Analogous to SpectrogramCube
, SpectrogramSequence
also provides a apply_exposure_time_coorection
method. This is simply a wrapper around the SpectrogramCube
version
that saves users from apply or removing the exposure time correction to each
SpectrogramCube
manually. To remind yourself how that method works,
see the SpectrogramCube
Exposure Time Correction section.
Slicing¶
SpectrogramSequence
provides an identical slicing API to
SpectrogramCube
.
Although recall that a SpectrogramSequence
has an additional dimension.
As with SpectrogramCube
, the slicing API manipulates not only the
data, but also all relevant supporting metadata including
uncertainties, mask, WCS object, extra_coords, etc.
To slice a SpectrogramSequence
, simply do:
>>> my_sequence_roi = my_sequence[1:3, 0:2, 1:3, 1:4]
We can check the effect of the slicing via the
dimensions
property.
>>> print(my_sequence.dimensions) # Check dimensionality before slicing.
(<Quantity 3. pix>, <Quantity 3. pix>, <Quantity 4. pix>, <Quantity 5. pix>)
>>> print(my_sequence_roi.dimensions) # See how slicing has changed dimensionality.
(<Quantity 2. pix>, <Quantity 2. pix>, <Quantity 2. pix>, <Quantity 3. pix>)
Slicing can reduce the dimensionality of SpectrogramSequence
instances.
For example, let’s slice out the 2nd pixel along the slit.
>>> my_3d_sequence = my_sequence[:, :, 2]
>>> print(my_3d_sequence.dimensions)
(<Quantity 3. pix>, <Quantity 3. pix>, <Quantity 5. pix>)
Plotting¶
To quickly and easily visualize slit spectrograph data,
SpectrogramSequence
supplies a simple, yet powerful plotting API.
It is intended as a useful quicklook tool and not a replacement
for high quality plots or animations, e.g. for publications or presentations.
>>> my_sequence.plot()
As with SpectrogramCube
, this method produces different types of
visualizations including line plots, 2-D images and 1- and 2-D animations.
Which is displayed depends on the dimensionality of the SpectrogramSequence
and the inputs of the user.
For learn more about how to customize plots and animations through the
plot
method, see the
NDCubeSequence plotting documentation.
RasterSequence¶
Slit spectrographs are often used to produce rasters.
(In fact, it is from this data product that sunraster
derives its name.)
A raster is produced by scanning the slit in discrete steps perpendicular
to its long axis, recording an exposure at each position.
Thus a spectral image over a region is built up over time despite the
slit spectrograph’s necessarily narrow horizontal field of view.
Another motivation can be to perform fast repeat raster scans in order to improve
the chances of catching an event with the slit, e.g. a solar flare.
In a raster, the slit-step axis is convolved with time.
Depending on the type of analysis being performed,
users may want to think of their data as if it were in
raster mode/4D (scan number
, slit step
, position along slit
, wavelength
)
or sit-and-stare mode/3D (time
, position along slit
, spectral
).
In order to access the data in the way they want, scientists may often have two
copies, a 3D version and a 4D version.
However, this means scientists have to keep track of two data structures which is
memory intensive both for the scientist and the computer and increases the chances
mistakes in analysis.
Solving this problem is the purpose of the RasterSequence
class.
It inherits from SpectrogramSequence
but enables users to label
one of the axes as the slit-step axis.
This in turn facilitates a new set of APIs which allows users to interact with their data
in sit-and-stare (SnS) or rastering mode seemlessly and interchangeably
without having to reformat their data.
Initialization¶
A RasterSequence
, is instantiated just like a SpectrogramCube
.
Let’s first create some SpectrogramCube
instances where each represents a
single raster scan.
As before, we will add the timestamps and exposure times as extra coordinates.
>>> import numpy as np
>>> import astropy.wcs
>>> import astropy.units as u
>>> from astropy.nddata import StdDevUncertainty
>>> from datetime import datetime, timedelta
>>> from astropy.time import Time
>>> from sunraster import SpectrogramCube
>>> # Define primary data array and WCS object.
>>> data = np.ones((3, 4, 5))
>>> wcs_input_dict = {
... 'CTYPE1': 'WAVE ', 'CUNIT1': 'Angstrom', 'CDELT1': 0.2, 'CRPIX1': 0, 'CRVAL1': 10, 'NAXIS1': 5,
... 'CTYPE2': 'HPLT-TAN', 'CUNIT2': 'deg', 'CDELT2': 0.5, 'CRPIX2': 2, 'CRVAL2': 0.5, 'NAXIS2': 4,
... 'CTYPE3': 'HPLN-TAN', 'CUNIT3': 'deg', 'CDELT3': 0.4, 'CRPIX3': 2, 'CRVAL3': 1, 'NAXIS3': 3}
>>> input_wcs = astropy.wcs.WCS(wcs_input_dict)
>>> # Define a mask with all pixel unmasked, i.e. mask values = False
>>> mask = np.zeros(data.shape, dtype=bool)
>>> # Define some RasterSequence metadata.
>>> seq_meta = {"description": "This is a RasterSequence."}
>>> # Define uncertaines for data, 2*data and data/2.
>>> uncertainties = StdDevUncertainty(np.sqrt(data))
>>> uncertainties2 = StdDevUncertainty(np.sqrt(data * 2))
>>> uncertainties05 = StdDevUncertainty(np.sqrt(data * 0.5))
>>> # Define exposure times.
>>> exposure_times = np.ones(data.shape[0])/2 * u.s
>>> axis_length = int(data.shape[0])
>>> # Create 1st raster
>>> timestamps0 = Time([datetime(2000, 1, 1) + timedelta(minutes=i)
... for i in range(axis_length)], format='datetime', scale='utc')
>>> extra_coords_input0 = [("time", 0, timestamps0), ("exposure time", 0, exposure_times)]
>>> raster0 = SpectrogramCube(data, input_wcs, uncertainty=uncertainties, mask=mask,
... meta=meta, unit=u.ct, extra_coords=extra_coords_input0)
>>> # Create 2nd raster
>>> timestamps1 = Time([timestamps0[-1].to_datetime() + timedelta(minutes=i)
... for i in range(1, axis_length+1)], format='datetime', scale='utc')
>>> extra_coords_input1 = [("time", 0, timestamps1), ("exposure time", 0, exposure_times)]
>>> raster1 = SpectrogramCube(data*2, input_wcs, uncertainty=uncertainties, mask=mask,
... meta=meta, unit=u.ct, extra_coords=extra_coords_input1)
>>> # Create 3rd raster
>>> timestamps2 = Time([timestamps1[-1].to_datetime() + timedelta(minutes=i)
... for i in range(1, axis_length+1)], format='datetime', scale='utc')
>>> extra_coords_input2 = [("time", 0, timestamps2), ("exposure time", 0, exposure_times)]
>>> raster2 = SpectrogramCube(data*0.5, input_wcs, uncertainty=uncertainties, mask=mask,
... meta=meta, unit=u.ct, extra_coords=extra_coords_input2)
The last thing we need to do before creating our RasterSequence
is
to identity the slit-step of the SpectrogramCube
s.
In the above raster
instances both the 0th and 1st axes correspond to spatial dimensions.
Therefore let’s define the 0th axes as the slit-step.
We will do this by setting the common_axis
argument 0.
>>> from sunraster import RasterSequence
>>> my_rasters = RasterSequence([raster0, raster1, raster2], common_axis=0, meta=seq_meta)
Dimensions¶
RasterSequence
provides a version of the
array_axis_physical_axis_types
property for
both raster and SnS representations.
>>> my_rasters.raster_array_axis_physical_types
[('meta.obs.sequence',),
('custom:pos.helioprojective.lat', 'custom:pos.helioprojective.lon'),
('custom:pos.helioprojective.lat', 'custom:pos.helioprojective.lon'),
('em.wl',)]
>>> my_rasters.SnS_array_axis_physical_types
[('custom:pos.helioprojective.lat', 'custom:pos.helioprojective.lon'),
('custom:pos.helioprojective.lat', 'custom:pos.helioprojective.lon'),
('em.wl',)]
In the raster case, 'meta.obs.sequence'
represents the raster scan number axis.
For those familiar with NDCubeSequence
, these are simply aliases for the
array_axis_physical_axis_types
and
cube_like_world_axis_physical_axis_types
, respectively.
The length of each axis can also be displayed in either the raster or SnS representation.
>>> my_rasters.raster_dimensions
(<Quantity 3. pix>, <Quantity 3. pix>, <Quantity 4. pix>, <Quantity 5. pix>)
raster_dimensions
always represents the length of
the scan number axis in the 0th position.
We can therefore see that we have 3 raster scans in our RasterSequence
.
This means that the slit-step axis is shifted by one.
Since we defined common_axis=0
during instantiation, this means that the
length of the slit-step can be found in the 1st element.
From this we can see that we have 3 slit positions per raster scan.
To see the length of the axes as though the data is in sit-and-stare mode, simply do:
>>> my_rasters.SnS_dimensions
<Quantity [9., 4., 5.] pix>
Note that scan number and slit-step axes have been combined into the 0th position.
From this we can see that we have 9 (3x3) spectrograms or times in our
RasterSequence
.
Coordinates¶
Coordinate Properties¶
RasterSequence
provides the same convenience
properties as SpectrogramSequence
to retrieve the real world
coordinate values for each pixel along each axis.
sunraster.RasterSequence.lon
, sunraster.RasterSequence.lat
,
and sunraster.RasterSequence.spectral
return their values in the raster
representation while sunraster.RasterSequence.time
and
sunraster.RasterSequence.exposure_time
return their values in the SnS
representation.
SnS Axis Extra Coordinates¶
As well as time
and
exposure_time
, some
sunraster.SpectrogramCube.extra_coords
may contain other coordinates
that are aligned with the slit step axis.
The sunraster.RasterSequence.SnS_axis_extra_coords
property enables users
to access these coordinates at the RasterSequence
level in the
form of an abbreviated extra_coords
dictionary.
Just like time
and sunraster.RasterSequence.exposure_time
,
the coordinates are concatenated so they mimic the sit-and-stare-like dimensionality
returned in the 0th element of sunraster.RasterSequence.SnS_dimensions
.
sunraster.RasterSequence.SnS_axis_extra_coords
is equivalent to
ndcube.NDCubeSequence.common_axis_extra_coords
.
To see examples of how to use this property, see the
NDCubeSequence Common Axis Extra Coordinates documentation.
Raster Axis Extra Coordinates¶
Analgous to SnS_axis_extra_coords
, it is also
possible to access the extra coordinates that are not assigned to any
SpectrogramCube
data axis via the
raster_axis_extra_coords
property.
Whereas SnS_axis_extra_coords
returns all the
extra coords with an 'axis'
value equal to the time/slit step axis,
scan_axis_extra_coords
returns all extra coords
with an 'axis'
value of None
.
Another way of thinking about an extra_coord
with and axis value of None
,
is that these coordinates correspond to the raster scan number axis.
Hence the property’s name.
Slicing¶
RasterSequence
not only enables users to inspect their data in
the raster and sit-and-stare representations.
It also enables them to slice the data in either representation as well.
This is done via the slice_as_raster
and
slice_as_SnS
properties.
As with SpectrogramCube
and SpectrogramSequence
,
these slicing properties ensure that not only the data is sliced,
but also all relevant supporting metadata including uncertainties, mask,
WCS object, extra_coords, etc.
To slice a RasterSequence
using the raster representation, do:
>>> my_rasters_roi = my_rasters.slice_as_raster[1:3, 0:2, 1:3, 1:4]
We can see the result of slicing using the dimensions
properties.
>>> print(my_rasters.raster_dimensions) # Check dimensionality before slicing.
(<Quantity 3. pix>, <Quantity 3. pix>, <Quantity 4. pix>, <Quantity 5. pix>)
>>> print(my_rasters_roi.raster_dimensions) # See how slicing has changed dimensionality.
(<Quantity 2. pix>, <Quantity 2. pix>, <Quantity 2. pix>, <Quantity 3. pix>)
>>> my_rasters_roi.SnS_dimensions # Dimensionality can still be represented in SnS form.
<Quantity [4., 2., 3.] pix>
To slice in the sit-and-stare representation, do the following:
>>> my_rasters_roi = my_rasters.slice_as_SnS[1:7, 1:3, 1:4]
Let’s check the effect of the slicing once again.
>>> print(my_rasters.SnS_dimensions) # Check dimensionality before slicing.
[9. 4. 5.] pix
>>> print(my_rasters_roi.SnS_dimensions) # See how slicing has changed dimensionality.
[6. 2. 3.] pix
>>> print(my_rasters_roi.raster_dimensions) # Dimensionality can still be represented in raster form.
(<Quantity 3. pix>, <Quantity [2., 3., 1.] pix>, <Quantity 2. pix>, <Quantity 3. pix>)
Notice that after slicing the data can still be inspected and interpreted in
the raster or sit-and-stare format, irrespective of which slicing
representation was used.
Also notice that the my_sequence.slice_as_SnS[1:7, 1:3, 1:4]
command led to
different SpectrogramCube
objects to have different lengths along the
slit step axis.
This can be seen from the fact that the slit step axis entry in the
output of my_sequence_roi.raster_dimensions
has a length greater than 1.
Each element represents the length of each SpectrogramCube
in the
SpectrogramSequence
along that axis.
As with SpectrogramSequence
, slicing can reduce a
RasterSequence
’s dimensionality.
As in the Exposure Time Correction section, let’s slice out the 2nd pixel along the slit.
This reduces the number of dimensions in the raster representation to 3
(raster scan
, slit step
, spectral
) and to 2 in the sit-and-stare
representation (time
, spectral
).
However, the raster and sit-and-stare representations are still valid.
>>> slit_pixel_rasters = my_rasters.slice_as_raster[:, :, 2]
>>> print(slit_pixel_rasters.raster_dimensions)
(<Quantity 3. pix>, <Quantity 3. pix>, <Quantity 5. pix>)
>>> print(slit_pixel_rasters.SnS_dimensions)
[9. 5.] pix
This demonstrates that the difference between the raster and sit-and-stare representations is more subtle than simply a 4-D or 3-D dimensionality. The difference is whether the raster scan and slit step axes are convolved into a time axis or whether they are represented separately. And because of this definition, the raster and sit-and-stare representations are valid and accessible for any dimensionality in which the raster scan and slit step axes are maintained.
Plotting¶
To quickly and easily visualize slit spectrograph data,
RasterSequence
supplies simple-to-use, yet powerful plotting APIs.
They are intended to be a useful quicklook tool and not a
replacement for high quality plots or animations, e.g. for
publications.
As with slicing, there are two plot methods for plotting in each of the
raster and sit-and-stare representations.
To visualize in the raster representation, simply call the following:
>>> my_rasters.plot_as_raster()
To visualize in the sit-and-stare representation, do:
>>> my_rasters.plot_as_SnS()
These methods produce different types of visualizations including line plots,
2-D images and 1- and 2-D animations.
Which is displayed depends on the dimensionality of the RasterSequence
and the inputs of the user.
plot_as_raster
and
plot_as_SnS
are in fact simply aliases for the
ndcube.NDCubeSequence.plot
and ndcube.NDCubeSequence.plot_as_cube
methods,
respectively.
For learn more about how these routines work and the optional inputs that
enable users to customize their output, see the
NDCubeSequence plotting documentation.
Extracting Data Arrays¶
It is possible that you may have some procedures that are designed to operate
on arrays instead of SpectrogramSequence
or RasterSequence
objects.
Therefore it may be useful to extract the data (or other array-like information
such as uncertainty
or mask
) into a single ndarray
.
A succinct way of doing this operation is using python’s list comprehension.
To make a 4-D array from the data arrays in my_sequence
, use numpy.stack
.
>>> print(my_sequence._dimensions) # Print sequence dimensions as a reminder.
(<Quantity 3. pix>, <Quantity 3. pix>, <Quantity 4. pix>, <Quantity 5. pix>)
>>> data = np.stack([cube.data for cube in my_sequence.data])
>>> print(data.shape)
(3, 3, 4, 5)
To define a 3D array where the data arrays of each SpectrogramCube
in the sequence is concatenated along an axis, use numpy.vstack
.
>>> data = np.vstack([cube.data for cube in my_sequence.data])
>>> print(data.shape)
(9, 4, 5)
To create 3D arrays by slicing sequences, do:
>>> data = np.stack([cube[2].data for cube in my_sequence.data])
>>> print(data.shape)
(3, 4, 5)
Spectrogram Collections¶
During analysis of slit spectrograph data, it is often desirable to group
different data sets together.
For example, you may have several SpectrogramCube
or
RasterSequence
objects representing observations in different
spectral windows.
Or we may have fit a spectral line in each pixel and extracted a property
such as linewidth, thus collapsing the spectral axis.
In both these cases, the RasterSequence
objects share a common
origin and set of coordinate transformations with the original observations
(except in the spectral axis in the latter example).
However, they do not have a sequential relationship in their common coordinate spaces
and in the latter case the data represents a different physical property to the
original observations.
Therefore, combining them in a RasterSequence
is not appropriate.
sunraster
does not provide a suitable object for this purpose.
However, because SpectrogramCube
SpectrogramSequence
and RasterSequence
are instances of ndcube
classes underneath,
users can employ the ndcube.NDCollection
class for this purpose.
NDCollection
is a dict
-like class that provides additional slicing
capabilities of its constituent data cubes along aligned axes.
To see whether NDCollection
could be helpful for your research, see
the
NDCollection documentation.