Images photometry#

After an observation is done, a common need is to reduce and extract fluxes from raw FITS images.

In this tutorial you will learn how to process a complete night of raw data from any telescope by building a data reduction Sequence with prose.

Simualting data#

You can follow this tutorial on your own data or generate a synthetic dataset. As an example, let’s generate a light curve

import numpy as np
import matplotlib.pyplot as plt
from prose.simulations import simulate_observation

time = np.linspace(0, 0.15, 100) + 2450000
target_dflux = 1 + np.sin(time * 100) * 1e-2

plt.plot(time, target_dflux)
_ = plt.ylim(0.98, 1.02)
../_images/71995a1b6d6a140e48499183af8ffb61171fe2da4766045ec11030ed572b3d6f.png

This might be the differential flux of a variable star. Let’s now simulate the fits images associated with the observation of this target:

# so we have the same data
np.random.seed(40)

fits_folder = "./tutorial_dataset"
simulate_observation(time, target_dflux, fits_folder)
100%|██████████| 100/100 [00:03<00:00, 27.58it/s]

here prose simulated comparison stars, their fluxes over time and some systematics noises.

Explore FITS#

The first thing we want to do is to see what is contained within our folder. For that we instantiate a FitsManager object on our folder to describe its content

from prose import FitsManager

fm = FitsManager(fits_folder, depth=2)
fm
date telescope filter type target width height files
id
3 2023-06-15 A bias prose 600 600 1
4 2023-06-15 A dark prose 600 600 1
2 2023-06-15 A a flat prose 600 600 4
1 2023-06-15 A a light prose 600 600 100

We have 100 images of the prose target together with some calibration files.

Extracting photometry#

Using a reference#

In order to perform the photometric extraction of stars fluxes on all images, we will select a reference image from which we will extract:

  • The stars positions, then reused and detected on all images (after being aligned to the reference)

  • The global full-width at half-maximum (fwhm) of the PSF, to scale the apertures used in the aperture photometry block

from prose import FITSImage

ref = FITSImage(fm.all_images[0])

Note

FITSImage is the primary function used to load a FITS image into an Image object. While other loaders can be used, this loader is compatible with most of the images aquired from small to large-size ground-based telescopes.

Let’s now build a Sequence to process the reference image

from prose import Sequence, blocks

calibration = Sequence(
    [
        blocks.Calibration(darks=fm.all_darks, bias=fm.all_bias, flats=fm.all_flats),
        blocks.Trim(),
        blocks.PointSourceDetection(),  # stars detection
        blocks.Cutouts(21),  # making stars cutouts
        blocks.MedianEPSF(),  # building PSF
        blocks.psf.Moffat2D(),  # modeling PSF
    ]
)

calibration.run(ref, show_progress=False)

ref.show()
ref.sources.plot()
../_images/d41fd7bd9b5fd5869bc64a47b8c519c72080a50da41af37995bf6ade7f662ddb.png

Tip

You can use a Gaia query to define which stars you want the photometry from (e.g. if too faint to be detected here)

Aperture photometry#

We can now extract the photometry of these stars

radii = np.linspace(0.2, 4, 30)

photometry = Sequence(
    [
        *calibration,  # calibration
        blocks.ComputeTransformTwirl(ref),  # compute alignment
        blocks.AlignReferenceSources(ref),  # alignment
        blocks.CentroidQuadratic(),  # centroiding
        blocks.AperturePhotometry(radii=radii),  # aperture photometry
        blocks.AnnulusBackground(),  # local background estimate
        blocks.GetFluxes(),
    ]
)

photometry.run(fm.all_images, loader=FITSImage)

Notice how we specified a loader to photometry.run, and instead of passing a list of Image objects, we passed a list of image paths. Although we cannot access each Image and its data individually, this allow to process a large amount of images without having to load them in memory. Instead, eadch image is loaded, processed and deleted in memory on the go.

The Fluxes object#

The GetFluxes block provide a way to retrieve the fluxes computed by the AperturePhotometry minus the background estimated with AnnulusBackground

raw_fluxes = photometry[-1].fluxes

we can then produce the differential photometry with autodiff

# picking target
raw_fluxes.target = 0

# good practice
raw_fluxes = raw_fluxes.sigma_clipping_data(bkg=3)

# differential photometry
diff_fluxes = raw_fluxes.autodiff()
diff_fluxes.plot()
diff_fluxes.bin(0.005, True).errorbar()

plt.ylim(0.98, 1.02)
plt.xlabel("time")
plt.ylabel("diff. flux")
plt.tight_layout()
../_images/44b128d14e72214a7a998f1b39c1839ec6b0d68d402b02468a64c5fad917eb7f.png
Hide code cell content
import shutil

shutil.rmtree(fits_folder)