"""
This module contains a class for translating data produced by ESO's NAOMI system.
"""
import importlib.resources
import warnings
import datetime
from pathlib import Path
import numpy as np
from astropy.io import fits
import aotpy
from .eso import ESOTranslator
# TODO set image units
[docs]
class NAOMITranslator(ESOTranslator):
"""Contains functions for translating telemetry data produced by ESO's NAOMI system.
Parameters
----------
path
Path to folder containing telemetry data.
at_number : {1, 2, 3, 4}
Number of the AT that produced the data.
"""
def __init__(self, path: str, at_number: int):
path = Path(path)
self._at_number = at_number
with fits.open(path / 'NAOMI_LOOP_0001.fits', extname='LoopFrame') as hdus:
main_hdr = hdus[0].header
main_loop_frame: fits.FITS_rec = hdus['LoopFrame'].data
self.system = aotpy.AOSystem(ao_mode='SCAO', name='NAOMI')
self.system.main_telescope = aotpy.MainTelescope(
uid=f'ESO VLT AT{at_number}',
elevation=main_hdr['ESO TEL ALT'],
azimuth=self._azimuth_conversion(main_hdr['ESO TEL AZ']),
parallactic=main_hdr['ESO TEL PRLTIC'],
enclosing_diameter=1.82,
inscribed_diameter=1.82
)
naomi_data_path = importlib.resources.files('aotpy.data.NAOMI')
with importlib.resources.as_file(naomi_data_path / 'zernike_control_modes.fits') as p:
# Load file with the representation of the modes controlled in NAOMI (Zernike modes 2 to 15)
control_modes = aotpy.Image('CONTROL MODES', fits.getdata(p))
if main_hdr['ESO AOS CM MODES CONTROLLED'] != control_modes.data.shape[0]:
warnings.warn("Keyword 'ESO AOS CM MODES CONTROLLED' does not match expected number of control modes.")
ngs = aotpy.NaturalGuideStar(uid='NGS',
right_ascension=main_hdr['RA'],
declination=main_hdr['DEC'])
main_timestamps = main_loop_frame['Seconds'] + main_loop_frame['USeconds'] / 1.e6
self.system.date_beginning = datetime.datetime.fromtimestamp(main_timestamps[0], datetime.UTC)
self.system.date_end = datetime.datetime.fromtimestamp(main_timestamps[-1], datetime.UTC)
main_frame_numbers = main_loop_frame['FrameCounter']
loop_time = aotpy.Time('Loop Time', timestamps=main_timestamps.tolist(),
frame_numbers=main_frame_numbers.tolist())
gradients = self._stack_slopes(main_loop_frame['Gradients'], subap_axis=1)
reference = self._stack_slopes(fits.getdata(path / 'Acq.DET1.REFSLP_WITH_OFFSETS_0001.fits'), subap_axis=1)[0]
wfs = aotpy.ShackHartmann(
uid='WFS',
source=ngs,
n_valid_subapertures=12,
measurements=aotpy.Image('Gradients', gradients, time=loop_time),
ref_measurements=aotpy.Image('Acq.DET1.REFSLP_WITH_OFFSETS', reference),
subaperture_intensities=aotpy.Image(f'Intensities', main_loop_frame['Intensities'], time=loop_time)
)
wfs.non_common_path_aberration = aotpy.Aberration(
uid='NCPA',
modes=control_modes,
coefficients=self._image_from_eso_file(path / 'Ctr.MODAL_OFFSETS_ROTATED_0001.fits') # in DM modal space
)
wfs.detector = aotpy.Detector(
uid='DET',
weight_map=self._image_from_eso_file(path / 'Acq.DET1.WEIGHT_0001.fits'),
dark=self._image_from_eso_file(path / 'Acq.DET1.DARK_0001.fits'),
flat_field=self._image_from_eso_file(path / 'Acq.DET1.FLAT_0001.fits'),
bad_pixel_map=self._image_from_eso_file(path / 'Acq.DET1.DEAD_0001.fits'),
sky_background=self._image_from_eso_file(path / 'Acq.DET1.BACKGROUND_0001.fits')
)
pix_loop_frame = fits.getdata(path / 'NAOMI_PIXELS_0001.fits')
wfs.detector.pixel_intensities = aotpy.Image(
'Pixels',
data=self._get_pixel_data_from_table(pix_loop_frame),
time=aotpy.Time('Pixel time', frame_numbers=pix_loop_frame['FrameCounter'].tolist())
)
dm = aotpy.DeformableMirror('DM', telescope=self.system.main_telescope, n_valid_actuators=241)
modal_coefficients = main_loop_frame['ModalCoefficients']
modal_coefficients += fits.getdata(path / 'Ctr.MODAL_OFFSETS_ROTATED_0001.fits') * 2
# These are saved in the DM modal space. Need to add the rotated offsets to get the real coefficients that are
# then sent to the DM after M2DM conversion.
s2m = self._stack_slopes(fits.getdata(path / 'Recn.REC1.CM_0001.fits'), subap_axis=1)
# The S2M matrix is already rotated to to DM modes
m2s = self._stack_slopes(fits.getdata(path / 'ModalRecnCalibrat.REF_IM_0001.fits'), subap_axis=0)
try:
ref_commands = aotpy.Image('Ctr.ACT_POS_REF_MAP', fits.getdata(path / 'Ctr.ACT_POS_REF_MAP_0001.fits')[0])
except FileNotFoundError:
ref_commands = None
warnings.warn("Reference commands file not found ('Ctr.ACT_POS_REF_MAP_0001.fits').")
loop = aotpy.ControlLoop(
uid='Main Loop',
input_sensor=wfs,
commanded_corrector=dm,
time=loop_time,
time_filter_num=aotpy.Image('Ctr.TERM_A', fits.getdata(path / 'Ctr.TERM_A_0001.fits')),
time_filter_den=aotpy.Image('Ctr.TERM_B', fits.getdata(path / 'Ctr.TERM_B_0001.fits')),
commands=aotpy.Image('DM positions', main_loop_frame['Positions'], time=loop_time),
ref_commands=ref_commands,
modes=control_modes,
modal_coefficients=aotpy.Image('Modal Coefficients', modal_coefficients, time=loop_time),
measurements_to_modes=aotpy.Image('Recn.REC1.CM', s2m),
modes_to_commands=self._image_from_eso_file(path / 'RTC.M2DM_SCALED_0001.fits'),
commands_to_modes=self._image_from_eso_file(path / 'RTC.DM2M_SCALED_0001.fits'),
modes_to_measurements=aotpy.Image('ModalRecnCalibrat.REF_IM', m2s),
closed=main_hdr['ESO AOS LOOP ST'],
framerate=main_hdr['ESO AOS LOOP RATE']
)
asm = aotpy.AtmosphericParameters(
'ESO ASM (Astronomical Site Monitor)',
wavelength=500e-9,
seeing=[main_hdr['ESO TEL AMBI FWHM']],
tau0=[main_hdr['ESO TEL AMBI TAU0']],
theta0=[main_hdr['ESO TEL AMBI THETA0']],
layers_wind_direction=aotpy.Image('ESO TEL AMBI WINDDIR', np.array([[main_hdr['ESO TEL AMBI WINDDIR']]])),
layers_wind_speed=aotpy.Image('ESO TEL AMBI WINDSP', np.array([[main_hdr['ESO TEL AMBI WINDSP']]]))
)
self.system.sources = [ngs]
self.system.wavefront_sensors = [wfs]
self.system.wavefront_correctors = [dm]
self.system.loops = [loop]
self.system.atmosphere_params = [asm]
def _get_eso_telescope_name(self) -> str:
# Allow for both:
# ESO-VLTI-Amnop
# ESO-VLTI-Uijkl-Amnop
# as long as mnop contains the correct number
return f"ESO-VLTI-%A%{self._at_number}%"
def _get_eso_ao_name(self) -> str:
return 'NAOMI'
def _get_run_id(self) -> str:
return '60.A-9278(D)'
def _get_chip_id(self) -> str:
return f'NAOMI{self._at_number}'