Generating Cubes and Cutouts from TESS FFIs#

Learning Goals#

By the end of this tutorial, you will:

  • Learn about the two TESS full frame image (FFI) types: SPOC and TICA.

  • Learn the structure of a TICA FFI.

  • Learn how to retrieve and download TESS FFIs using astroquery.mast.Observations.

  • Learn how to generate cubes from TESS FFIs with astrocut’s CubeFactory and TicaCubeFactory.

  • Learn how to generate cutouts from TESS cubes with astrocut’s CutoutFactory.

  • Learn the structure of a TESS target pixel file (TPF), and therefore an astrocut cutouts file, and how to retrieve and plot the data stored within.


In this tutorial, you will learn the basic functionality of astrocut’s CubeFactory, TicaCubeFactory, and CutoutFactory. These 3 tools allow users to generate cubes and cutouts of TESS images. TESS image cubes are stacks of TESS full frame images (TESS FFIs) which are formatted to allow users to make cutouts with CutoutFactory. These cutouts are sub-images that will only contain your region of interest, to make analysis easier.

The process of generating cubes, and making cutouts from them, mimicks the web-based TESSCut service. This tutorial will teach you to make cubes and cutouts that are customized for your specific use-cases, while also giving insight to how TESSCut calls upon astrocut to create cutouts.

As in the TESSCut service, there are two different TESS FFI product types you can work with in this notebook. These two product types are the TESS mission-provided, Science Processing Operations Center (SPOC) FFIs, and the TESS Image CAlibrator (TICA) FFIs. Both are available via astroquery.mast.Observations, and both have different use-cases. The TICA products offer the latest sector observations due to their faster delivery cadence, and can be available up to 3 times sooner than their SPOC counterparts. As such, for those who are working with time-sensitive observations, TICA may be the best option for generating your cutouts. Note that the SPOC and TICA products are processed through different pipelines and thus have different pixel values and WCS solutions. Please refer to the SPOC pipeline paper and the TICA pipeline paper for more information on the differences and similarities between the SPOC and TICA products.

TICA and SPOC ingest

The workflow for this notebook consists of:

  • Retrieving and Downloading the FFIs

    • Retrieving the FFIs

    • Downloading the FFIs

    • Inspecting the FFIs

  • Creating a Cube

  • Creating the Cutouts

  • Exercise: Generate Cutouts for TIC 2527981 Sector 27 SPOC Products

  • Resources


Below is a list of the packages used throughout this notebook, and their use-cases. Please ensure you have the recommended version of these packages installed on the environment this notebook has been launched in. See the requirements.txt file for recommended package versions.

  • astropy.units for unit conversion

  • matplotlib.pyplot for plotting data

  • numpy to handle array functions

  • astrocut CubeFactory for generating cubes out of TESS-SPOC FFIs

  • astrocut TicaCubeFactory for generating cubes out of TICA FFIs

  • astrocut CutoutFactory for generating cutouts out of astrocut cubes

  • astropy.coordinates SkyCoord for creating sky coordinate objects

  • fits for accessing FITS files

  • astroquery.mast Observations for retrieving and downloading the TESS FFIs

  • matplotlib.path Path to generate a drawing path for plotting purposes

  • matplotlib.patches PathPatch to generate a patch that represents the CCD footprint for plotting purposes

%matplotlib inline

import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np

from astrocut import CubeFactory, TicaCubeFactory, CutoutFactory
from astropy.coordinates import SkyCoord
from import fits
from astroquery.mast import Observations
from matplotlib.path import Path
from matplotlib.patches import PathPatch

Below are the helper functions we will call for plotting purposes later in the notebook.

def convert_coords(ra, dec):
    """ Wrap the input coordinates `ra` and `dec` around the upper limit of 180
    degrees and convert to radians so that they may be plotted on the Aitoff canvas.
    ra : str, int, or float
        The Right Ascension your target lands on, in degrees. May be any object type that 
        can be converted to a float, such as a string, or integer.
    dec : str, int, or float
        The Declination your target lands on, in degrees. May be any object type that 
        can be converted to a float, such as a string, or integer.
    ra_rad : float
        The Right Ascension of your target in radians.
    dec_rad : float
        The Declination of your target in radians.

    # Make a SkyCoord object out of the RA, Dec
    c = SkyCoord(ra=float(ra)*, dec=float(dec)*, frame='icrs')

    # The plotting only works when the coordinates are in radians.
    # And because it's an aitoff projection, we can't 
    # go beyond 180 degrees, so let's wrap the RA vals around that upper limit.
    ra_rad = c.ra.wrap_at(180 * u.deg).radian
    dec_rad = c.dec.radian

    return ra_rad, dec_rad

def plot_footprint(coords):
    """ Plots a polygon of 4 vertices onto an aitoff projection on a matplotlib canvas.
    coords : list or array of tuples
        An iterable object (can be a list or array) of 5 tuples, each tuple containing the 
        RA and Dec (float objects) of a vertice of your footprint. There MUST be 5: 1 for each
        vertice, and 1 to return to the starting point, as PathPatch works with a set of drawing 
        instructions, rather than a predetermined shape.
    ax : Matplotlib.pyplot figure object subplot
        The subplot that contains the aitoff projection and footprint drawing.
    assert len(coords) == 5, 'We need 5 sets of coordinates. 1 for each vertice + 1 to return to the starting point.'
    instructions = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY]
    # Create the path patch using the `coords` list of tuples and the
    # instructions from above
    c = np.array(coords)
    path = Path(c, instructions)
    ppatch = PathPatch(path, edgecolor='k', facecolor='salmon')

    # Create a matplotlib canvas
    fig = plt.figure(figsize=(12,12))
    # We will be using the `aitoff` projection for a globe canvas
    ax = plt.subplot(111, projection='aitoff')
    # Need grids!
    # Add the patch to your canvas
    return ax

Retrieving and Downloading the FFIs#

Before we begin generating cubes and cutouts from TESS FFIs, we must first acquire these FFIs. To do this, we will request and download the FFIs locally using astroquery.mast, or more specifically, the astroquery.mast.Observations tool, which will grant us access to the database that houses the TESS products. This is also how the TESSCut API generates its cutouts.

Retrieving the FFIs#

You can download FFIs for any sector, camera, and CCD type. This notebook will generate cutouts for target TIC 2527981 using TICA FFIs in sector 27, but feel free to substitute a favorite target of your own. To find our target, we will filter with the database criteria: coordinates, target_name, dataproduct_type, and sequence_number.

The coordinates criteria will filter for the coordinates of our target, while the target_name will filter for TICA FFIs and exclude all SPOC FFIs. dataproduct_type ensures we get image observations, and the sequence_number will be used to filter for only sector 27 observations. With this information, let’s compile our query…

# We will pass in the coordinates as a Sky Coord object
coordinates = SkyCoord(289.0979, -29.3370, unit="deg")

obs = Observations.query_criteria(coordinates=coordinates,
                                  target_name='TICA FFI',


This is the sector 27 FFI that contains TIC2527981. Note that the s_ra and s_dec listed above are the center of the CCD that contains the target, not the coordinates of the target itself.

To demonstrate this, let’s plot the s_region metadata, the s_ra and s_dec coordinates, and the input coordinates of our target to see where it lands relative to sector 27, camera 1, CCD 4.

# Extract the polygon vertices, then store them in separate lists
polygon = obs['s_region'][0]
split = polygon.split(' ')
# Removing the "POLYGON" string
# Storing the RAs and Decs separately
ras, decs = split[::2], split[1::2]

# Now we convert our RAs/Decs into radians and wrap them around 180 degrees
coords = []
for ra, dec in zip(ras, decs):
    ra_rad, dec_rad = convert_coords(ra, dec)
    coords.append((ra_rad, dec_rad))

# We use Matplotlib's Path and PathPatch to plot the footprint of sector 27, camera 1, CCD 4
ax = plot_footprint(coords)

# Now let's plot our input target's RA, Dec on top of the footprint to see its relative position
target_ra, target_dec =,
target_coords = convert_coords(target_ra, target_dec)
ax.scatter(target_coords[0], target_coords[1], color='k', label='target coords')

# And lastly, let's plot the s_ra and s_dec coordinates to see what they represent
s_coords = convert_coords(obs['s_ra'][0], obs['s_dec'][0])
ax.scatter(s_coords[0], s_coords[1], color='grey', label='s_ra, s_dec')

ax.set_title('Sector 27, Camera 1, CCD 4 Footprint' + '\n' + ' ')

The plot above shows the positioning of TIC 2527981 relative to sector 27, camera 1, CCD 4. You can see that it is more or less on the edge of this CCD, and not the center. Note also that the s_ra and s_dec fall exactly in the center of the CCD.

From this observation we can now retrieve the corresponding TICA products (FFIs). Let’s use the get_product_list method to retrieve these FFIs.

Note: If you do not know which sectors contain your target, you should query for your target without the `sequence_number` filter to see what is available. However, the cutout functionality does not currently support making cross-sector cutouts, so FFI products from different sectors cannot be stacked into the same cube. Keep this in mind when retrieving the FFI products intended for your cube.
products = Observations.get_product_list(obs)

Downloading the FFIs#

Now we are ready to download the products. Use the download_products call to download these locally to your machine.

Note: If you have many files, or a large download size, you should set the `curl_flag` option to `True`. This will generate a bash script that will download the products (via cURL) when executed.

To save space in this example, we will only download the first 5 TICA FFIs.

manifest = Observations.download_products(products[:5], curl_flag=False)

Inspecting the FFIs#

Now that we have the TICA FFIs stored locally, let’s do some quick plotting and inspection. We can use the manifest output from our download call above to get the list of our downloaded FFIs in their respective locations. Let’s open up one of them and do some plotting and header inspection.

ffi = manifest['Local Path'][0]
print(' ')
print('HDU List')

Unlike the SPOC FFIs, the TICA FFIs have the science data stored in the Primary HDU of the FITS file. The BinTable HDU contains the list of all the reference sources used to derive the WCS solutions for the data. More information on this is available in the TICA pipeline paper, but let’s move forward and retrieve the header information from the Primary HDU.

header = fits.getheader(ffi, 0)

The header information above contains important metadata like the CD matrix elements used for the WCS transformations, exposure time of the observation, size of the image, and so on. Now let’s retrieve the science data from this Primary HDU and plot the sector 27 camera 1 CCD 4 field.

# Retrieve the data and some header metadata for labeling
data = fits.getdata(ffi, 0)
cam, ccd = header['CAMNUM'], header['CCDNUM']

# Plotting
plt.imshow(data, vmin=0, vmax=1000000)

# Labeling
plt.title(f'Sector 27, Cam {cam}, CCD {ccd}', fontsize=20)
plt.xlabel('COLUMNS (X)', fontsize=15)
plt.ylabel('ROWS (Y)', fontsize=15)
plt.tick_params(axis='both', which='major', labelsize=15)

Creating a Cube#

Now that we have retrieved, downloaded, and inspected our TICA FFIs, it’s time to make a cube out of them. To do this, we will call on astrocut’s TicaCubeFactory class which contains all the functionality to stack the TICA FFIs into a cube as efficiently as possible and store all the FFIs’ metadata in a binary table.

tica_cube_maker = TicaCubeFactory()

We will utilize the manifest output from our download_products call in the previous section to feed TicaCubeFactory.make_cube the batch of 5 TICA FFIs for the cube-making process. The verbose argument is set to True by default, which will print updates about the call, but you may change verbose to False if you so choose.

cube = tica_cube_maker.make_cube(manifest['Local Path'])

The cube file is stored in your current working directory with the default name img-cube.fits if none is specified. Let’s inspect the contents of the cube file:

The structure for each cube file is the same for both TICA and SPOC. It consists of:

  • The Primary HDU, which contains header information derived from the first FFI in the cube stack, as well as WCS information derived from the FFI in the middle of the cube stack.

  • The Image HDU, which is the cube itself. The dimensions of the cube for this example is (2, 5, 2136, 2078) and can be explained as follows:

    • The 0th axis represents the science and error “layers”, with the science arrays in index=0 and the error arrays in index=1. Because the TICA pipeline does not propagate errors, the error layer is empty for all the FFIs and should be ignored.

    • The 1st axis represents the number of FFIs that went into the cube. For this example it is 5. Indexing this will pull out the 2D array of science data for a given FFI.

    • The 2nd axis represents the number of rows each FFI consists of.

    • The 3rd axis represents the number of columns each FFI consists of.

  • The BinTable HDU, which contains the image header information for every FFI that went into the stack in the form of a table. Each column in the table is an image header keyword (for TICA it’s the primary header keyword, since the TICA image lives in the primary HDU), and each row in the column is the corresponding value for that keyword for an FFI.

For more information on the cube file structure, see the cube file format section of the Astrocut documentation.

Before we move onto generating the cutouts, it’s important to note that CubeFactory is distinct from TicaCubeFactory. Both classes are used to generate cubes, but TicaCubeFactory is specifically designed to generate TICA cubes, and CubeFactory is specifically designed to generate SPOC cubes. Any attempt at generating TICA cubes with CubeFactory or vice versa will be unsuccessful. See below for an example:

# This cell will give an error if you uncomment the lines below!

#spoc_cube_maker = CubeFactory()
#spoc_cube_maker.make_cube(manifest['Local Path'])

Creating the Cutouts#

Now that we have our cube we are able to generate cutouts of our region of interest. Unlike in the cube-making process, we have a single class for generating the cutouts from both SPOC or TICA cubes, CutoutFactory. To make the cutouts, we will call upon CutoutFactory’s cube_cut and feed it the cube we just made, the coordinates for the center of our region of interest, which in this case is our object TIC 2527981, the cutout size for our region (in pixels), and lastly, the product type that was used for making our cube.

Note: The call below may generate a "VerifyWarning: Card is too long, comment will be truncated." warning message, which can safely be ignored.
cutout_maker = CutoutFactory()

cutout_file = cutout_maker.cube_cut(cube, coordinates=coordinates, cutout_size=25, product='TICA')

Our cutout file is saved to our current working directory with the default name structured as follows: img_[RA]_[Dec]_[rows]x[cols]_astrocut.fits

Let’s inspect the HDU list of this file…

The structure for each cutout file is the same for both TICA and SPOC. It consists of:

  • The Primary HDU, which contains header information derived from the first FFI in the cube stack, as well as WCS information derived from the FFI in the middle of the cube stack.

  • The BinTable HDU, which is a FITS table that stores the actual cutouts under the FLUX column, with each row being occupied by a single FFI cutout of the requested size.

  • The Image HDU, which contains the aperture, or field of view, of the cutouts. This image is only useful for the moving targets feature on TESSCut, and should be blank if your target has a set of coordinates assigned to it.

For more information on the cutout file structure, see the target pixel file format section of the Astrocut documentation. For now let’s take a closer look at the Primary HDU header:

cutout_header = fits.getheader(cutout_file, 0)

As mentioned previously, the cutout files will have header information that is inherited from the first FFI and the middle FFI in the cube stack (just like the cube files), so most of these keywords should look familiar. However, there are some new header keywords in the cutout files that are added after processing:


These keywords are intentionally added so that the cutout is formatted like a TESS target pixel file (TPF). Conveniently, this means you can use existing scripts on these cutout files without having to readapt them. Let’s go ahead and plot one of the cutouts in our file:

# Extracting the cutouts science data from the BinTable HDU
cutouts = fits.getdata(cutout_file)['FLUX']

# Plotting an arbitrary cutout from the FLUX column

# Labeling
plt.xlabel('COLUMNS (X)', fontsize=15)
plt.ylabel('ROWS (Y)', fontsize=15)
plt.tick_params(axis='both', which='major', labelsize=15)

Exercise: Generate Cutouts for TIC 2527981 Sector 27 SPOC Products#

Let’s now generate cutouts of our target from the SPOC products of the same observation, and use this as an opportunity to compare the FFI file structure differences between SPOC and TICA.

Perform the same astroquery.mast.Observations query, inspect the FFIs, generate the cube (remember that there are two cube-generating classes, CubeFactory and TicaCubeFactory), and generate the cutouts from the cube, in the cells below.

# Make a query for the correct observation using `astroquery.mast.Observations.query_criteria`

# Retrieve the products corresponding to this observations using `astroquery.mast.Observations.download_products`

# Inspect one of the FFIs with `matplotlib.pyplot`. Compare the HDU List between the SPOC FFI and one of the TICA FFIs
# and note the structural differences.
# Generate a cube with the SPOC FFIs.
# If you've stored the output from the `astroquery.mast.Observations.download_products` call above, 
# it's helpful to use the Local Path column as the CubeFactory input.
# Inspect the cube as you wish. Make a note of the cube size and ensure that the dimensions are as you expect.
# Generate the cutouts with the SPOC cube from above.
# Inspect the cutouts as you wish.


The following is a list of resources that were referenced throughout the tutorial, as well as some additional references that you may find useful:


If you use any of astroquery’s tools or astrocut for published research, please cite the authors. Follow these links for more information about citing astroquery and astrocut:

About this Notebook#

If you have comments or questions on this notebook, please contact us through the Archive Help Desk e-mail at

Author(s): Jenny V. Medina
Keyword(s): Tutorial, TESS, TICA, SPOC, astroquery, astrocut, cutouts, ffi, tpf, tesscut
Last Updated: Mar 2023

Top of Page Space Telescope Logo