This package provides tools to work with Cassini UVIS data.

How to install

It is recommended to manage your Python environments with conda or its new and faster sibling mamba.

After installation of mamba/conda, create a new Python environment with the name py38 (or whatever name you prefer) with:

conda create -n py38 python=3.8 -c conda-forge

If you need a refresher or intro into managing your Python environment with conda, have a look at my tutorials here (long version) and here (short version).

Once you have created the new environment, activate it:

conda activate py38

(or your \ name)

and install the latest requirements for the package:

mamba install -c conda-forge hvplot xarray pandas

followed by a pip install for this package (a pyuvis conda package will be created later):

pip install pyuvis

The package depends on planetarypy to organize data discovery, download, and local data management and the pip install should pull in planetarypy with the minimum version of 0.15.

How to use

When launched for the first time, the planetarypy package will ask for a general storage path for all data managed by planetarypy. You should give it a folder path where you have sufficient space to store the planetary data you want to work with.

The different available modules were born out of special use cases for analysis of UVIS data, but for the general user the most important classes are UVPDS and the UVISObs.

UVPDS

The UVPDS class is designed to deal with exactly one PDS product for either EUV or FUV data:

from pyuvis import UVPDS
pid = "FUV2005_172_03_35"
uv = UVPDS(pid)

If the data file does not exist on your local storage yet, planetarypy will go ahead and download it for you and provide the local path to UVPDS for reading it.

You don't have to do anything to get the data and you don't have to decide anything about where to store the data. :) If you are interested in seeing how the planetarypy.uvis module achieves this, have a look at https://michaelaye.github.io/nbplanetary/cassini_uvis

While the data qube itself is accessible as a numpy.array via the data attribute:

uv.data[100]
array([[0, 0, 0, ..., 0, 2, 1],
       [0, 1, 0, ..., 0, 1, 0],
       [0, 0, 0, ..., 0, 1, 2],
       ...,
       [0, 0, 1, ..., 0, 0, 0],
       [0, 1, 0, ..., 0, 1, 0],
       [0, 0, 0, ..., 0, 0, 0]], dtype=uint16)

... it is recommended to use the xarray attribute for a more advanced data container with labeled coordinate axes

("spectral", "spatial", "samples") :

uv.xarray
<xarray.DataArray 'FUV2005_172_03_35' (spectral: 1024, spatial: 60, samples: 164)>
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Coordinates:
  * spectral  (spectral) float64 111.5 111.6 111.7 111.8 ... 191.1 191.2 191.3
  * spatial   (spatial) int64 2 3 4 5 6 7 8 9 10 ... 53 54 55 56 57 58 59 60 61
  * samples   (samples) int64 1 2 3 4 5 6 7 8 ... 158 159 160 161 162 163 164
Attributes:
    units:                 Counts
    long_name:             FUV raw data
    n_bands:               1024
    integration_duration:  Quantity(value=240.0, units='SECOND')

One can see that the spatial range was already reduced to the valid range of 2..61 according to the label data.

Note that the sample count starts at 1, to be fitting to the UVIS manual.

I usually story the xarray.DataArray into a da or arr variable for further analysis and plotting:

arr = uv.xarray

A built-in example plot method shows you how to use hvplot to generate interactive plots:

uv.plot()

The widget to the right allows you to scroll through the samples dimension.

Hint:You can look at the source code of any method using the double question mark:

uv.plot??
Signature:
uv.plot(
    precise: bool = False,
    percentiles: tuple = (0.5, 99.5),
    clim: tuple = None,
    cmap: str = 'viridis',
    calibrated=False,
)
Source:   
    def plot(
        self,
        precise: bool = False,  # switch to choose more precise quadmesh plot
        percentiles: tuple = (
            0.5,
            99.5,
        ),  # percentiles to be applied as minimum/maximum stretch
        clim: tuple = None,  # Set the visual stretch manually instead of via percentiles
        cmap: str = "viridis",  # default colormap. Other nice ones are 'plasma' or 'magma'
        calibrated = False,  # switch to control if to plot raw or calibrated data
    ):
        """Create default hvplot for the data.

        Due to non-equidistant wavelengths, one should use the quadmesh plot,
        but that is less performant than a constant raster and creates an annoying
        aliasing structure when zoomed out (however correct, though).

        I am investigating if that aliasing can be avoided, it might come from gridlines.
        So I leave it to the user to switch to the raster plot using the `precise` switch."""
        data = self.xarray if not calibrated else self.calibrated
        # define good stretch by using percentiles:
        stretch = (
            tuple(np.percentile(data, percentiles)) if clim is None else clim
        )
        # use `frameswise=False` to allow zooming in survice scrolling over samples
        kwargs = dict(
            x="spectral",
            y="spatial",
            framewise=False,
            cmap=cmap,
            clim=stretch,
            clabel=data.attrs['units'],
            title=data.attrs['long_name'],
        )
        if precise:
            kwargs["kind"] = "quadmesh"
        return data.hvplot(**kwargs).opts(gridstyle={"grid_line_color": "white"}).opts(axiswise=True)
File:      ~/Dropbox/src/pyuvis/pyuvis/io.py
Type:      method

Plot style

I chose to use the less precise (due to our non-equidistant wavelengths) raster plot by default, instead of the more precise quadmesh, because the latter creates an annoying aliasing effect,possibly due to gridlines, I'm am investigating.

If you need to be really precise on the wavelength position, use the precise=True keyword:

uv.plot(precise=True)

Control the visual limits using the percentile stretch or the clim parameter directly:

uv.plot(percentiles=(2, 98))
uv.plot(clim=(None, 10))

Control the colormap using cmap:

uv.plot(precise=False, cmap='magma')

All the options added to the UVPDS.plot method can be applied to the xarray directly, if you need the direct control.

For example, you can select a specific sample using the xarray.sel method and plot only that:

arr.sel(samples=100).hvplot(x='spectral', y='spatial', cmap='viridis')

One can also apply a video scrubber to the samples instead:

np.percentile(arr, (1, 99))  # determine good color stretch for inner 98% of the data
array([ 0., 55.])
arr.hvplot(
    x="spectral",
    y="spatial",
    widget_type="scrubber",
    widget_location="bottom",
    clim=(0, 55),
    cmap="viridis",
)

Calibrated data

Calibration masks are being downloaded automatically by the planetarypy.uvis module.

It is applied following the qube description in the calibration data label:

uv.caliblabel.QUBE.DESCRIPTION
'This UVIS Qube contains a (1,024x64) matrix of 4 byte IEEE reals. This matrix is made up of calibration values used to convert raw detector counts into rayleighs. Use these instructions to generate calibrated data: 1. multiply each sample of the raw data by the matrix in this object. 2. multiply each product by the CORE_MULTIPLIER value defined in this object The resulting object contains calibrated data in units of kiloRayleighs. CORE_NULL values correspond to pixels which do not contain valid data and which should not be used in any application of calibrated data. CORE_NULL values can be eliminated by interpolation which is a process described in SOFTWARE/CALIB/UVIS_INVALID_PIXEL_DISCUSSION.DOC. The data in this Qube was produced using the algorithm found in: GET_FUV_2010_LAB_CALIBRATION.PRO and data found in: FUV_1999_LAB_CAL.DAT'
uv.calibrated
<xarray.DataArray 'FUV2005_172_03_35' (spectral: 1024, spatial: 60, samples: 164)>
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... -0.0 -0.0 -0.0 -0.0 -0.0 -0.0 -0.0 -0.0
Coordinates:
  * spectral  (spectral) float64 111.5 111.6 111.7 111.8 ... 191.1 191.2 191.3
  * spatial   (spatial) int64 2 3 4 5 6 7 8 9 10 ... 53 54 55 56 57 58 59 60 61
  * samples   (samples) int64 1 2 3 4 5 6 7 8 ... 158 159 160 161 162 163 164
Attributes:
    units:                 kiloRayleighs
    long_name:             FUV calibrated data
    n_bands:               1024
    integration_duration:  Quantity(value=240.0, units='SECOND')

Plot the calibrated data by using the calibrated=True option to plot():

uv.plot(calibrated=True, clim=(0, None))

Labels

One can get to the labels via the datalabel or caliblabel attribute:

uv.caliblabel.SLIT_STATE
'LOW_RESOLUTION'
uv.datalabel.SC_PLANET_POSITION_VECTOR
(#3) [Quantity(value=856763.0, units='KM'),Quantity(value=1976970.8, units='KM'),Quantity(value=386939.91, units='KM')]
uv.cal_data.data[uv.cal_data.data == -1] = np.nan

UVISObs

The UVISObs class is just a convenience class that provides quick access to both the EUV and FUV version of a UVIS Observation with the same start time.

One can conveniently initiate it with only the yyyy_jjj_hh_mm time string or with the full PDS product_id of either a FUV or EUV file:

from pyuvis import UVISObs
pid
'FUV2005_172_03_35'
obs = UVISObs(pid)
obs.euv.datalabel.PRODUCT_ID
'EUV2005_172_03_35'
obs.fuv.datalabel.PRODUCT_ID
'FUV2005_172_03_35'

hvplot / holoviews

hvplot is an entry interface to the holoviews plotting framework. One of the design principles of holoviews is re-usability, which makes combining existing plots a breeze.

What happens below is that '+'-ing two plots creates a Layout object consisting of the two DynamicMap objects, dynamic because of the scroll widget through the samples.

This layout object can now be rendered with additional options:

layout = obs.euv.plot() + obs.fuv.plot()
print(layout)
:Layout
   .DynamicMap.I  :DynamicMap   [samples]
   .DynamicMap.II :DynamicMap   [samples]
layout
layout.cols(1)

Found issues?

Please report any issues found at the GitHub issue tracker for this project:

https://github.com/Cassini-UVIS/pyuvis/issues