Basic Principles

Let’s imagine you want to do something slightly more complicated. Maybe you want to make a movie, and include an linked time series plot next to it. Maybe you want to include several movie frames at different cadences. Maybe you want to create your own snazzy custom format. To do so, there are three core concepts you need to understand.

a Sequence is a collection of data, with times

To make an animation, we need a data structure that has some kind of data that has a time axis associated with it. For example, here we make a Sequence out of a small postage stamp of imaging data.

In [1]:
from playground.tv import *
In [2]:
from playground.cartoons import create_test_stamp
stamp = create_test_stamp(N=10, seed=42)

The make_sequence function is a general wrapper that does a decent job of figuring out what kind of sequence should be made out of the given inputs.

In [3]:
seq = make_sequence(stamp)

make_sequence should be able to handle at least the following types of input: + single FITS filename, and an ext_image= keyword for the extension to use + list of FITS filenames, and an ext_image= keyword + a glob pattern to search for FITS files, and an ext_image= keyword + single FITS HDUList, and an ext_image= keyword + list of loaded FITS HDULists, and an ext_image= keyword + a Stamp object from the cosmics package

Each sequence has a .time attribute. It is an astropy.time object, and can be indexed as an array. If times are fake (for example, if they are simply image number or cadence number), then we pretend (artificially) that the times are gps format with a cadence of 1s.

In [4]:
print("""
{}
 has {} times defined,
 spanning {}
       to {}
 with a cadence of {:.2}""".format(
    seq, len(seq.time), seq.time[0], seq.time[-1], seq.cadence().to('s')))

<stamp-sequence of 10 images>
 has 10 times defined,
 spanning 2018-01-01 00:00:00.000
       to 2018-01-01 00:00:18.000
 with a cadence of 2.0 s

Each sequence can be indexed directly with [i] to extract the ith element of the dataset. Different types of sequences will store data in different ways. For a small stamp, it will generally load the entire 3D (x,y,t) dataset into memory. For a sequence of large FITS images, it will generally only load individual FITS images on the fly as needed, to preserve memory. These different background behaviors should hopefully remain hidden to the casual user.

In [5]:
fi, ax = plt.subplots(1,3, figsize=(10,3))
for i in range(3):
    ax[i].imshow(seq[i])
    ax[i].set_title(seq.time[i])
_images/basics_10_0.png

a Frame is a plot panel, which can display a Sequence

A frame is a container in which a sequence can be displayed. Different frames can display different views, even of the same dataset. For example, here we create two different frames that include our little sequence seq as their data.

In [6]:
normal = imshowFrame(name='cecelia',
                     data=seq)
normal
Out[6]:
<imshow Frame | data=<stamp-sequence of 10 images> | name=cecelia>

Below, we can create a different frame using the same data, but by including the processingsteps keyword, it will subtract the mean image from each individual image before displaying it.

In [7]:
subtracted = imshowFrame(name='henrietta',
                         data=seq,
                         title='(median subtracted)',
                         processingsteps=['subtractmean'])
subtracted
Out[7]:
<imshow Frame | data=<stamp-sequence of 10 images> | name=henrietta>

We will be able to visualize these frames once they are included in an illustration.

an Illustration is a collection of Frames, with its own layout

Once we have created our frames, we can include them in an illustration. The illustration handles not only the basic grid layout of the frames, but also synchronizing their frames when plotting an animation. When you call the illustration’s .plot method, it will make all the constituent frames plot too. When you call the .animate method, it will make an animation by looping through a grid of times and updating each frame whenever it needs updating.

In [ ]:
i = GenericIllustration(imshows=[normal, subtracted])
i.plot()
i.animate('_static/a-pair-of-frames.mp4')

Looking back at the normal and subtracted frames that we defined, we can see a few features. The data and times for the two frames are identical, so the images update synchronously at each timestep of the animation. The subtracted frame looks like noise scattered around 0, as we would expect for having subtracted off the mean image. We gave subtracted a custom title, but normal fell back on an initial best guess.

If you want, you can access the individual frames inside an illustration through the .frames dictionary. This may come in handy if you want to modify the attributes of a frame once it’s in an illustration, or change something in how it is plotted after calling i.plot(). (See examples below.)

In [9]:
print('{} contains {} frames: {})'.format(i, len(i.frames), list(i.frames.keys())))
<I"Generic"> contains 2 frames: ['cecelia', 'henrietta'])

By default, all image frames will share the same color mapping. We can let each frame have its own color mapping by setting the illustration’s sharecolorbar= keyword to False.

In [ ]:
i = GenericIllustration(imshows=[normal, subtracted], sharecolorbar=False)
i.plot()
i.animate('_static/a-pair-of-frames-with-different-colorbars.mp4')

If you’re trying to directly compare values across different frames, keep sharecolorbar=True. If you’re looking for qualitative features and want to highlight the details unique to each frame, let each have its own colorbar. In this example, where the data were simulated to represent photon counts, we can see that the noise in the difference images is highest where the flux in the normal image is too.

some example Frame combinations

We can treat frames as somewhat modular containers, and build up illustrations through combinations of them. Here are a couple of examples of making custom illustrations by following the general process of 1. make some sequences 2. put them in frames 3. fill an illustration

simple ZoomFrame (recreating illustratefits)

Let’s start by recreating the zoom feature shown with illustratefits in the quickstart.

In [ ]:
# first create a sequence
big = make_sequence(create_test_stamp(N=10, xsize=80, ysize=100))

# then define some frames
image = imshowFrame(name='big-image', data=big, title='imshow')
zoom = ZoomFrame(name='zoom', source=image, position=(25, 50), size=(20,10), title='zoom')

# then populate an illustration with them
i = GenericIllustration(imshows=[image, zoom])
i.plot()
#i.animate('_static/an-example-of-a-zoom.mp4', dpi=75)
f = i.frames['big-image']

multiple ZoomFrames on multiple images

Now, let’s build a bit on that by including both the original images and some median-subtracted ones. In addition to the extra frames, we’re also including some more complicated options in generating the illustration layout.

In [ ]:
# first create a sequence
big = make_sequence(create_test_stamp(N=10, xsize=80, ysize=100))

# then define some frames
image = imshowFrame(name='big-image', data=big, title='imshow')
zoom = ZoomFrame(name='zoom', source=image, position=(25, 50), size=(20,10), title='zoom')
subtracted = imshowFrame(name='big-subtracted', data=big, title='subtracted', processingsteps=['subtractmedian'])
subtractedzoom = ZoomFrame(name='zoom-subtracted', source=subtracted, position=(25, 50), size=(20,10), title='zoom')

# then populate an illustration with them
i = GenericIllustration(imshows=[image, zoom, subtracted, subtractedzoom], imshowrows=2,
                        figsize=(7,6), hspace=0.2, wspace=0.02,
                        left=0.05, right=0.95, bottom=0.05, top=0.9)
i.plot()
i.animate('_static/an-example-of-a-more-complicated-zoom.mp4', dpi=75)

an EmptyTimeseriesFrame linked to an imshowFrame

It might be nice to see a light curve synced up to the data from which it is derived. The EmptyTimeseriesFrame provides us with an empty frame, into which we can plot some time-series data. Here’s a first step toward that capability; it still needs a bit of work, but hopefully this is enough to get started. The basic idea is to create an available plot location (an Axes object, in matplotlib-speak), where you can fill later with whatever plot elements you like. The below example demonstrates how to populate the time-series frame in two ways, both using its built-in .plot function and simply by adding elements to i.frames['timeseries'].ax.

In [ ]:
# make a dataset
star = make_sequence(create_test_stamp(N=25, xsize=10, ysize=10, single=True))

# create an empty time-series frame into which we will plot a timeseries
lightcurve = EmptyTimeseriesFrame(name='timeseries')
someimage = imshowFrame(name='image', data=star, title='', plotingredients=['image', 'arrows', 'colorbar'])

# create an illustration that positions these frames next to each other
i = SideBySideIllustration(timeseries=[lightcurve], imshows=[someimage])

# plotting the illustration creates the basic structure...
i.plot()

# ...but we can still modify it through the individual frames
f = i.frames['timeseries']

# the time-series f.plot plot will subtract a the illustration's shared time offset
time = star.time.jd
flux = star._gather_3d().sum(-1).sum(-1)
f.plot(time, flux, marker='o', color='black')

# the f.ax refers to the time-series axes, so this is how we set up to plot into it
plt.sca(f.ax)
plt.axhline(np.mean(flux), color='cornflowerblue', zorder=-1)
plt.ylabel('Some Flux')

i.animate('_static/an-example-of-a-timeseries.mp4')

The interface for plotting timeseries is still a little buggy and may change soon. For now, hopefully you find this helpful!

Some more examples are included, in one way or another, in the tests/ directory of the playground repository. Check those out, or please feel free to contribute new examples here! As always, please do not hesitate to contact Zach Berta-Thompson or leave an issue on the gitlab repository.