import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Ellipse
from matplotlib.patches import Wedge
%matplotlib inline
Developed by K. Ward-Duong (kwduong@stsci.edu), last revision 2021-03-22
Some of the most important steps in planning and preparing coronagraphic observations with STIS involve:
Considerations for these decisions include avoiding the locations of the central star’s diffraction spikes relative to any scientific feature(s) of interest, and verifying the true north angle of the CCD detector to understand where a given companion or disk feature might be located.
To this end, the functions and examples in this notebook are meant to be an illustrative guide to visualizing possible aperture+companion+orientation configurations.

The purpose of this notebook is to provide a function to visualize the STIS detector orientation for various astrophysical scenes and instrumental/telescope configurations. Examples below include a simple star+companion scene, and a star+disk scene, in order to determine the visibility of the scientific feature of interest relative to the STIS occulter of choice, stellar diffraction spikes, and true north angle.
The function _showorients is used to display images and takes the following keywords:
inputs
----------------
orients : (float or list) single ORIENT value or list of values
aperture : (str) which STIS occulting aperture position to use. Valid options are:
'BAR10', 'BAR5', 'WEDGEA0.6', 'WEDGEA1.0', 'WEDGEA2.0', 'WEDGEB1.0'
fov : (float) region of the detector to display, in arcsec
feature : (str) whether to display a point source companion or extended disk ("companion" or "disk")
featurepa : (float) position angle of feature of interest in degrees E of N
postarg : (tuple) x, y offset from the fiducial aperture in arcsec. Default is no offset (0,0)
avoidspikes : (float, default=20) 'avoidance' region to avoid stellar diffraction spikes
optional arguments
-----------------
featuresep : (float) separation of companion
disk_height : (float) semiminor axis of the disk in arcsec
disk_width : (float) semimajor axis of the disk in arcsec
featuresep keyword must be called in the function. disk_height and disk_width must be supplied to the function call. postarg (target position offset from fiducial position) are set to (0,0), but other values can be supplied if one wishes to visualize an offset or non-supported aperture.avoidspikes is 20 degrees, but can also be adjusted as the user sees fit.def show_orients(orients, aperture, fov, feature, featurepa, postarg = (0,0), avoidspikes=20, **kwargs):
"""
show_orients
-----------------
Function to visualize the location of a companion or disk feature of interest relative to the STIS diffraction spikes.
Displays figure illustrating positions based upon ORIENT (+U3 vector) given a single value or a range of values.
inputs
----------------
orients : (float or list) single ORIENT value or list of values
aperture : (str) which STIS occulting aperture position to use. Valid options are:
'BAR10', 'BAR5', 'WEDGEA0.6', 'WEDGEA1.0', 'WEDGEA2.0', 'WEDGEB1.0'
fov : (float) region of the detector to display, in arcsec
feature : (str) whether to display a point source companion or extended disk ("companion" or "disk")
featurepa : (float) position angle of feature of interest in degrees E of N
postarg : (tuple) x, y offset from the fiducial aperture in arcsec. Default is no offset (0,0)
avoidspikes : (float, default=20) 'avoidance' region to avoid stellar diffraction spikes
optional arguments
-----------------
featuresep : (float) separation of companion
disk_height : (float) semiminor axis of the disk in arcsec
disk_width : (float) semimajor axis of the disk in arcsec
returns
---------------
(none - displays figures in line)
"""
if type(orients) is int:
orients = [orients]
for orient in orients:
# initializing the figure
fig = plt.figure(figsize=(15,15))
# setting the axis limits in [left, bottom, width, height]
rect = [0.1, 0.1, 0.8, 0.8] # was 0.1 0.1, 0.8 0.8
# the cartesian axis:
ax_cart = fig.add_axes(rect)
# the polar axis:
ax_polar = fig.add_axes(rect, polar=True, frameon=False)
# setting the size of the zoomed view of the detector, defined manually
# could also be set based upon which of the occulters is being used
ax_cart.set_xlim(-fov/2,fov/2)
ax_cart.set_ylim(-fov/2,fov/2)
# define a dictionary here mapping the occulter to the 'zoomed' version of the detector, in arcsec
aper_pos = {'BAR10':(-1.5, -1.5),
'BAR5' :(-0.25, -0.25)}
if aperture == 'BAR10':
rectangle = plt.Rectangle((-1.5 - postarg[0], -1.5 - postarg[1]), 3, 10, fc='dodgerblue') #tuple bottom lefthand corner, width, height
ax_cart.add_patch(rectangle)
# add edges of detector
bottom = plt.Rectangle((-31.11 - postarg[0], -42.26 -5 - postarg[1]), 50, 5, fc='dodgerblue') #tuple bottom lefthand corner, width, height
top = plt.Rectangle((-31.11 - postarg[0], 7.74 - postarg[1]), 50, 5, fc='dodgerblue') #tuple bottom lefthand corner, width, height
left = plt.Rectangle((-31.11 - 5 - postarg[0], -42.26 - postarg[1]), 5, 50, fc='dodgerblue') #tuple bottom lefthand corner, width, height
right = plt.Rectangle((18.89 - postarg[0], -42.26 - postarg[1]), 5, 50, fc='dodgerblue') #tuple bottom lefthand corner, width, height
ax_cart.add_patch(bottom)
ax_cart.add_patch(left)
ax_cart.add_patch(right)
ax_cart.add_patch(top)
elif aperture == 'BAR5':
rectangle = plt.Rectangle((-1.5 - postarg[0], 0.15 - postarg[1]), 3, 0.4, angle=-12, fc='dodgerblue') #tuple bottom lefthand corner, width, height
ax_cart.add_patch(rectangle)
angle = plt.Rectangle((2.75 - 1.25 - postarg[0], -0.5 + 0.4 - postarg[1]), 2, 0.5, fc='dodgerblue') #tuple bottom lefthand corner, width, height
ax_cart.add_patch(angle)
angle2 = plt.Rectangle((2.5 - 1.25 - postarg[0], -0.85 + 0.4 - postarg[1]), 0.75, 0.4, angle = 45 , fc='dodgerblue') #tuple bottom lefthand corner, width, height
ax_cart.add_patch(angle2)
detedge = plt.Rectangle((4-1.25 - postarg[0], -10 + 0.4 - postarg[1]), 50, 50, fc='dodgerblue') #tuple bottom lefthand corner, width, height
ax_cart.add_patch(detedge)
# -- add WEDGEA positions -- #
elif aperture == 'WEDGEA0.6':
# wedge needs to start 6% up the detector, wedge origin about 10" below detector
wedgeA1 = Wedge((0 - postarg[0],-13.3-postarg[1]), 60, 88.5, 91.5, color='dodgerblue') # position, radius, theta1, theta2
ax_cart.add_patch(wedgeA1)
# add wedgeB
wedgeB = Wedge((-25 - postarg[0],11.85-postarg[1]), 60, -1.5, 1.5, color='dodgerblue') # position, radius, theta1, theta2
ax_cart.add_patch(wedgeB)
# add edges of detector
bottom = plt.Rectangle((-15.5 - postarg[0], -5.5 -5 - postarg[1]), 50, 5, fc='dodgerblue') #tuple bottom lefthand corner, width, height
top = plt.Rectangle((-15.5 - postarg[0], 44.5 - postarg[1]), 50, 5, fc='dodgerblue') #tuple bottom lefthand corner, width, height
left = plt.Rectangle((-15.5 - 5 - postarg[0], -5.5 - postarg[1]), 5, 50, fc='dodgerblue') #tuple bottom lefthand corner, width, height
right = plt.Rectangle((34.5 - postarg[0], -5.5 - postarg[1]), 5, 50, fc='dodgerblue') #tuple bottom lefthand corner, width, height
ax_cart.add_patch(bottom)
ax_cart.add_patch(left)
ax_cart.add_patch(right)
ax_cart.add_patch(top)
elif aperture == 'WEDGEA1.0':
# wedge needs to start 11% up the detector
wedgeA1 = Wedge((0 - postarg[0], -20 - postarg[1]), 60, 88.5, 91.5, color='dodgerblue') # position, radius, theta1, theta2
ax_cart.add_patch(wedgeA1)
# add wedgeB
wedgeB = Wedge((-25 - postarg[0],4.47-postarg[1]), 60, -1.5, 1.5, color='dodgerblue') # position, radius, theta1, theta2
ax_cart.add_patch(wedgeB)
# add edges of detector
bottom = plt.Rectangle((-15.5 - postarg[0], -10.7 -5 - postarg[1]), 50, 5, fc='dodgerblue') #tuple bottom lefthand corner, width, height
top = plt.Rectangle((-15.5 - postarg[0], 39.4 - postarg[1]), 50, 5, fc='dodgerblue') #tuple bottom lefthand corner, width, height
left = plt.Rectangle((-15.5 - 5 - postarg[0], -10.7 - postarg[1]), 5, 50, fc='dodgerblue') #tuple bottom lefthand corner, width, height
right = plt.Rectangle((34.43 - postarg[0], -10.7 - postarg[1]), 5, 50, fc='dodgerblue') #tuple bottom lefthand corner, width, height
ax_cart.add_patch(bottom)
ax_cart.add_patch(left)
ax_cart.add_patch(right)
ax_cart.add_patch(top)
elif aperture == 'WEDGEA2.0':
# wedge needs to start 60% up the detector
wedgeA2 = Wedge((0 - postarg[0], -40-postarg[1]), 60, 88.5, 91.5, color='dodgerblue') # position, radius, theta1, theta2
ax_cart.add_patch(wedgeA2)
# add wedgeB
wedgeB = Wedge((-25-postarg[0],-15.5-postarg[1]), 60, -1.5, 1.5, color='dodgerblue') # position, radius, theta1, theta2
ax_cart.add_patch(wedgeB)
# add edges of detector
bottom = plt.Rectangle((-15.5 - postarg[0], -30 -5 - postarg[1]), 50, 5, fc='dodgerblue') #tuple bottom lefthand corner, width, height
top = plt.Rectangle((-15.5 - postarg[0], 20 - postarg[1]), 50, 5, fc='dodgerblue') #tuple bottom lefthand corner, width, height
left = plt.Rectangle((-15.5 - 5 - postarg[0], -30 - postarg[1]), 5, 50, fc='dodgerblue') #tuple bottom lefthand corner, width, height
right = plt.Rectangle((34.43 - postarg[0], -30 - postarg[1]), 5, 50, fc='dodgerblue') #tuple bottom lefthand corner, width, height
ax_cart.add_patch(bottom)
ax_cart.add_patch(left)
ax_cart.add_patch(right)
ax_cart.add_patch(top)
# -- add WEDGEB positions -- #
elif aperture == 'WEDGEB1.0':
# wedge needs to start 23% from the detector left edge
wedgeB = Wedge((-11.5 - 10 - postarg[0], -0 - postarg[1]), 60, -1.5, 1.5, color='dodgerblue') # position, radius, theta1, theta2
ax_cart.add_patch(wedgeB)
wedgeA1 = Wedge((3.76 - postarg[0],-25-postarg[1]), 60, 88.5, 91.5, color='dodgerblue') # position, radius, theta1, theta2
ax_cart.add_patch(wedgeA1)
# add edges of detector
bottom = plt.Rectangle((-11.5 - postarg[0], -14.89 -5 - postarg[1]), 50, 5, fc='dodgerblue') #tuple bottom lefthand corner, width, height
top = plt.Rectangle((-11.5 - postarg[0], 35.1 - postarg[1]), 50, 5, fc='dodgerblue') #tuple bottom lefthand corner, width, height
left = plt.Rectangle((-11.5 - 5 - postarg[0], -14.89 - postarg[1]), 5, 50, fc='dodgerblue') #tuple bottom lefthand corner, width, height
right = plt.Rectangle((38.5 - postarg[0], -14.89 - postarg[1]), 5, 50, fc='dodgerblue') #tuple bottom lefthand corner, width, height
ax_cart.add_patch(bottom)
ax_cart.add_patch(left)
ax_cart.add_patch(right)
ax_cart.add_patch(top)
elif aperture == 'WEDGEB2.0':
# wedge needs to start 59% from the detector left edge
wedgeB = Wedge((-29.66 - 10 - postarg[0], -0 - postarg[1]), 60, -1.5, 1.5, color='dodgerblue') # position, radius, theta1, theta2
ax_cart.add_patch(wedgeB)
wedgeA1 = Wedge((-14.83 - postarg[0],-20-postarg[1]), 60, 88.5, 91.5, color='dodgerblue') # position, radius, theta1, theta2
ax_cart.add_patch(wedgeA1)
# add edges of detector
bottom = plt.Rectangle((-29.66 - postarg[0], -10.7 -5 - postarg[1]), 50, 5, fc='dodgerblue') #tuple bottom lefthand corner, width, height
top = plt.Rectangle((-29.66 - postarg[0], 39.4 - postarg[1]), 50, 5, fc='dodgerblue') #tuple bottom lefthand corner, width, height
left = plt.Rectangle((-29.66 - 5 - postarg[0], -10.7 - postarg[1]), 5, 50, fc='dodgerblue') #tuple bottom lefthand corner, width, height
right = plt.Rectangle((20.25 - postarg[0], -10.7 - postarg[1]), 5, 50, fc='dodgerblue') #tuple bottom lefthand corner, width, height
ax_cart.add_patch(bottom)
ax_cart.add_patch(left)
ax_cart.add_patch(right)
ax_cart.add_patch(top)
elif aperture == 'WEDGEB2.5':
# wedge needs to start 78.5% from the detector left edge
wedgeB = Wedge((-39.27 - 10 - postarg[0], -0 - postarg[1]), 60, -1.5, 1.5, color='dodgerblue') # position, radius, theta1, theta2
ax_cart.add_patch(wedgeB)
wedgeA1 = Wedge((-24.71-postarg[0],-20-postarg[1]), 60, 88.5, 91.5, color='dodgerblue') # position, radius, theta1, theta2
ax_cart.add_patch(wedgeA1)
# add edges of detector
bottom = plt.Rectangle((-39.27 - postarg[0], -10.7 -5 - postarg[1]), 50, 5, fc='dodgerblue') #tuple bottom lefthand corner, width, height
top = plt.Rectangle((-39.27 - postarg[0], 39.4 - postarg[1]), 50, 5, fc='dodgerblue') #tuple bottom lefthand corner, width, height
left = plt.Rectangle((-39.27 - 5 - postarg[0], -10.7 - postarg[1]), 5, 50, fc='dodgerblue') #tuple bottom lefthand corner, width, height
right = plt.Rectangle((10.75 - postarg[0], -10.7 - postarg[1]), 5, 50, fc='dodgerblue') #tuple bottom lefthand corner, width, height
ax_cart.add_patch(bottom)
ax_cart.add_patch(left)
ax_cart.add_patch(right)
ax_cart.add_patch(top)
else:
raise NameError(f"Aperture {aperture} unknown (or yet unsupported).")
# plot some text providing the value of the ORIENT and aperture
ax_cart.text(0.7*(fov/2), 0.9*(fov/2), f'ORIENT = {orient}' + r'$^{\circ}$', fontsize=16)
ax_cart.text(0.7*(fov/2), 0.8*(fov/2), f'{aperture}', fontsize=16)
# add the polar plot
ax_polar.plot(0, 0, color='goldenrod', marker='*',mec='k',markersize=60)
max_radius = fov/2
# Plot the various vectors
orient = orient
avoid = avoidspikes
u2 = orient + 90
neg_u3 = orient + 180
neg_u2 = orient + 270
# U3
ax_polar.quiver(orient*np.pi/180.,0,0,1, color='goldenrod', angles="xy", scale_units='xy', scale=1/max_radius)
ax_polar.fill_between(np.linspace((orient-avoid)*np.pi/180., (orient+avoid)*np.pi/180., 100), 0, max_radius, color='red', alpha=0.2)
# U2
ax_polar.quiver((u2)*np.pi/180.,0,0,1, color='goldenrod', alpha = 0.4, angles="xy", scale_units='xy', scale=1/max_radius)
ax_polar.fill_between(np.linspace((u2-avoid)*np.pi/180., (u2+avoid)*np.pi/180., 100), 0, max_radius, color='red', alpha=0.2)
# -U3
ax_polar.quiver((neg_u3)*np.pi/180.,0,0,1, color='goldenrod', alpha = 0.3, angles="xy", scale_units='xy', scale=1/max_radius)
ax_polar.fill_between(np.linspace((neg_u3-avoid)*np.pi/180., (neg_u3+avoid)*np.pi/180., 100), 0, max_radius, color='red', alpha=0.2)
# -U2
ax_polar.quiver((neg_u2)*np.pi/180.,0,0,1, color='goldenrod', alpha = 0.3, angles="xy", scale_units='xy', scale=1/max_radius)
ax_polar.fill_between(np.linspace((neg_u2-avoid)*np.pi/180., (neg_u2+avoid)*np.pi/180., 100), 0, max_radius, color='red', alpha=0.2)
if feature == 'companion':
if 'featuresep' not in kwargs:
raise NameError("Separation of companion not specified.")
planet_pa = featurepa
planet_sep = kwargs['featuresep']
ax_polar.plot(planet_pa*np.pi/180, planet_sep, color='magenta', marker='o', mec='black', mew=2, markersize=12)
elif feature == 'disk':
if ('disk_width' not in kwargs) or ('disk_height' not in kwargs):
raise NameError("Disk width and/or height not specified.")
el = Ellipse((0,0), kwargs['disk_width'], kwargs['disk_height'], featurepa+90, fill=0, fc=None, lw=5, ec='magenta')
ax_cart.add_patch(el)
else:
raise NameError("Feature type not recognized (must be 'companion' or 'disk').")
# Plot vector showing true north
ax_polar.quiver(0.,0,0,1, color='lime', linestyle=':', angles="xy", scale_units='xy', scale=1/max_radius)
# Set true north direction on polar plot and size
ax_polar.set_theta_zero_location("N", offset=-(orient-45))
ax_polar.set_rmax(max_radius)
ax_polar.set_rlabel_position((orient-45)+90) # set to be due east
plt.show()
Note that at this ORIENT angle, the companion would be visible and unaffected by scattered light from the diffraction spikes. This configuration also avoids accidental placement behind the occulter.
show_orients(orients = 195, aperture = 'BAR10', fov = 10, feature = 'companion', featurepa = 45, featuresep=1.75)
Here, the configuration is virtually identical to Example #1 in terms of companion properties and occulter selection, but the ORIENT angle of the observatory has been changed from 195º in the previous example to 33º.
At this ORIENT angle, the companion would not only lie within the diffraction spike avoidance region (red shaded area), but would also be partially obscured by the BAR10 occulter itself.
show_orients(orients = 33, aperture = 'BAR10', fov = 10, feature = 'companion', featurepa = 45, featuresep=1.75)
This example shows a simple disk feature, where the central star has been placed behind the WEDGEA1.0 occulter. The selected parameters are as follows:
show_orients(orients = 45, aperture = 'WEDGEA1.0', fov = 15, feature = 'disk', featurepa = 300, disk_width=10, disk_height = 3)