"""
Provides class and functions to interact with FOBOS aperture deployment.
.. include:: ../include/links.rst
"""
import time
from itertools import chain, combinations
from IPython import embed
import numpy
from scipy.optimize import linear_sum_assignment
from matplotlib import pyplot, patches
from sklearn.neighbors import KDTree
from astropy import units
from astropy.coordinates import SkyCoord, SkyOffsetFrame
from . import data_file
from .collisions import remove_collisions
from .data.bitmask import BitMask
[docs]class FOBOSModeBitMask(BitMask):
def __init__(self):
mask_bits = dict(MOS='Single-fiber, multi-object spectroscopy mode',
IFU='Multiplexed IFU mode',
MONO='Monolithic IFU mode')
super().__init__(list(mask_bits.keys()), descr=list(mask_bits.values()))
[docs] def validate(self, mode):
_mode = numpy.atleast_1d(mode)
valid_modes = [0] + (numpy.array(list(self.bits.values()))+1).tolist()
if any([m not in valid_modes for m in _mode]):
raise ValueError('Modes invalide. Must be 0 (off), 1 (single-fiber), 2 (multi-IFU), '
'or 3 (monolithic IFU).')
[docs]class FOBOSApertureBitMask(BitMask):
def __init__(self):
mask_bits = dict(SCIENCE='Science aperture',
SKY='Designated sky fiber',
GUIDE='Tracking and focus-offset imaging bundle',
CALIB='Flux-calibration bundle')
super().__init__(list(mask_bits.keys()), descr=list(mask_bits.values()))
[docs]class FOBOSApertures:
module_src = data_file('deploy/fobos_modules.db')
"""
File providing the centers of all FOBOS starbug modules. This is currently
never used.
"""
starbug_src = data_file('deploy/fobos_starbugs.db')
"""
Main file with the distribution, types, etc of the FOBOS apertures.
"""
version = '0.1'
"""
Version of the aperture deployment design.
"""
mode_bm = FOBOSModeBitMask()
"""
Mode selection bit mask
"""
ap_bm = FOBOSApertureBitMask()
"""
Aperture selection bit mask
"""
fov = 20./60.
"""
Diameter of the field-of-view in decimal degrees.
"""
def __init__(self, mode=1, baseline=True, config=1):
"""
Args:
mode (:obj:`int`, array-like, optional):
The mode assignment for each spectrograph. Must be 1 for the
single-fiber apertures, 2 for multi-IFU mode, or 3 for
monolithic IFU mode. If a single integer, all spectrographs are
put in the same mode; otherwise, must provide the mode for each
of the 3 spectrographs separately.
baseline (:obj:`bool`, optional):
Flag to only return the apertures in the CoDR baseline
configuration.
config (:obj:`int`, optional):
Configuration selection for mapping aperture modules to each
spectrograph. Must be either: (1) modules mapped to each
spectrograph are spread over the entire focal plane; or (2)
modules mapped to each spectrograph fill coherent focal-plane
zones.
"""
# TODO: Check version?
_data = numpy.genfromtxt(str(self.starbug_src), dtype=str)
self.bid = _data[:,0].astype(int)
self.mid = _data[:,1].astype(int)
self.nap = self.bid.size
self.id = numpy.arange(self.nap, dtype=int)
self.id_name = numpy.array([f'{m}-{b}' for m, b in zip(self.mid, self.bid)])
self.spc = None
self.on = None
self.sky = None
self._spc1 = _data[:,2].astype(int)
self._on1 = _data[:,3].astype(int)
self._sky1 = _data[:,4].astype(int)
self._spc2 = _data[:,5].astype(int)
self._on2 = _data[:,6].astype(int)
self._sky2 = _data[:,7].astype(int)
self.coo = _data[:,8:10].astype(float)
self.in_baseline = _data[:,11].astype(int).astype(bool)
self.payload = _data[:,10].astype(int)
# Initialize the aperture types based only on the payload type.
self.type = numpy.zeros(self.nap, dtype=self.ap_bm.minimum_dtype(asuint=True))
indx = numpy.isin(self.payload, [0,1,2])
self.type[indx] = self.ap_bm.turn_on(self.type[indx], 'SCIENCE')
indx = self.payload == 3
self.type[indx] = self.ap_bm.turn_on(self.type[indx], 'GUIDE')
indx = self.payload == 4
self.type[indx] = self.ap_bm.turn_on(self.type[indx], 'CALIB')
self.mode = None
self.baseline = None
self.config = None
self.active = None
self.configure(mode=mode, baseline=baseline, config=config)
[docs] @staticmethod
def parse_mode(mode):
"""
Parse and validate the provided FOBOS spectrograph mode.
Args:
mode (:obj:`int`, array-like):
The mode assignment for each spectrograph. Must be 0 to turn
off all apertures, 1 to select single-fiber mode, 2 for
multi-IFU mode, or 3 for monolithic IFU mode. If a single
integer, all spectrographs are put in the same mode; otherwise,
must provide the mode for each of the 3 spectrographs
separately.
Returns:
`numpy.ndarray`_: A 3-element array with the integer mode identifier
for each of the three spectrographs.
Raises:
ValueError:
Raised if the mode number is not understood, or if the mode is
not provided as a single value or one for each spectrograph.
"""
# Check the mode input
_mode = numpy.atleast_1d(mode).astype(int)
if _mode.size == 1:
_mode = numpy.repeat(_mode, 3)
if _mode.size != 3:
raise ValueError(f'Mode not understood: {mode}. Must enter single mode for all '
f'spectrographs, or the mode for each of the three spectrographs.')
# Check the modes are valid
FOBOSApertures.mode_bm.validate(_mode)
return _mode
[docs] def select(self, payload):
r"""
Construct a boolean array that selects specific payload types.
Type must be:
- ``'science'`` for science apertures,
- ``'sky'`` for designated sky apertures,
- ``'guide'`` for imaging guide bundles, or
- ``'calib'`` for flux-calibration bundles.
Note that in MOS mode (mode=1), the only sky fibers are the ones
designated for the always-ready IFU in spectrograph 1.
Args:
payload (:obj:`str`):
The payload selection.
return_id (:obj:`bool`, optional):
Return the running (0-indexed) number for the aperture.
Returns:
`numpy.ndarray`_: A boolean array used to select the viable
apertures of the requested type.
"""
valid = list(self.ap_bm.bits.keys())
if payload.upper() not in valid:
raise ValueError(f"Unknown payload type: {payload.upper()}. Options are: "
f"{', '.join(valid)}")
return self.active & self.ap_bm.flagged(self.type, payload.upper())
[docs] def show(self, include_patrol=False, by_spec=False, legend=True):
"""
Make a plot of the currently active apertures.
Args:
include_patrol (:obj:`bool`, optional):
Use a light shaded region to show the patrol region of every
aperture.
by_spec (:obj:`bool`, optional):
Instead of coloring the aperture locations by their payload
type, color them by their spectrograph mapping.
legend (:obj:`bool`, optional):
Include the plot legend
"""
w,h = pyplot.figaspect(1)
fig = pyplot.figure(figsize=(1.5*w,1.5*h))
ax = fig.add_axes([0.1, 0.1, 0.8, 0.8])
ax.set_xlim([-14, 14])
ax.set_ylim([-14, 14])
ax.minorticks_on()
ax.grid(True, which='major', color='0.9', zorder=0, linestyle='-')
ax.tick_params(which='major', direction='in', length=8, top=True, right=True)
ax.tick_params(which='minor', direction='in', length=4, top=True, right=True)
# Single fibers
indx = self.active & (self.payload == 0)
if by_spec:
_indx = indx & (self.spc == 1)
ax.scatter(self.coo[_indx,0], self.coo[_indx,1],
marker='.', s=40, lw=0, color='C0', zorder=5, label='Single Fiber (Spec 1)')
_indx = indx & (self.spc == 2)
ax.scatter(self.coo[_indx,0], self.coo[_indx,1],
marker='.', s=40, lw=0, color='C2', zorder=5, label='Single Fiber (Spec 2)')
_indx = indx & (self.spc == 3)
ax.scatter(self.coo[_indx,0], self.coo[_indx,1],
marker='.', s=40, lw=0, color='C4', zorder=5, label='Single Fiber (Spec 3)')
else:
ax.scatter(self.coo[indx,0], self.coo[indx,1],
marker='.', s=40, lw=0, color='C1', zorder=5, label='Single Fiber')
# IFUs
indx = self.active & (self.payload == 1)
if by_spec:
_indx = indx & (self.spc == 1)
ax.scatter(self.coo[_indx,0], self.coo[_indx,1],
marker='s', s=20, lw=0.5, color='C0',zorder=5, label='IFU (Spec 1)')
_indx = indx & (self.spc == 2)
ax.scatter(self.coo[_indx,0], self.coo[_indx,1],
marker='s', s=20, lw=0.5, color='C2',zorder=5, label='IFU (Spec 2)')
_indx = indx & (self.spc == 3)
ax.scatter(self.coo[_indx,0], self.coo[_indx,1],
marker='s', s=20, lw=0.5, color='C4',zorder=5, label='IFU (Spec 3)')
else:
ax.scatter(self.coo[indx,0], self.coo[indx,1],
marker='s', s=20, lw=0.5, color='C2',zorder=5, label='IFU')
# Guide bundles
indx = self.active & (self.payload == 3)
ax.scatter(self.coo[indx,0], self.coo[indx,1],
marker='o', s=20, lw=0.5, color='C3', zorder=5, label='Guide Bundle')
# Flux-calibration bundles
indx = self.active & (self.payload == 4)
if by_spec:
_indx = indx & (self.spc == 1)
ax.scatter(self.coo[_indx,0], self.coo[_indx,1],
marker='x', s=20, lw=1, color='C0', zorder=5, label='Calib. Bundle (Spec 1)')
_indx = indx & (self.spc == 2)
ax.scatter(self.coo[_indx,0], self.coo[_indx,1],
marker='x', s=20, lw=1, color='C2', zorder=5, label='Calib. Bundle (Spec 1)')
_indx = indx & (self.spc == 3)
ax.scatter(self.coo[_indx,0], self.coo[_indx,1],
marker='x', s=20, lw=1, color='C4', zorder=5, label='Calib. Bundle (Spec 1)')
else:
ax.scatter(self.coo[indx,0], self.coo[indx,1],
marker='x', s=20, lw=1, color='C4', zorder=5, label='Calib. Bundle')
# Include the patrol region (this in combination with the legend can
# make the plot very slow)
if include_patrol:
indx = self.active & numpy.isin(self.payload, [0,1,3,4])
for x, y in self.coo[indx]:
ax.add_patch(patches.Circle((x,y), radius=138/60., facecolor='k',
edgecolor='none', zorder=3, alpha=0.05))
# FOBOS field-of-view
ax.add_patch(patches.Circle((0.,0.), radius=10., facecolor='none', edgecolor='C3',
zorder=4, label='FOBOS FOV'))
if legend:
ax.legend()
ax.set_xlabel(r'$\xi$ [arcmin]')
ax.set_ylabel(r'$\eta$ [arcmin]')
pyplot.show()