6. PSF modeling and subtraction with reference star differential imaging

Author: Valentin Christiaens
Suitable for VIP v1.0.3 onwards
Last update: 2024/06/04

Table of contents

This tutorial shows:

  • how to load ADI-ready datacubes;

  • how to use the stellar PSF subtraction algorithms implemented in VIP to produce final post-processed images, and leverage the RDI strategy;

  • how to use iterative (greedy) algorithms in VIP to correct for geometric biases.

Note:

Most PSF subtraction routines in VIP (and some other functions) have been implemented in VIP to allow for multiprocessing, in order to optimally harness the power of machines equipped with multiple CPUs. Any function where the nproc parameter is available in its call can be run in multi-processing, with the value of nproc setting the requested number of CPUs to use. Instead of an integer, one can set nproc=None to use half of all available CPUs. For optimal results in multiprocessing, set the following environment parameters in your terminal BEFORE launching your Jupyter notebook:

export MKL_NUM_THREADS=1

export NUMEXPR_NUM_THREADS=1

export OMP_NUM_THREADS=1

In the case of PCA, singular value decomposition can also be done on GPU by setting svd_mode to an appropriate value (see Sec. 3.5.1. and docstrings of vip_hci.psfsub.pca for details).

Let’s first import a couple of external packages needed in this tutorial:

%matplotlib inline
from hciplot import plot_frames, plot_cubes  # plotting routines
from matplotlib import pyplot as plt
from multiprocessing import cpu_count
import numpy as np
from packaging import version

In the following box we check that your version of VIP is recent enough.

import vip_hci as vip
vvip = vip.__version__
print("VIP version: ", vvip)
if version.parse(vvip) < version.parse("1.0.3"):
    msg = "Please upgrade your version of VIP"
    msg+= "It should be 1.0.3 or above to run this notebook."
    raise ValueError(msg)
VIP version:  1.6.2

6.1. Loading ADI data

In the ‘dataset’ folder of the VIP_extras repository you can find a toy ADI (Angular Differential Imaging) cube and a NACO point spread function (PSF) to demonstrate the capabilities of VIP. This is an L’-band VLT/NACO dataset of beta Pictoris published in Absil et al. (2013) obtained using the Annular Groove Phase Mask (AGPM) Vortex coronagraph. The sequence has been heavily sub-sampled temporarily to make it smaller. The frames were also cropped to the central 101x101 area. In case you want to plug-in your cube just change the path of the following cells.

More info on this dataset, and on opening and visualizing fits files with VIP in general, is available in Tutorial 1. Quick start.

Let’s load the datacube, associated parallactic angles and non-coronagraphic PSF:

from vip_hci.fits import open_fits
from astropy.utils.data import download_file

url_d = "https://github.com/vortex-exoplanet/VIP_extras/raw/master/datasets"
f1 = download_file("{}/naco_betapic_cube_cen.fits".format(url_d), cache=True)
f2 = download_file("{}/naco_betapic_psf.fits".format(url_d), cache=True)
f3 = download_file("{}/naco_betapic_derot_angles.fits".format(url_d), cache=True)

# alternatively, for local files simply provide their full or relative path. E.g.:
#f1 = '../datasets/naco_betapic_cube_cen.fits'
#f2 = '../datasets/naco_betapic_psf.fits'
#f3 = '../datasets/naco_betapic_derot_angles.fits'

cube = open_fits(f1)
psf = open_fits(f2)
angs = open_fits(f3)
FITS HDU-0 data successfully loaded. Data shape: (61, 101, 101)
FITS HDU-0 data successfully loaded. Data shape: (39, 39)
FITS HDU-0 data successfully loaded. Data shape: (61,)

For the purpose of illustrating RDI capabilities in VIP, let’s just define a fiducial reference cube as the science cube flipped along both the x and y axes. Normally you should load it the same way as your science cube.

cube_ref = np.flip(cube, axis=1)       # just for the example
cube_ref = np.flip(cube_ref, axis=2)

Let’s fit the PSF with a 2D Gaussian to infer the FWHM, the flux in a 1-FWHM size aperture, and get a flux-normalized PSF:

%matplotlib inline
from vip_hci.fm import normalize_psf
psfn, flux, fwhm_naco = normalize_psf(psf, size=19, debug=True, full_output=True)
../_images/4b9e371483a1c16c4cb03351f60f44e8d39846518b27c764cf4a7909b42cc4e3.png
FWHM_y = 4.926059872957138
FWHM_x = 4.67577889500593 

centroid y = 9.010992107833063
centroid x = 9.01917912265807
centroid y subim = 9.010992107833063
centroid x subim = 9.01917912265807 

amplitude = 0.10032285220380605
theta = -38.446187060503924

Mean FWHM: 4.801
Flux in 1xFWHM aperture: 1.307
print(fwhm_naco)
4.800919383981534
Note:

The normalize_psf function performs internally a fine centering of the PSF. The input PSF should nevertheless already be centered within a few pixel accuracy for the fine centering to work.

Let’s visualize the normalized PSF with hciplot.plot_frames. Feel free to adapt the backend argument throughout the notebook: 'matplotlib' (default) allows paper-quality figures with annotations which can be saved (default), while 'bokeh' enables interactive visualization.

plot_frames(psfn, grid=True, size_factor=4)
../_images/a0bfb3c352a4096d6552552d02045f4f5f7016a74fc1ea0480f6f6292fe75f41.png

Let’s finally define the pixel scale for NACO (L’ band), which we get from a dictionary stored in the conf subpackage:

from vip_hci.config import VLT_NACO
pxscale_naco = VLT_NACO['plsc']
print(pxscale_naco, "arcsec/px")
0.02719 arcsec/px

6.2. Classical RDI

6.2.1. median-RDI

The most straightforward way to leverage a stack of reference PSF images is to consider its median image as PSF model, and subtract it to each image of the science cube. This can be done with the median_sub function:

from vip_hci.psfsub import median_sub
med_rdi = median_sub(cube, angs, cube_ref=cube_ref)
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-10-28 16:08:46
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Median psf reference subtracted
Done derotating and combining
Running time:  0:00:04.439478
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

Let’s visualize the results, after masking the central 13 px of the image.

from vip_hci.var import mask_circle
plot_frames(mask_circle(med_rdi, 13), grid=True)
../_images/201a4d3a442b8fd0a0bda3d161b02ef870bad207b9ea961c7b342d1cc65c226d.png

As can be seen above, this approach is sufficient to detect exoplanet beta Pic b towards the bottom right of the image (after masking the central part of the image)! This is not too bad, considering we only considered as fake reference cube the science images flipped horizontally and vertically.

The default behaviour will subtract the median reference image of the reference stack however other options can be used. Although the median is more robust, the mean of the reference stack can be requested instead:

mean_rdi = median_sub(cube, angs, cube_ref=cube_ref, collapse_ref='mean')
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-10-28 16:08:51
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Median psf reference subtracted
Done derotating and combining
Running time:  0:00:04.334821
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

Alternatively, the mean/median reference image can be scaled to match in flux each science image before subtraction - this can be useful if the reference image is known to be brighter or fainter than the science image:

med_rdi_sc = median_sub(cube, angs, cube_ref=cube_ref, collapse_ref='sc_median')
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-10-28 16:08:55
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Median psf reference subtracted
Done derotating and combining
Running time:  0:00:04.381679
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

By default when scaling is requested, the scaling factor is calculating from integrating the flux on the whole science and reference images. But an inner and outer radius can be provided, which can be relevant if a coronagraph is used or contamination is present at large radius, respectively. This is done by adding 2 numbers directly at the end of the collapse_ref string, separated by ‘-’. For example, if we want to consider an annulus going from 1 to 3 FWHM for this coronagraphic NaCo dataset (FWHM ~ 5pixels), we can set:

med_rdi_sc515 = median_sub(cube, angs, cube_ref=cube_ref, collapse_ref='sc_median5-15')
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-10-28 16:09:00
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Median psf reference subtracted
Done derotating and combining
Running time:  0:00:04.414767
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

Alternatively, median-RDI can be applied in concentric annuli with a scaling factor calculated per annulus. This is controlled with the mode argument (set to ‘fullfr’ by default). The width of the concentric annuli is set in pixels using argument asize, and the radius of the first annuli is set with radius_int. Let’s set both to the FWHM.

med_rdi_ann = median_sub(cube, angs, mode='annular', asize=fwhm_naco, radius_int=fwhm_naco,
                         cube_ref=cube_ref, collapse_ref='sc_median')
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-10-28 16:09:04
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
N annuli = 9, FWHM = 4
Warning: No valid output stream.
Optimized median psf reference subtracted
Done derotating and combining
Running time:  0:00:04.485246
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

Let’s now visualize all the results obtained above, without any numerical mask:

plot_frames((mean_rdi, med_rdi_sc, med_rdi_sc515, med_rdi_ann), vmin=-200, vmax=200, grid=True)
../_images/0e6539b2f52ed3e6885db0d0320640efdeef834712d87e18dcc12217c7e3742f.png

As expected the results obtained with the different collapse_ref options are very similar – since here we used as fake reference cube the flipped science cube. We also notice strong residuals near the center of the image which shows the limitation of assuming a perfect centro-symmetry stellar halo. Nonetheless the signature of beta Pic b can be seen towards the bottom right of the image.

6.2.2. PCA-RDI

Using PCA leveraging the RDI strategy is as simple as calling the pca function in VIP with the cube_ref argument set to your cube of reference images. For example, to run PCA using 42 principal components:

from vip_hci.psfsub import pca
pca_rdi_fr = pca(cube, angs, ncomp=42, cube_ref=cube_ref)
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-10-28 16:09:10
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
System total memory = 8.101 GB
System available memory = 6.418 GB
Done vectorizing the frames. Matrix shape: (61, 10201)
Done vectorizing the frames. Matrix shape: (61, 10201)
Done SVD/PCA with numpy SVD (LAPACK)
Running time:  0:00:00.059065
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Done de-rotating and combining
Running time:  0:00:04.512246
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

Let’s visualize the results, after masking the central 13 px of the image.

from vip_hci.var import mask_circle
plot_frames(mask_circle(pca_rdi_fr, 13), grid=True)
../_images/01df69a2cf671eab534492a3cb071faf84742b14e536642c4734fb497d92b679.png

We now see Beta Pic b a bit clearer than with the median-subtraction approach.

A lot of options are available with the PCA function, and are also relevant with a reference cube. We refer to Tutorial 3 (section 3.5), for all available options. Below we just illustrate the use of a convenient function providing the SNR of a candidate as a function of number of principal components.

Let’s first define the approximate coordinates of the companion based on the image above (remember to subtract 1 to the coordinates read in the image since Python uses a zero-based index system):

xy_b = (58, 35)
from vip_hci.psfsub import pca_grid
pca_rdi_opt = pca_grid(cube, angs, cube_ref=cube_ref, fwhm=fwhm_naco, range_pcs=(1,51,1),
                       source_xy=xy_b, mode='fullfr', full_output=True, plot=True)
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-10-28 16:09:15
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Done SVD/PCA with numpy SVD (LAPACK)
Running time:  0:00:00.055566
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[19], line 2
      1 from vip_hci.psfsub import pca_grid
----> 2 pca_rdi_opt = pca_grid(cube, angs, cube_ref=cube_ref, fwhm=fwhm_naco, range_pcs=(1,51,1),
      3                        source_xy=xy_b, mode='fullfr', full_output=True, plot=True)

File ~/checkouts/readthedocs.org/user_builds/vip/envs/latest/lib/python3.10/site-packages/vip_hci/psfsub/utils_pca.py:349, in pca_grid(cube, angle_list, fwhm, range_pcs, source_xy, cube_ref, mode, annulus_width, svd_mode, scaling, mask_center_px, fmerit, collapse, ifs_collapse_range, verbose, full_output, debug, plot, save_plot, start_time, scale_list, initial_4dshape, weights, exclude_negative_lobes, **rot_options)
    347 for pc in pclist:
    348     if mode == 'fullfr':
--> 349         frame = truncate_svd_get_finframe(matrix, angle_list, pc, V)
    350     elif mode == 'annular':
    351         frame = truncate_svd_get_finframe_ann(matrix, annind,
    352                                               angle_list, pc, V)

File ~/checkouts/readthedocs.org/user_builds/vip/envs/latest/lib/python3.10/site-packages/vip_hci/psfsub/utils_pca.py:224, in pca_grid.<locals>.truncate_svd_get_finframe(matrix, angle_list, ncomp, V)
    221 else:
    222     residuals_reshaped = residuals_res
--> 224 residuals_res_der = cube_derotate(residuals_reshaped, angle_list,
    225                                   **rot_options)
    226 res_frame = cube_collapse(residuals_res_der, mode=collapse, w=weights)
    227 return res_frame

File ~/checkouts/readthedocs.org/user_builds/vip/envs/latest/lib/python3.10/site-packages/vip_hci/preproc/derotation.py:369, in cube_derotate(array, angle_list, imlib, interpolation, cxy, nproc, border_mode, mask_val, edge_blend, interp_zeros, ker)
    367     array_der = np.zeros_like(array)
    368     for i in range(n_frames):
--> 369         array_der[i] = frame_rotate(array[i], -angle_list[i], imlib=imlib,
    370                                     interpolation=interpolation, cxy=cxy,
    371                                     border_mode=border_mode,
    372                                     mask_val=mask_val,
    373                                     edge_blend=edge_blend,
    374                                     interp_zeros=interp_zeros, ker=ker)
    375 elif nproc > 1:
    377     res = pool_map(nproc, _frame_rotate_mp, iterable(array),
    378                    iterable(-angle_list), imlib, interpolation, cxy,
    379                    border_mode, mask_val, edge_blend, interp_zeros, ker)

File ~/checkouts/readthedocs.org/user_builds/vip/envs/latest/lib/python3.10/site-packages/vip_hci/preproc/derotation.py:228, in frame_rotate(array, angle, imlib, interpolation, cxy, border_mode, mask_val, edge_blend, interp_zeros, ker)
    225         raise ValueError(msg)
    227 if imlib == 'vip-fft':
--> 228     array_out = rotate_fft(array_prep, angle)
    230 elif imlib == 'skimage':
    231     if interpolation == 'nearneig':

File ~/checkouts/readthedocs.org/user_builds/vip/envs/latest/lib/python3.10/site-packages/vip_hci/preproc/derotation.py:596, in rotate_fft(array, angle)
    594 # TODO: make FFT padding work for other option than '0'.
    595 s_x = _fft_shear(array_in, arr_x, a, ax=1, pad=0)
--> 596 s_xy = _fft_shear(s_x, arr_y, b, ax=0, pad=0)
    597 s_xyx = _fft_shear(s_xy, arr_x, a, ax=1, pad=0)
    599 if y_ori % 2 or x_ori % 2:
    600     # set it back to original dimensions

File ~/checkouts/readthedocs.org/user_builds/vip/envs/latest/lib/python3.10/site-packages/vip_hci/preproc/derotation.py:621, in _fft_shear(arr, arr_ori, c, ax, pad, shift_ini)
    619 s_x = np.exp(-2j*np.pi*c*arr_u*arr_ori)*s_x
    620 s_x = fftshift(s_x)
--> 621 s_x = ifft(s_x, axis=ax)
    622 s_x = fftshift(s_x)
    624 return s_x

File ~/checkouts/readthedocs.org/user_builds/vip/envs/latest/lib/python3.10/site-packages/numpy/fft/_pocketfft.py:318, in ifft(a, n, axis, norm, out)
    316 if n is None:
    317     n = a.shape[axis]
--> 318 output = _raw_fft(a, n, axis, False, False, norm, out=out)
    319 return output

File ~/checkouts/readthedocs.org/user_builds/vip/envs/latest/lib/python3.10/site-packages/numpy/fft/_pocketfft.py:94, in _raw_fft(a, n, axis, is_real, is_forward, norm, out)
     90 elif ((shape := getattr(out, "shape", None)) is not None
     91       and (len(shape) != a.ndim or shape[axis] != n_out)):
     92     raise ValueError("output array has wrong shape.")
---> 94 return ufunc(a, fct, axes=[(axis,), (), (axis,)], out=out)

KeyboardInterrupt: 
pca?
Signature: pca(*all_args: List, **all_kwargs: dict)
Docstring:
Full-frame PCA algorithm applied to PSF substraction.

The reference PSF and the quasi-static speckle pattern are modeled using
Principal Component Analysis. Depending on the input parameters this PCA
function can work in ADI, RDI or mSDI (IFS data) mode.

ADI: the target ``cube`` itself is used to learn the PCs and to obtain a
low-rank approximation model PSF (star + speckles). Both `cube_ref`` and
``scale_list`` must be None. The full-frame ADI-PCA implementation is based
on [AMA12]_ and [SOU12]_. If ``batch`` is provided then the cube is
processed with incremental PCA as described in [GOM17]_.

(ADI+)RDI: if a reference cube is provided (``cube_ref``), its PCs are used
to reconstruct the target frames to obtain the model PSF (star + speckles).

(ADI+)mSDI (IFS data): if a scaling vector is provided (``scale_list``) and
the cube is a 4d array [# channels, # adi-frames, Y, X], it's assumed it
contains several multi-spectral frames acquired in pupil-stabilized mode.
A single or two stages PCA can be performed, depending on ``adimsdi``, as
explained in [CHR19]_.

Parameters
----------
all_args: list, optional
    Positionnal arguments for the PCA algorithm. Full list of parameters
    below.
all_kwargs: dictionary, optional
    Mix of keyword arguments that can initialize a PCA_Params and the
    optional 'rot_options' dictionnary (with keyword values ``border_mode``,
    ``mask_val``, ``edge_blend``, ``interp_zeros``, ``ker``; see docstring
    of ``vip_hci.preproc.frame_rotate``). Can also contain a PCA_Params
    dictionary named `algo_params`.

PCA parameters
--------------
cube : str or numpy ndarray, 3d or 4d
    Input cube (ADI or ADI+mSDI). If 4D, the first dimension should be
    spectral. If a string is given, it must correspond to the path to the
    fits file to be opened in memmap mode (incremental PCA-ADI of 3D cubes
    only).
angle_list : numpy ndarray, 1d
    Vector of derotation angles to align North up in your images.
cube_ref : 3d or 4d numpy ndarray, or list of 3D numpy ndarray, optional
    Reference library cube for Reference Star Differential Imaging. Should
    be 3D, except if input cube is 4D and no scale_list is provided,
    reference cube can then either be 4D or a list of 3D cubes (i.e.
    providing the reference cube for each individual spectral cube).
scale_list : numpy ndarray, 1d, optional
    If provided, triggers mSDI reduction. These should be the scaling
    factors used to re-scale the spectral channels and align the speckles
    in case of IFS data (ADI+mSDI cube). Usually, the
    scaling factors are the last channel wavelength divided by the
    other wavelengths in the cube (more thorough approaches can be used
    to get the scaling factors, e.g. with
    ``vip_hci.preproc.find_scal_vector``).
ncomp : int, float, tuple of int/None, or list, optional
    How many PCs are used as a lower-dimensional subspace to project the
    target frames.

    * ADI (``cube`` is a 3d array): if an int is provided, ``ncomp`` is the
    number of PCs extracted from ``cube`` itself. If ``ncomp`` is a float
    in the interval [0, 1] then it corresponds to the desired cumulative
    explained variance ratio (the corresponding number of components is
    estimated). If ``ncomp`` is a tuple of two integers, then it
    corresponds to an interval of PCs in which final residual frames are
    computed (optionally, if a tuple of 3 integers is passed, the third
    value is the step). If ``ncomp`` is a list of int, these will be used to
    calculate residual frames. When ``ncomp`` is a tuple or list, and
    ``source_xy`` is not None, then the S/Ns (mean value in a 1xFWHM
    circular aperture) of the given (X,Y) coordinates are computed.

    * ADI+RDI (``cube`` and ``cube_ref`` are 3d arrays): ``ncomp`` is the
    number of PCs obtained from ``cube_ref``. If ``ncomp`` is a tuple,
    then it corresponds to an interval of PCs (obtained from ``cube_ref``)
    in which final residual frames are computed. If ``ncomp`` is a list of
    int, these will be used to calculate residual frames. When ``ncomp`` is
    a tuple or list, and ``source_xy`` is not None, then the S/Ns (mean
    value in a 1xFWHM circular aperture) of the given (X,Y) coordinates are
    computed.

    * ADI or ADI+RDI (``cube`` is a 4d array): same input format allowed as
    above. If ``ncomp`` is a list with the same length as the number of
    channels, each element of the list will be used as ``ncomp`` value
    (be it int, float or tuple) for each spectral channel. If not a
    list or a list with a different length as the number of spectral
    channels, these will be tested for all spectral channels respectively.

    * ADI+mSDI (``cube`` is a 4d array and ``adimsdi="single"``): ``ncomp``
    is the number of PCs obtained from the whole set of frames
    (n_channels * n_adiframes). If ``ncomp`` is a float in the interval
    (0, 1] then it corresponds to the desired CEVR, and the corresponding
    number of components will be estimated. If ``ncomp`` is a tuple, then
    it corresponds to an interval of PCs in which final residual frames
    are computed. If ``ncomp`` is a list of int, these will be used to
    calculate residual frames. When ``ncomp`` is a tuple or list, and
    ``source_xy`` is not None, then the S/Ns (mean value in a 1xFWHM
    circular aperture) of the given (X,Y) coordinates are computed.

    * ADI+mSDI  (``cube`` is a 4d array and ``adimsdi="double"``): ``ncomp``
    must be a tuple, where the first value is the number of PCs obtained
    from each multi-spectral frame (if None then this stage will be
    skipped and the spectral channels will be combined without
    subtraction); the second value sets the number of PCs used in the
    second PCA stage, ADI-like using the residuals of the first stage (if
    None then the second PCA stage is skipped and the residuals are
    de-rotated and combined).

svd_mode : Enum, see `vip_hci.config.paramenum.SvdMode`
    Switch for the SVD method/library to be used.
scaling : Enum, see `vip_hci.config.paramenum.Scaling`
    Pixel-wise scaling mode using ``sklearn.preprocessing.scale``
    function. If set to None, the input matrix is left untouched.
mask_center_px : None or int
    If None, no masking is done. If an integer > 1 then this value is the
    radius of the circular mask.
source_xy : tuple of int, optional
    For ADI-PCA, this triggers a frame rejection in the PCA library, with
    ``source_xy`` as the coordinates X,Y of the center of the annulus where
    the PA criterion is estimated. When ``ncomp`` is a tuple, a PCA grid is
    computed and the S/Ns (mean value in a 1xFWHM circular aperture) of the
    given (X,Y) coordinates are computed.
delta_rot : int, optional
    Factor for tuning the parallactic angle threshold, expressed in FWHM.
    Default is 1 (excludes 1xFWHM on each side of the considered frame).
fwhm : float, list or 1d numpy array, optional
    Known size of the FWHM in pixels to be used. Default value is 4.
    Can be a list or 1d numpy array for a 4d input cube with no scale_list.
adimsdi : Enum, see `vip_hci.config.paramenum.Adimsdi`
    Changes the way the 4d cubes (ADI+mSDI) are processed. Basically it
    determines whether a single or double pass PCA is going to be computed.
crop_ifs: bool, optional
    [adimsdi='single'] If True cube is cropped at the moment of frame
    rescaling in wavelength. This is recommended for large FOVs such as the
    one of SPHERE, but can remove significant amount of information close to
    the edge of small FOVs (e.g. SINFONI).
imlib : Enum, see `vip_hci.config.paramenum.Imlib`
    See the documentation of ``vip_hci.preproc.frame_rotate``.
imlib2 : Enum, see `vip_hci.config.paramenum.Imlib`
    See the documentation of ``vip_hci.preproc.cube_rescaling_wavelengths``.
interpolation : Enum, see `vip_hci.config.paramenum.Interpolation`
    See the documentation of the ``vip_hci.preproc.frame_rotate`` function.
collapse : Enum, see `vip_hci.config.paramenum.Collapse`
    Sets how temporal residual frames should be combined to produce an
    ADI image.
collapse_ifs : Enum, see `vip_hci.config.paramenum.Collapse`
    Sets how spectral residual frames should be combined to produce an
    mSDI image.
ifs_collapse_range: str 'all' or tuple of 2 int
    If a tuple, it should contain the first and last channels where the mSDI
    residual channels will be collapsed (by default collapses all channels).
mask_rdi: tuple of two numpy array or one signle 2d numpy array, opt
    If provided, binary mask(s) will be used either in RDI mode or in
    ADI+mSDI (2 steps) mode. If two masks are provided, they will the anchor
    and boat regions, respectively, following the denominations in [REN23]_.
    If only one mask is provided, it will be used as the anchor, and the
    boat images will not be masked (i.e., full frames used).
check_memory : bool, optional
    If True, it checks that the input cube is smaller than the available
    system memory.
batch : None, int or float, optional
    When it is not None, it triggers the incremental PCA (for ADI and
    ADI+mSDI cubes). If an int is given, it corresponds to the number of
    frames in each sequential mini-batch. If a float (0, 1] is given, it
    corresponds to the size of the batch is computed wrt the available
    memory in the system.
nproc : None or int, optional
    Number of processes for parallel computing. If None the number of
    processes will be set to (cpu_count()/2). Defaults to ``nproc=1``.
full_output: bool, optional
    Whether to return the final median combined image only or with other
    intermediate arrays.
verbose : bool, optional
    If True prints intermediate info and timing.
weights: 1d numpy array or list, optional
    Weights to be applied for a weighted mean. Need to be provided if
    collapse mode is 'wmean'.
left_eigv : bool, optional
    Whether to use rather left or right singularvectors
    This mode is not compatible with 'mask_rdi' and 'batch'
min_frames_pca : int, optional
    Minimum number of frames required in the PCA library. An error is raised
    if less than such number of frames can be found.
cube_sig: numpy ndarray, opt
    Cube with estimate of significant authentic signals. If provided, this
    will be subtracted before projecting considering the science cube as
    reference cube.

Return
-------
final_residuals_cube : List of numpy ndarray
    [(ncomp is tuple or list) & (source_xy=None or full_output=True)] List
    of residual final PCA frames obtained for a grid of PC values.
frame : numpy ndarray
    [ncomp is scalar or source_xy!=None] 2D array, median combination of the
    de-rotated/re-scaled residuals cube.
pcs : numpy ndarray
    [full_output=True, source_xy=None] Principal components. Valid for
    ADI cubes 3D or 4D (i.e. ``scale_list=None``). This is also returned
    when ``batch`` is not None (incremental PCA).
recon_cube, recon : numpy ndarray
    [full_output=True] Reconstructed cube. Valid for ADI cubes 3D or 4D
    (i.e. ``scale_list=None``)
residuals_cube : numpy ndarray
    [full_output=True] Residuals cube. Valid for ADI cubes 3D or 4D
    (i.e. ``scale_list=None``)
residuals_cube_ : numpy ndarray
    [full_output=True] Derotated residuals cube. Valid for ADI cubes 3D or
    4D (i.e. ``scale_list=None``)
residuals_cube_channels : numpy ndarray
    [full_output=True, adimsdi='double'] Residuals for each multi-spectral
    cube. Valid for ADI+mSDI (4D) cubes (when ``scale_list`` is provided)
residuals_cube_channels_ : numpy ndarray
    [full_output=True, adimsdi='double'] Derotated final residuals. Valid
    for ADI+mSDI (4D) cubes (when ``scale_list`` is provided)
cube_allfr_residuals : numpy ndarray
    [full_output=True, adimsdi='single']  Residuals cube (of the big cube
    with channels and time processed together). Valid for ADI+mSDI (4D)
    cubes (when ``scale_list`` is provided)
cube_adi_residuals : numpy ndarray
    [full_output=True, adimsdi='single'] Residuals cube (of the big cube
    with channels and time processed together) after de-scaling the wls.
    Valid for ADI+mSDI (4D) (when ``scale_list`` is provided).
ifs_adi_frames : numpy ndarray
    [full_output=True, 4D input cube, ``scale_list=None``] This is the cube
    of individual ADI reductions for each channel of the IFS cube.
medians : numpy ndarray
    [full_output=True, source_xy=None, batch!=None] Median images of each
    batch, in incremental PCA, for 3D input cubes only.
File:      ~/GitHub/VIP/vip_hci/psfsub/pca_fullfr.py
Type:      function
from vip_hci.config.paramenum import SvdMode
SvdMode?
Init signature:
SvdMode(
    value,
    names=None,
    *,
    module=None,
    qualname=None,
    type=None,
    start=1,
)
Docstring:     
Define the various modes to use with SVD in PCA as constant strings.

Modes
-----
* ``LAPACK``: uses the LAPACK linear algebra library through Numpy
and it is the most conventional way of computing the SVD
(deterministic result computed on CPU).

* ``ARPACK``: uses the ARPACK Fortran libraries accessible through
Scipy (computation on CPU).

* ``EIGEN``: computes the singular vectors through the
eigendecomposition of the covariance M.M' (computation on CPU).

* ``RANDSVD``: uses the randomized_svd algorithm implemented in
Sklearn (computation on CPU), proposed in [HAL09]_.

* ``CUPY``: uses the Cupy library for GPU computation of the SVD as in
the LAPACK version. `

* ``EIGENCUPY``: offers the same method as with the ``eigen`` option
but on GPU (through Cupy).

* ``RANDCUPY``: is an adaptation of the randomized_svd algorithm,
where all the computations are done on a GPU (through Cupy). `

* ``PYTORCH``: uses the Pytorch library for GPU computation of the SVD.

* ``EIGENPYTORCH``: offers the same method as with the ``eigen``
option but on GPU (through Pytorch).

* ``RANDPYTORCH``: is an adaptation of the randomized_svd algorithm,
where all the linear algebra computations are done on a GPU
(through Pytorch).
File:           ~/GitHub/VIP/vip_hci/config/paramenum.py
Type:           EnumMeta
Subclasses:     

6.2.3. PCA-ARDI

One can also leverage both ADI and RDI in cases where there is enough field rotation in your science cube and an available cube of reference images - this is referred to as ARDI. PCA-ARDI is best done by calling the pca_annular function in VIP, with the cube_ref argument set to your cube of reference images – although a trick can be done for a faster usage in full-frame (see below).

Note:

Mind the different default behaviours of the pca and pca_annular functions when provided a cube_ref reference cube. pca will adopt by default a RDI-only strategy (i.e., not use the science images to calculate the principal components), while pca_annular will by default follow an ARDI stratey (i.e., use both the reference images and the science images that meet a certain rotation threshold to calculate the principal components associated to each image of the cube). The latter approach is thus slower.

First let’s consider two concentric annuli with respective width set to one quarter of the image size, using 13 principal components.

from vip_hci.psfsub import pca_annular
asize = cube.shape[-1]//4 - 1

pca_ardi_fr = pca_annular(cube, angs, ncomp=13, asize=asize, fwhm=fwhm_naco, cube_ref=cube_ref)
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-09-11 12:45:20
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
N annuli = 2, FWHM = 4.801
PCA per annulus (or annular sectors):
Ann 1    PA thresh:  2.29    Ann center:  12    N segments: 1 
Done PCA with lapack for current annulus
Running time:  0:07:52.442157
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Ann 2    PA thresh:  7.85    Ann center:  35    N segments: 1 
Done PCA with lapack for current annulus
Running time:  0:15:56.485911
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Done derotating and combining.
Running time:  0:15:59.441470
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

Let’s visualize the results, after masking the central 1 FWHM in the image.

plot_frames(mask_circle(pca_ardi_fr, fwhm_naco), grid=True)
../_images/f926fbacfb2e9fef0c5dfefbbc9d91cf8612830fa84a8bcf9ffaffbb268c6971.png

We now see Beta Pic b even clearer - with a result more similar to PCA-ADI (tutorial 3 (section 3.5)).

By default, pca_annular uses both science and reference images for the PCA library, but the PCA library is recalculated for each image of the cube and for each concentric annulus (which is why it is much slower than full-frame pca). Only science images meeting a certain rotation threshold with respect to the considered image are considered for the associated PCA library. By default, the threshold is a minimum rotation of 0.1xFWHM for the innermost annulus, linearly increasing up to 1xFWHM for the outermost annulus. This behaviour is controlled with the delta_rot argument (set by default to (0.1, 1)).

Note:

The delta_rot parameter is expressed in FWHM units, and corresponds to the amount of required linear motion (in pixels) at the middle of each concentric annulus. A single value, a tuple of 2 elements, or a list of n_annuli values, can be provided for that argument corresponding to: a single threshold in rotation to be used for all annuli, the lower and upper bounds of a linearly increasing rotation threshold, and the exact rotation threshold to be used for each of all concentric annuli, respectively.

Let’s consider a few example cases - the most relevant choice for you will depend on your data:

  • ARDI using all science and reference images in the PCA library, in 2 concentric annuli:

pca_ardi_all_fr = pca_annular(cube, angs, ncomp=13, asize=asize, cube_ref=cube_ref, delta_rot=0)
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-09-11 14:07:13
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
N annuli = 2, FWHM = 4.000
PCA per annulus (or annular sectors):
Ann 1    Ann center:  12    N segments: 1 
Done PCA with lapack for current annulus
Running time:  0:08:07.345447
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Ann 2    Ann center:  35    N segments: 1 
Done PCA with lapack for current annulus
Running time:  0:16:46.945749
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Done derotating and combining.
Running time:  0:16:50.021504
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
  • Pure RDI in 2 concentric annuli. This is done by setting delta_rot to an arbitrarily high value for no science image to meet the criterion for inclusion in the PCA library.

pca_rdi_ann_fr = pca_annular(cube, angs, ncomp=13, asize=asize, cube_ref=cube_ref, delta_rot=100)
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-09-11 13:26:21
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
N annuli = 2, FWHM = 4.000
PCA per annulus (or annular sectors):
Ann 1    PA thresh: 173.13    Ann center:  12    N segments: 1 
Done PCA with lapack for current annulus
Running time:  0:02:34.106366
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Ann 2    PA thresh: 160.15    Ann center:  35    N segments: 1 
Done PCA with lapack for current annulus
Running time:  0:05:16.320061
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Done derotating and combining.
Running time:  0:05:19.488118
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
  • ARDI in a single annulus, with a rotation threshold of 1 FWHM for inclusion of the science images to the PCA library:

asize1 = cube.shape[-1]//2 - 1
pca_ardi_thr1 = pca_annular(cube, angs, ncomp=13, asize=asize1, cube_ref=cube_ref, delta_rot=1)
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-09-11 13:31:40
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
N annuli = 1, FWHM = 4.000
PCA per annulus (or annular sectors):
Ann 1    PA thresh:  9.73    Ann center:  24    N segments: 1 
Done PCA with lapack for current annulus
Running time:  0:06:54.914208
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Done derotating and combining.
Running time:  0:06:57.950685
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
  • ARDI in 4 concentric annuli, with a rotation threshold linearly increasing from 0.2 FWHM for the innermost annulus up to 1 FWHM for the outermost annulus:

asize4 = cube.shape[-1]//8 - 1
pca_ardi_ann4 = pca_annular(cube, angs, ncomp=13, asize=asize4, cube_ref=cube_ref, delta_rot=[0.1, 0.25, 0.5, 1])
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-09-11 13:38:38
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
N annuli = 4, FWHM = 4.000
PCA per annulus (or annular sectors):
Ann 1    PA thresh:  4.17    Ann center:   6    N segments: 1 
Done PCA with lapack for current annulus
Running time:  0:06:43.615960
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Ann 2    PA thresh:  3.47    Ann center:  16    N segments: 1 
Done PCA with lapack for current annulus
Running time:  0:13:38.191523
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Ann 3    PA thresh:  4.17    Ann center:  28    N segments: 1 
Done PCA with lapack for current annulus
Running time:  0:21:10.653297
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Ann 4    PA thresh:  6.11    Ann center:  38    N segments: 1 
Done PCA with lapack for current annulus
Running time:  0:28:18.134590
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Done derotating and combining.
Running time:  0:28:21.212663
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
  • ARDI in full-frame considering all science and reference images (the fastest of all available options for ARDI). Here the trick is to redefine the reference cube to be a concatenation of the original science and reference cubes before passing it to the pca function:

cube_ref_tmp = np.concatenate((cube, cube_ref))
pca_ardi_full = pca(cube, angs, ncomp=13, cube_ref=cube_ref_tmp)
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-09-11 14:06:59
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
System total memory = 17.180 GB
System available memory = 3.515 GB
Done vectorizing the frames. Matrix shape: (61, 10201)
Done vectorizing the frames. Matrix shape: (122, 10201)
Done SVD/PCA with numpy SVD (LAPACK)
Running time:  0:00:09.125319
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Done de-rotating and combining
Running time:  0:00:12.217991
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Note:

If you run ARDI without any threshold in rotation on the science images, you may get very similar results to either ADI or RDI depending on the relative number of science and reference images in each cube, as the larger the stack the more it will dominate the principal components.

Let’s visualize the results. For this, let’s first place all the processed images in a numpy array, and mask the inner 10px of each image:

pca_ardi_res = np.array((pca_ardi_fr, pca_ardi_all_fr, pca_rdi_ann_fr,
                         pca_ardi_thr1, pca_ardi_ann4, pca_ardi_full))
pca_ardi_res_mask = mask_circle(pca_ardi_res, 12)
plot_frames(pca_ardi_res_mask, rows=2, grid=True)
../_images/ea0c98f627b3eb63dbeeb2acee92a1ee018c05d14c56ad1a7c35eebe1dc5a2dc.png

Remember that the optimal strategy and relative weighting of science vs reference images will depend on the data at hand… Is a lot of field rotation available? Are the reference images well correlated to the science images? Do the radial profile of the reference and science PSF match?

6.2.4. Contrast curves

Let’s now calculate and compare contrast curves obtained with PCA-ADI, PCA-RDI and PCA-ARDI for this dataset. For simplicity, we’ll just consider a single set-up for each case in terms of number of principal components and rotation threshold.

Note that the correct way to do this is to first estimate the parameters of any companion (see Tutorial 5A) and remove the companion from the data. We quickly do this with the cube_planet_free function in the next cell, considering the parameters of beta Pic b reported in Absil et al. (2013).

from vip_hci.fm import cube_planet_free

r_b =  0.452/pxscale_naco # Absil et al. (2013)
theta_b = 211.2+90 # Absil et al. (2013)
f_b = 648.2

cube_emp = cube_planet_free([(r_b, theta_b, f_b)], cube, angs, psfn=psfn)

Among the parameters of the contrast_curve function, one needs to provide starphot which corresponds to the (expected) flux of the star in the coronagraphic images. The latter can be obtained from the non-coronagraphic PSF (e.g. through the normalize_psf function), and after rescaling this flux to the integration time ratio used for the coronagraphic vs non-coronagraphic observations. Let’s assume here that this task was done separately:

starphot = flux # 764939.6

Another relevant parameter of the contrast curve function is algo, which takes any function in VIP for model PSF subtraction, or a user-defined imported function. Optional parameters of the algo can also be passed as regular parameters of contrast_curve.

Let’s now calculate the contrast curves:

from vip_hci.metrics import contrast_curve
cc_adi = contrast_curve(cube_emp, angs, psfn, fwhm=fwhm_naco, pxscale=pxscale_naco, starphot=starphot, 
                        sigma=5, nbranch=3, algo=pca, ncomp=25)
cc_rdi = contrast_curve(cube_emp, angs, psfn, fwhm=fwhm_naco, pxscale=pxscale_naco, starphot=starphot, 
                        sigma=5, nbranch=3, algo=pca, ncomp=10, cube_ref=cube_ref)
cc_ardi = contrast_curve(cube_emp, angs, psfn, fwhm=fwhm_naco, pxscale=pxscale_naco, starphot=starphot, 
                         sigma=5, nbranch=3, algo=pca_annular, ncomp=20, cube_ref=cube_ref, asize=asize, delta_rot=0,
                         verbose=False)
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-07-05 18:23:35
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
ALGO : pca, FWHM = 4.800919383981533, # BRANCHES = 3, SIGMA = 5, STARPHOT = 764939.6
Finished the throughput calculation
Running time:  0:00:19.781555
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-07-05 18:23:55
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
ALGO : pca, FWHM = 4.800919383981533, # BRANCHES = 3, SIGMA = 5, STARPHOT = 764939.6
Finished the throughput calculation
Running time:  0:00:20.264567
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
../_images/8fe12685d33ed9985a482475c8d0abc1efb6decaf1eeff564477069b8ac84377.png ../_images/c4167178a779a1db04473be907b70515d153f8c06921da9cbb74d087b7a79384.png ../_images/666fe58ce98784be1850c6d7d94d05180a1bfa362a07db11aa5b3f824f1b2fcd.png

Let’s now compare the 3 contrast curves in a single plot, and also place Beta Pic b in the plot:

plt.figure(figsize=(8,5))
plt.plot(cc_adi['distance']*pxscale_naco, 
         -2.5*np.log10(cc_adi['sensitivity_student']), 
         'b-', label='5-sigma contrast (PCA-ADI)', alpha=0.5)
plt.plot(cc_rdi['distance']*pxscale_naco, 
         -2.5*np.log10(cc_rdi['sensitivity_student']), 
         'r-', label='5-sigma contrast (PCA-RDI)', alpha=0.5)
plt.plot(cc_ardi['distance']*pxscale_naco, 
         -2.5*np.log10(cc_ardi['sensitivity_student']), 
         'm-', label='5-sigma contrast (PCA-ARDI)', alpha=0.5)
plt.plot(r_b*pxscale_naco, 
         -2.5*np.log10(f_b/starphot), 'gs', label='Beta Pic b')
plt.gca().invert_yaxis()
plt.ylabel('Contrast (mag)')
plt.xlabel('Separation (arcsec)')
_ = plt.legend(loc='best')
plt.show()
../_images/37392b6cdb2180e99d77fd09b5c3770c0dc547b09f29e38f72874b2efd20a751.png

Note that for well-correlated reference images (not the case here with our fake reference cube), the contrast achieved with PCA-RDI can become deeper than PCA-ADI at short separation, where little linear motion for field rotation is available.

Check out tutorial 4 for more examples of contrast curve calculations.

6.2.5. NEGFC

Tutorial 5A showcases in details how to use the NEGFC technique (combined with the ADI strategy) to estimate the position and flux of a directly imaged companion in your data. The typical workflow involves obtaining a first guess with a simplex algorithm, followed by using a MCMC algorithm for better estimates on the uncertainties on the parameters of the companion.

Below we just briefly illustrate how to use the NEGFC technique with the simplex (firstguess) algorithm for either RDI or ARDI strategies, but the same applies to the NEGFC algorithm.

First, let’s inject a fake companion in the data:

from vip_hci.fm import cube_inject_companions
rad_fc = 30.5
theta_fc = 240
flux_fc = 400.
gt = [rad_fc, theta_fc, flux_fc]
cubefc = cube_inject_companions(cube, psf_template=psfn, angle_list=angs, flevel=flux_fc, plsc=pxscale_naco, 
                                rad_dists=[rad_fc], theta=theta_fc, n_branches=1)
from vip_hci.var import frame_center
cy, cx = frame_center(cube[0])
xy_test = cx+rad_fc*np.cos(np.deg2rad(theta_fc)), cy+rad_fc*np.sin(np.deg2rad(theta_fc))
xy_test
(34.749999999999986, 23.58622518457463)

6.2.5.1. PCA-RDI

By default, the algorithm used by NEGFC is PCA in a single annulus (pca_annulus) encompassing the location of the companion. This function behaves in the same way as full-frame PCA when provided with a reference cube (i.e., it triggers pure RDI). The reference cube is provided through the algo_options argument, which allows to provide additional arguments associated to the algorithm:

from vip_hci.fm import firstguess
r_rdi, theta_rdi, f_rdi = firstguess(cubefc, angs, psfn, ncomp=13, planets_xy_coord=[xy_test], 
                                     fwhm=fwhm_naco, f_range=None, annulus_width=4*fwhm_naco, 
                                     aperture_radius=2, simplex=True, plot=True,
                                     algo_options={'cube_ref':cube_ref}, verbose=True)
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-07-05 18:25:11
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
             Planet 0           
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

Planet 0: flux estimation at the position [34.749999999999986,23.58622518457463], running ...
Step | flux    | chi2r
1/30   0.100   1.025
2/30   0.149   1.025
3/30   0.221   1.025
4/30   0.329   1.024
5/30   0.489   1.024
6/30   0.728   1.023
7/30   1.083   1.022
8/30   1.610   1.021
9/30   2.395   1.018
10/30   3.562   1.014
11/30   5.298   1.007
12/30   7.880   0.999
13/30   11.721   0.984
14/30   17.433   0.962
15/30   25.929   0.932
16/30   38.566   0.892
17/30   57.362   0.833
18/30   85.317   0.752
19/30   126.896   0.631
20/30   188.739   0.426
21/30   280.722   0.200
22/30   417.532   0.053
23/30   621.017   0.332
24/30   923.671   1.098
25/30   1373.824   2.331
../_images/fe023b3d9d88c4ca4de38a3b8c93995f7b553c52fb83b0090bc661ece0f70090.png
Planet 0: preliminary position guess: (r, theta)=(30.5, 240.0)
Planet 0: preliminary flux guess: 417.5
Planet 0: Simplex Nelder-Mead minimization, running ...
Planet 0: Success: True, nit: 105, nfev: 229, chi2r: 0.0510203564208559
message: Optimization terminated successfully.
Planet 0 simplex result: (r, theta, f)=(30.558, 239.944, 402.470) at 
          (X,Y)=(34.70, 23.55)

 ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― 
DONE !
 ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Running time:  0:00:49.165751
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
print("Ground truth injected parameters:", gt)
print("Estimated parameters by NEGFC (simplex):", [r_rdi[0], theta_rdi[0], f_rdi[0]])
Ground truth injected parameters: [30.5, 240, 400.0]
Estimated parameters by NEGFC (simplex): [30.558242070331474, 239.943979957012, 402.4698017374459]

6.2.5.2. PCA-ARDI

To activate the ARDI strategy, one needs to use the pca_annular function (note the minor but important difference with pca_annulus). In this case, we also request a rotation threshold (delta_rot) to be 1 in the algorithm options dictionary.

r_ardi, theta_ardi, f_ardi = firstguess(cubefc, angs, psfn, ncomp=13, planets_xy_coord=[xy_test], 
                                        fwhm=fwhm_naco, f_range=None, annulus_width=4*fwhm_naco, 
                                        aperture_radius=2, simplex=True, plot=True, algo=pca_annular,
                                        algo_options={'cube_ref':cube_ref, 'verbose':False,
                                                      'delta_rot':1}, 
                                        verbose=True)
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-07-05 18:26:00
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
             Planet 0           
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

Planet 0: flux estimation at the position [34.749999999999986,23.58622518457463], running ...
Step | flux    | chi2r
1/30   0.100   1.890
2/30   0.149   1.890
3/30   0.221   1.889
4/30   0.329   1.888
5/30   0.489   1.887
6/30   0.728   1.885
7/30   1.083   1.882
8/30   1.610   1.877
9/30   2.395   1.871
10/30   3.562   1.861
11/30   5.298   1.846
12/30   7.880   1.824
13/30   11.721   1.791
14/30   17.433   1.744
15/30   25.929   1.674
16/30   38.566   1.570
17/30   57.362   1.426
18/30   85.317   1.212
19/30   126.896   0.928
20/30   188.739   0.592
21/30   280.722   0.233
22/30   417.532   0.035
23/30   621.017   0.474
24/30   923.671   2.723
25/30   1373.824   9.889
../_images/27c71778a9e9474e3dfea74840eb8927e047b38f27620e56d1dd325d59c8adcb.png
Planet 0: preliminary position guess: (r, theta)=(30.5, 240.0)
Planet 0: preliminary flux guess: 417.5
Planet 0: Simplex Nelder-Mead minimization, running ...
Planet 0: Success: True, nit: 85, nfev: 195, chi2r: 0.033955191901336564
message: Optimization terminated successfully.
Planet 0 simplex result: (r, theta, f)=(30.534, 240.161, 417.392) at 
          (X,Y)=(34.81, 23.51)

 ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― 
DONE !
 ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Running time:  0:01:39.080224
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
print("Ground truth injected parameters:", gt)
print("Estimated parameters by NEGFC (simplex):", [r_ardi[0], theta_ardi[0], f_ardi[0]])
Ground truth injected parameters: [30.5, 240, 400.0]
Estimated parameters by NEGFC (simplex): [30.534119831048265, 240.16124692799315, 417.39168439970615]

6.3. RDI for disk imaging

6.3.1. PCA with data imputation

Now let’s try the Principal Component Analysis (PCA)-based algorithms (Amara & Quanz 2012, Soummer et al. 2012) in vip.pca.

We will more specifically use PCA-RDI with data imputation, as presented in Ren (2023). We refer to that paper for the maths and nomenclature used in this section, including the concepts of anchor and boat masks.

Let’s first define a binary mask, with zeros covering the area of the image which could host circumstellar signals. Let’s assume it covers the inner 20px radius:

from vip_hci.var import mask_circle

anchor_mask = np.ones_like(cube[0])
anchor_mask = mask_circle(anchor_mask, 20)

plot_frames(anchor_mask)
../_images/1fe88fe9357293bfd8b0269077e3c8db0fb0c79c5af6e3a01880154f158b9c65.png

Let’s set to 30 the number of principal components ncomp considered for model creation, and let’s run the PCA algorithm (feel free to test other values):

from vip_hci.psfsub import pca
ncomp = 30

mask_rdi1 = anchor_mask   # can be a 2D bool array

fr_pca_di1 = pca(cube, angs, ncomp=ncomp, cube_ref=cube_ref, mask_rdi=mask_rdi1, nproc=5)
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-07-05 18:27:39
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
System total memory = 17.180 GB
System available memory = 3.793 GB
Done de-rotating and combining
Running time:  0:00:00.530680
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
plot_frames(fr_pca_di1, grid=True)
../_images/1989cd7ec3fe6c3d7f6cc3579c6fe096165d3dbb5e702c6424cd636a754ed2f5.png

There are strong residuals because the reference cube is not well correlated in this example. A better approach would be to define 2 masks, anchor and boat masks, as in Ren et al. (2023), where the boat mask only covers the relevant part of the field of view you are interested in (e.g. mask the IWA of the coronagraph or the areas too far out in the field).

In the case of a single mask as above, it is assumed to correspond to the anchor mask, and that the boat mask simply covers the full image.

Let’s consider more sophisticated annular masks than above, and leverage the get_annulus_segments utility function in VIP:

from vip_hci.var import get_annulus_segments
get_annulus_segments?
Signature:
get_annulus_segments(
    data,
    inner_radius,
    width,
    nsegm=1,
    theta_init=0,
    optim_scale_fact=1,
    mode='ind',
    out=False,
)
Docstring:
Return indices or values in segments of a centered annulus.

The annulus is defined by ``inner_radius <= annulus < inner_radius+width``.

Parameters
----------
data : 2d numpy ndarray or tuple
    Input 2d array (image) ot tuple with its shape.
inner_radius : float
    The inner radius of the donut region.
width : float
    The size of the annulus.
nsegm : int
    Number of segments of annulus to be extracted.
theta_init : int
    Initial azimuth [degrees] of the first segment, counting from the
    positive x-axis counterclockwise.
optim_scale_fact : float
    To enlarge the width of the segments, which can then be used as
    optimization segments (e.g. in LOCI).
mode : {'ind', 'val', 'mask'}, optional
    Controls what is returned: indices of selected pixels, values of
    selected pixels, or a boolean mask.
out : bool; optional
    Return all indices or values outside the centered annulus.

Returns
-------
indices : list of ndarrays
    [mode='ind'] Coordinates of pixels for each annulus segment.
values : list of ndarrays
    [mode='val'] Pixel values.
masked : list of ndarrays
    [mode='mask'] Copy of ``data`` with masked out regions.

Note
----
Moving from ``get_annulus`` to ``get_annulus_segments``:

.. code::python
    # get_annulus handles one single segment only, so note the ``[0]`` after
    the call to get_annulus_segments if you want to work with one single
    segment only.

    get_annulus(arr, 2, 3, output_indices=True)
    # is the same as
    get_annulus_segments(arr, 2, 3)[0]

    get_annulus(arr, inr, w, output_values=True)
    # is the same as
    get_annulus_segments(arr, inr, w, mode="val")[0]

    get_annulus(arr, inr, w)
    # is the same as
    get_annulus_segments(arr, inr, w, mode="mask")[0]

    # the only difference is the handling of the border values:
    # get_annulus_segments is `in <= ann < out`, while get_annulus is
    # `in <= ann <= out`. But that should make no difference in practice.
File:      ~/GitHub/VIP/vip_hci/var/shapes.py
Type:      function
coro = 12    # discard signal within ~IWA of coronagraphic mask
ring_in = 24
ring_out = 40

ones = np.ones_like(cube[0])
anchor_mask = get_annulus_segments(ones, ring_in, ring_out-ring_in, mode="mask")[0]
boat_mask = get_annulus_segments(ones, coro, ring_out-coro, mode="mask")[0]

plot_frames((anchor_mask, boat_mask))
../_images/4e0d79dcbbb05c696a9d7670b7d44e866bf94b9e1ee625cb6ab017e454eea0af.png
from vip_hci.psfsub import pca
ncomp = 30

mask_rdi2 = (anchor_mask, boat_mask)   # can be a tuple of 2 2D bool array

fr_pca_di2 = pca(cube, angs, ncomp=ncomp, cube_ref=cube_ref, mask_rdi=mask_rdi2)
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Starting time: 2024-07-05 18:27:40
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
System total memory = 17.180 GB
System available memory = 3.964 GB
Done de-rotating and combining
Running time:  0:00:02.099145
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
plot_frames(fr_pca_di2, grid=True)
../_images/762e87e86d444e80f852ebaabf94c67833f37b1dcc2dfd79dd21e6a728b5bec7.png

6.3.2. Iterative PCA

IPCA-ADI

from vip_hci.greedy import ipca

fr_ipca_ADI = ipca(cube=cube, angle_list=angs, ncomp=15, nit=20)
Iterating...
0% [##############################] 100% | ETA: 00:00:00
Total time elapsed: 00:02:28
%matplotlib inline
plot_frames(fr_ipca_ADI, grid=True)
../_images/f13ea42fea39e214bbadd8da3457518de032532811284f9f7d18a2279dad8ba1.png

To check more outputs, including the cube of images obtained at each iteration, it’s a matter of setting full_output=True:

from vip_hci.greedy import ipca

res_ipca_ADI = ipca(cube=cube, angle_list=angs, ncomp=15, nit=20, full_output=True)
Iterating...
0% [##############################] 100% | ETA: 00:00:00
Total time elapsed: 00:03:01
fr_ipca, it_cube, sig_images, residuals_cube, residuals_cube_, stim_cube, it_cube_nd = res_ipca_ADI
plot_cubes(it_cube, grid=True)
:Dataset   [x,y,time]   (flux)
:Cube_shape	[101, 101, 20]

IPCA-ARDI (recommended)

For all cases above, and this one below, the number of PCs is constant, and the number of iterations set by nit.

from vip_hci.greedy import ipca

res_ipca_ARDI = ipca(cube=cube, angle_list=angs, ncomp=15, nit=20, cube_ref=cube_ref, strategy='ARDI', full_output=True)
Iterating...
0% [##############################] 100% | ETA: 00:00:00
Total time elapsed: 00:05:00
fr_ipca_ARDI, it_cube_ARDI, sig_images, residuals_cube, residuals_cube_, stim_cube, it_cube_nd = res_ipca_ARDI
%matplotlib inline
plot_frames(fr_ipca_ARDI)
../_images/0962d0067c4b5ce4ca59310991fc048308430e7c55f22d92da76b2d00c315231.png
plot_cubes(it_cube_ARDI, grid=True)
:Dataset   [x,y,time]   (flux)
:Cube_shape	[101, 101, 20]

It is possible to increase the number of PCs with the iterations by setting mode='Pairet21', in this case ncomp is the final number of PCs at the last iteration, and nit is the number of iterations with each tested number of PCs:

from vip_hci.greedy import ipca

res_ipca_ARDI_incr = ipca(cube=cube, angle_list=angs, ncomp=30, nit=5, cube_ref=cube_ref, strategy='ARDI', 
                          mode='Pairet21', full_output=True)
Iterating...
0% [##############################] 100% | ETA: 00:00:00
Total time elapsed: 00:31:21
fr_ipca_ARDI_incr, it_cube_ARDI_incr, sig_images, residuals_cube, residuals_cube_, stim_cube, it_cube_nd = res_ipca_ARDI_incr
%matplotlib inline
plot_frames(fr_ipca_ARDI_incr, grid=True)
../_images/c72c8ed24cc4f6a1e10b55475a72421a6f3fc282469e9e5057a99b87bf9fe211.png
plot_cubes(it_cube_ARDI_incr, grid=True, vmin=-20, vmax=50)
:Dataset   [x,y,time]   (flux)
:Cube_shape	[101, 101, 150]

IPCA-ARDI with data imputation at the first iteration (not yet much tested, but possibly even better than regular IPCA-ARDI)

from vip_hci.greedy import ipca

res_ipca_ARDI_di = ipca(cube=cube, angle_list=angs, ncomp=30, nit=42, cube_ref=cube_ref,
                        strategy='ARDI', mask_rdi=mask_rdi2, full_output=True)
Iterating...
0% [##########################    ] 100% | ETA: 00:00:48Final Convergence criterion met after 37 iterations

An internal criterion of convergence exists, and if met the algorithm can stop earlier than the requested number of iterations. The convergence criterion can be adapted with arguments rtol and atol, the relative and absolute tolerance, respectively, which concerns the difference between the images of 2 consecutive iterations.

fr_ipca_ARDI_di, it_cube_ARDI_di, sig_images, residuals_cube, residuals_cube_, stim_cube, it_cube_nd = res_ipca_ARDI_di
%matplotlib inline
plot_frames(fr_ipca_ARDI_di, grid=True)
../_images/f2423e941b1a61701aa1b4147e57d85634fee60446f80eb8d8d271c7048b69af.png
plot_cubes(it_cube_ARDI_di, grid=True)
:Dataset   [x,y,time]   (flux)
:Cube_shape	[101, 101, 38]