How to account for field rotation

Table of Contents

1 Introduction

Unlike the venerable Palomar Hale Telescope that stands out as one of the last great telescopes to feature an equatorial mount, large telescopes use a more compact alt-azimuthal mount which means that as the telescope tracks a given target, if one were to lie down on the primary mirror, one would see the sky rotate. At the focus of an instrument that is at a fixed orientation relative to the pupil, the sky also rotates. Thus when tracking a star with companions as they transit, the companions will rotate around the primary.

It is possible to compensate for this apparent motion of the target that prevents the use of very long integrations, with a field derotator, but from the point of view of the camera, it is now the pupil of the telescope that rotates, and this will result in spinning diffraction features.

For high-contrast imaging, field rotation turns out to be a useful feature that makes it possible to easily disentangle the static diffraction features from actual companions. Full-aperture kernel-phase and sparse aperture closure-phase operate in fixed pupil mode, which means that they have to compose with sky rotation.

This page introduces how to use the XARA pipeline to account for field rotation when interpreting kernel-phase.

2 Detector position angle

When experiencing field rotation, the only way to make sense of the data being acquired at any instant is to keep track of the orientation of the detector relative to the true north, with an angle refered to as the detector position angle. In the software, the variable keeping track of this angle is called DETPA.

Assuming that one has been processing an image datacube with the XARA pipeline using the default cube extraction method (cf. example script), a look at the kpo1.DETPA variable would reveal an array filled with zeros: by default, the pipeline simply assumes that the detector pixels are aligned with the on-sky reference axis, with the north (N) pointing up and east (E) to the left, as is actually the case for PHARO images.

The convention used in the software is illustrated in the following image. The detector position angle DETPA is labeled ω and the companion position angle measured relative to the north is labeled \(\theta\).

DETPA.png

At the time of the extraction, the knowledge of the DETPA is not required: the only thing that matters is that you have a good, reliable discrete model that correctly accounts for all the features of the pupil. But when the time to interpret the data comes, such as when attempting to fit a binary kernel-phase model to the kernel-phase extracted from the datacube, then the proper DETPA info must be fed to the kpo data structure. Assuming the first data cube (index = 0) contains nim images, and that the known detector position angles are available in a distinct array called mydetpa, one can directly update the values using:

kpo1.DETPA[0] = mydetpa[:nim]

so that later, the call of kpo1.kpd_binary_model([sep, PA, con]) returns a list of arrays that matches the shape of the data acquired.

3 Simulating a dataset with field rotation

The following program uses the XAOSIM package to simulate a SPHERE SAM data-set in the K1 filter. XAOSIM features a preset for the AO system of GRAVITY that we can use here since SPHERE equips one of the VLT telescopes.

It saves a fits datacube of SAM interferograms recorded on a binary and (for the purpose of the demonstration) as the detector position angle varies continuously from 0 to 89 degrees… over 90 frames (1 additional degree of rotation per frame).

Note that no atmosphere or AO residuals are used here: the point of this tutorial is to see how field rotation affects the interpretation of kernel-phase.

Also note that we use XAOSIM but do not use its pseudo real-time features. In order to produce images, we need to do two things at each iteration:

  • make_image() updates the image on a shared memory data structure
  • get_image() reads the data on the shared memory data structure and returns an array
import xaosim as xs
import numpy as np
from xaosim.pupil import SPHERE_IRDIS_SAM as sam
import astropy.io.fits as pf
import matplotlib.pyplot as plt

pscale = 12.255   # SPHERE IRDIS plate scale (mas/pixel)
cwavel = 2.11e-6  # K1 filter central wavelength (in meters)

try:
        _ = sphere.name
except NameError:
        sphere = xs.instrument("gravity", csz=480)
        sphere.cam2.update_cam(pscale=pscale, wl=cwavel)
        csz = sphere.csz
        ppscale = sphere.cam2.pdiam / csz  # pupil plate scale (meter/pixel)
        pupil0 = sphere.cam2.pupil.copy()  # the original pupil

sphere.cam2.update_cam(between_pixel=False)
sphere.cam2.pupil *= sam(csz)  # replacing the pupil by the SAM mask

# ----------------------------------------------------------------------
# prepare for later image crops: the cube built here is 215 x 215 pixels
# ----------------------------------------------------------------------

rsz = 215  # image size retained for cube
sphere.cam2.make_image()  # update simulation$
img0 = sphere.cam2.get_image()  # take an image
isz = img0.shape[0]  # image size

xy0 = (isz - rsz) // 2 + 1
xy1 = xy0 + rsz

imgc = img0[xy0:xy1, xy0:xy1]  # image cropped

# ----------------------------------------------------------------------
# properties of the simulated companion.
# ----------------------------------------------------------------------
sep = 115.  # separation (in mas)
PA = 257.0  # position angle (in degrees)
con = 0.1   # contrast (secondary/primary)

# ----------------------------------------------------------------------
# field rotation parameters: detector P.A. varies from 0 to 90 degrees
# ----------------------------------------------------------------------
nim = 90
detpas = np.arange(nim)
dcube = np.zeros((nim, rsz, rsz))

off_pointing = sphere.cam2.off_pointing  # short-hand!

# ----------------------------------------------------------------------
# simulating field rotation while observing the binary
# ----------------------------------------------------------------------
sep_pix = sep/pscale  # separation in # of pixels
for ii, detpa in enumerate(detpas):
        azim = (PA + detpa)*np.pi/180.0
        dx = -sep_pix * np.sin(azim)
        dy = sep_pix * np.cos(azim)
        sphere.cam2.make_image(phscreen=off_pointing(dx, dy))
                print("\r%04d: dra=%+6.2f, ddec=%+6.2f" % (ii, -dx, dy),
                  end="", flush=True)
        img1 = sphere.cam2.get_image()[xy0:xy1, xy0:xy1]
        dcube[ii] = imgc + con * img1

# save a datacube with a binary including rotation model
pf.writeto("sim_cube.fits", dcube, overwrite=True)

f1 = plt.figure(figsize=(5, 5))
ax1 = f1.add_subplot(111)
ax1.imshow(dcube[0]**0.2)
ax1.set_title("First frame of the saved datacube")
f1.set_tight_layout(True)

frame_SAM_example.png

4 Kernel-phase interpretation of the dataset

To extract sensible kernel-phase out of the dataset that was just simulated, one first needs a model of the aperture. This is a sparse aperture masking dataset: there are more or less sophisticated ways of building models for one such dataset but we will stick to the simplest approach: our model will consist in only one sampling point per aperture.

4.1 Kernel-phase extraction

The coordinates of the seven holes of the SPHERE IRDIS SAM mask are used as input (coordinates taken from the source code of the pupil.SPHERE_IRDIS_SAM() function in the XAOSIM package source code) to build the model. These seven holes form 21 distinct baselines that can be assembled to form 15 kernel- or closure-phases.

With the detector plate scale and the wavelength of the filter in hand, the data cube can directly be processed and results in the compilation of 90 kernel-phase vector realisations, that all include the effect of a (thus far unaccounted for) varying detector position angle (DETPA).

import matplotlib.pyplot as plt
import numpy as np
import xara
import astropy.io.fits as pf
from xara.core import polar_coord_map as pcmap


# ==== KERNEL MODEL CREATION ====
mask_pos = 0.8 * np.array(
        [[+0.000, -3.9366],
         [-3.788, -1.7496],
         [+3.788, -1.7496],
         [-1.894, -0.6561],
         [-3.788,  0.4374],
         [-1.894,  3.7179],
         [+1.894,  3.7179]])

kpo1 = xara.KPO(array=mask_pos)

#  ==== KERNEL-PHASE EXTRACTION ====
tgt = pf.getdata("sim_cube.fits")
nim = tgt.shape[0]  # number of images
pscale = 12.255     # mas/pixel
cwavel = 2.11e-6    # central wavelength in meters

kpo1.extract_KPD_single_cube(tgt, pscale, cwavel)
data1 = np.array(kpo1.KPDT[0])

Because of the field rotation, we expect to see our 15 kernel-phases evolve over time, which is what the following snippet attempts to illustrate:

dmax = np.abs(data1).max()
nd = int(-np.round(np.log10(dmax)))
offset = np.round(dmax, nd)

f1 = plt.figure(figsize=(10, 5))
ax1 = f1.add_subplot(111)
for ii in range(kpo1.kpi.nbkp):
  ax1.plot(data1[:, ii] + offset * ii, label=r"k$_{%d}$" % (ii,))
ax1.legend()
ax1.set_xlabel("Detector Position Angle (degrees)")
ax1.set_ylabel("Kernel-phase value (+ offset)")
ax1.set_title("Kernel-phase with field rotation")
f1.set_tight_layout(True)
f1.savefig("kp_with_field_rotation.png")

kp_with_field_rotation.png

4.2 Kernel-phase interpretation

In order to make sense of the data in the presence of field rotation, the KPO needs to know what is the value of the detector position angle (DETPA) for each frame. In practice, the DETPA would have to be extracted from the FITS header information that accompanies images produced by an instrument… or by additional byproducts of the early data massing software that a program usually relies on. Here, we know that the DETPA goes from 0 to 89 degrees in 1 degree increment and we can directly feed that to the KPO data structure.

4.2.1 1st guess: colinearity map

It is now possible to take advantage of a convenient kernel-phase investigation tool called the colinearity map. The following code snippet features one such use case: it computes a colinearity map over a 40 x 40 grid with a 10 mas step size. Wherever the signal recorded in the KPO data structure (present at index=0) matches the signal of a binary, the map peaks and a first estimate for the separation, position angle and contrast can be extracted from this first pass colinearity map. How close the result of this first estimate is from the expected value depends on the granularity of the map (here 10 mas).

kpo1.DETPA[0] = np.arange(nim)  # feed the DETPA!

gsize = 40  # grid size (gsize x gsize)
gstep = 10  # grid step in mas

mycomap = kpo1.kpd_colinearity_map(gsize, gstep, index=0)
f1 = xara.field_map_cbar(mycomap, gstep, vlabel="colinearity")
f1.suptitle("Signal colinearity map")

dist, azim = pcmap(gsize, gsize, scale=gstep)

x0, y0 = np.argmax(mycomap) % gsize, np.argmax(mycomap) // gsize
print("max colinearity found for sep = %.2f mas and ang = %.2f deg" % (
                dist[y0, x0], azim[y0, x0]))

kp_field_rot_colinearity_map.png

For this example, the thus far best solution is reasonably close to the values that were effectively simulated in the first place:

  Sep (mas) PA (deg) con
Simulation input 115 257 10
Analysis output 114.02 254.75 9.75

4.2.2 Correlation plot

One other usual way of representing how this solution matches the actual dataset is to represent both in a correlation diagram. Building up from the above code, this how one could go about displaying this:

p0 = [dist[y0, x0], azim[y0, x0], mycomap.max()]
kmodel = kpo1.kpd_binary_model(p0)

f1 = plt.figure(figsize=(5, 5))
ax1 = f1.add_subplot(111)
ax1.plot(kmodel.flatten(), data1.flatten(), '.')
ax1.set_xlabel("theoretical expected kernel-phase")
ax1.set_ylabel("experimental kernel-phase")
ax1.set_title("colinearity map optimum")
f1.set_tight_layout(True)
f1.savefig("kp_field_rot_correlation_plot.png")

kp_field_rot_correlation_plot.png

4.2.3 Further optimisation

The first estimate offered by the analysis of the colinearity map can serve as a first guess for an optimization procedure:

optim = kpo1.binary_model_fit(p0)
p1 = optim[0]  # best fit parameters
kmodel = kpo1.kpd_binary_model(p1)

f1 = plt.figure(figsize=(5, 5))
ax1 = f1.add_subplot(111)
ax1.plot(kmodel.flatten(), data1.flatten(), '.')
ax1.set_xlabel("theoretical expected kernel-phase")
ax1.set_ylabel("experimental kernel-phase")
ax1.set_title("colinearity map optimum")
f1.set_tight_layout(True)
f1.savefig("kp_field_rot_correlation_plot_2.png")

kp_field_rot_correlation_plot_2.png

In this noise free case, the match between the solution and the data is of course perfect. Real world applications will of course not look as good as this ideal scenario! But at least, you should now feel able to tackle the kernel-phase analysis of data featuring field rotation!

Back to the documentation index


XARA documentation page
Author: Frantz Martinache