plato-fits 2024.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,1221 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import warnings
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from astropy.io import fits
|
|
7
|
+
from astropy.io.ascii.cparser import AstropyWarning
|
|
8
|
+
from egse.settings import Settings
|
|
9
|
+
from egse.setup import Setup
|
|
10
|
+
from egse.setup import SetupError
|
|
11
|
+
from egse.spw import DataPacket
|
|
12
|
+
from egse.spw import DataPacketType
|
|
13
|
+
from egse.state import GlobalState
|
|
14
|
+
from egse.persistence import PersistenceLayer
|
|
15
|
+
from egse.system import time_since_epoch_1958
|
|
16
|
+
|
|
17
|
+
LOGGER = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
CCD_SETTINGS = Settings.load("CCD")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FITSPersistenceError(Exception):
|
|
23
|
+
""" Error for the FITS persistence layer."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FITS(PersistenceLayer):
|
|
28
|
+
""" Persistence layer that saves (image) data in a FITS file."""
|
|
29
|
+
|
|
30
|
+
extension = "fits"
|
|
31
|
+
|
|
32
|
+
warnings.simplefilter('ignore', category=AstropyWarning)
|
|
33
|
+
|
|
34
|
+
def __init__(self, filename: str, prep: dict):
|
|
35
|
+
""" Initialisation of the FITS persistence layer.
|
|
36
|
+
|
|
37
|
+
This consists of the following steps:
|
|
38
|
+
|
|
39
|
+
- Initialise the filepath (for the given filename);
|
|
40
|
+
- Fetch the register map from the DPU;
|
|
41
|
+
- Read all necessary data from that register map:
|
|
42
|
+
- Which CCD(s) will be read;
|
|
43
|
+
- Which side(s) of the CCD(s) will be read (E = left; F = right);
|
|
44
|
+
- Which rows and columns will be transmitted;
|
|
45
|
+
- How can we know whether all information has been received for a particular exposure;
|
|
46
|
+
- Initialisation of the (1D) arrays in which the received data will be stored;
|
|
47
|
+
- Initialisation of basic headers for the image, serial pre-scan(s), serial over-scan(s), and parallel
|
|
48
|
+
over-scan for any exposure.
|
|
49
|
+
|
|
50
|
+
Assumed is that the crucial parameters in the register map (N-FEE mode, v_start, v_end, h_end) will stay the
|
|
51
|
+
same throughout data acquisition and assembly of the FITS file. It is not explicitly checked whether this is
|
|
52
|
+
indeed the case.
|
|
53
|
+
|
|
54
|
+
All information (image data, serial pre-scan, serial over-scan, and parallel over-scan) will be stored in the
|
|
55
|
+
same FITS file. If the data of multiple CCDs would be transmitted, it is all stored in the same FITS file. The
|
|
56
|
+
extension name will indicate which kind of data it contains (image data, serial pre-scan, serial over-scan, or
|
|
57
|
+
parallel over-scan), for which CCD. In case of the serial scan maps, the extension will also indicate which
|
|
58
|
+
CCD side it applies to.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
- filename: Name of the output FITS file.
|
|
62
|
+
- prep: Dictionary with the following information:
|
|
63
|
+
* v_start (int) and v_end(int): index of the first and the last row being transmitted;
|
|
64
|
+
* h_end (int): index of the last serial readout of the readout register;
|
|
65
|
+
* rows_final_dump:
|
|
66
|
+
* ccd_mode_config (egse.fee.n_fee_mode): readout mode;
|
|
67
|
+
* ccd_readout_order (List[int]): CCDs that will be read out;
|
|
68
|
+
* expected_last_packet_flags (List[bool]): expected last packet flags;
|
|
69
|
+
* obsid: observation identifier;
|
|
70
|
+
* cycle_time: image cycle time [s];
|
|
71
|
+
* cgse_version: version of the Common EGSE;
|
|
72
|
+
* setup: setup;
|
|
73
|
+
* register_map: FEE register map;
|
|
74
|
+
* vgd: configured VGD;
|
|
75
|
+
* in case of windowing mode or windowing pattern mode: dictionary with one window list for each CCD.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
self.fee_side = GlobalState.setup.camera.fee.ccd_sides.enum
|
|
79
|
+
|
|
80
|
+
self._filepath = Path(filename)
|
|
81
|
+
|
|
82
|
+
# Which side(s) of which CCD(s) will be transmitted?
|
|
83
|
+
# (initialise the data arrays for each of these)
|
|
84
|
+
|
|
85
|
+
self.ccd_readout_order = prep["ccd_readout_order"] # CCD numbering [1-4]
|
|
86
|
+
self.selected_ccds = np.unique(self.ccd_readout_order)
|
|
87
|
+
LOGGER.debug(f"Selected CCDs: {self.selected_ccds}")
|
|
88
|
+
|
|
89
|
+
self.data_arrays = {}
|
|
90
|
+
self.init_data_arrays()
|
|
91
|
+
|
|
92
|
+
self.frame_number = {ccd: {self.fee_side.LEFT_SIDE: 0, self.fee_side.RIGHT_SIDE: 0} for ccd in self.selected_ccds}
|
|
93
|
+
self.timestamp = None
|
|
94
|
+
self.finetime = None
|
|
95
|
+
|
|
96
|
+
# How can you know whether or not all data for a given CCD has been received?
|
|
97
|
+
# (this depends on whether or not there is parallel over-scan data and on which CCD side(s) will be transmitted)
|
|
98
|
+
|
|
99
|
+
self.expected_last_packet_flags = prep["expected_last_packet_flags"] # To be received
|
|
100
|
+
self.received_last_packet_flags = {ccd: [False] * 4 for ccd in self.selected_ccds} # Actually received
|
|
101
|
+
|
|
102
|
+
LOGGER.debug(f"Init last packets flag: {self.received_last_packet_flags}")
|
|
103
|
+
|
|
104
|
+
# Readout mode
|
|
105
|
+
|
|
106
|
+
self.ccd_mode_config = prep["ccd_mode_config"]
|
|
107
|
+
self.is_windowing_mode = self.check_readout_mode()
|
|
108
|
+
|
|
109
|
+
# if self.is_windowing_mode:
|
|
110
|
+
#
|
|
111
|
+
# self.windows = self.init_window_list(prep["window_list"])
|
|
112
|
+
|
|
113
|
+
# Define the values of the WCS keywords
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
self.v_start = prep["v_start"] # First transmitted row
|
|
117
|
+
self.v_end = prep["v_end"] # Last transmitted row
|
|
118
|
+
self.h_end = prep["h_end"] # Last transmitted column
|
|
119
|
+
self.rows_final_dump = prep["rows_final_dump"] # Number of rows to be dumped after readout
|
|
120
|
+
self.cycle_time = prep["cycle_time"] # Image cycle time [s]
|
|
121
|
+
self.cgse_version = prep["cgse_version"] # Version of the Common EGSE
|
|
122
|
+
self.obsid = prep["obsid"]
|
|
123
|
+
|
|
124
|
+
self.register_map = prep["register_map"] # Register map
|
|
125
|
+
|
|
126
|
+
# Read information from the setup
|
|
127
|
+
|
|
128
|
+
self.setup: Setup = prep["setup"]
|
|
129
|
+
|
|
130
|
+
self.site_name = self.setup["site_id"] # Site ID
|
|
131
|
+
self.setup_id = self.setup.get_id() # Setup ID
|
|
132
|
+
self.camera_id = self.setup.camera.get("ID") # Camera ID (None if not present in the setup)
|
|
133
|
+
|
|
134
|
+
self.readout_time = self.calculate_readout_time(self.setup) # Readout time [s]
|
|
135
|
+
try:
|
|
136
|
+
self.exposure_time = self.cycle_time - self.readout_time # Exposure time [s]
|
|
137
|
+
except TypeError:
|
|
138
|
+
# Image cycle time is unknown (None)
|
|
139
|
+
self.exposure_time = None
|
|
140
|
+
|
|
141
|
+
self.has_serial_overscan = self.h_end >= CCD_SETTINGS.LENGTH_SERIAL_PRESCAN + CCD_SETTINGS.NUM_COLUMNS / 2
|
|
142
|
+
self.has_parallel_overscan = self.v_end >= CCD_SETTINGS.NUM_ROWS
|
|
143
|
+
|
|
144
|
+
# Create basic WCS
|
|
145
|
+
# (part of the information can only be determined when the data is assembled)
|
|
146
|
+
|
|
147
|
+
self.image_header = self.create_base_image_wcs()
|
|
148
|
+
self.serial_prescan_header = self.create_base_serial_prescan_wcs()
|
|
149
|
+
self.serial_overscan_header = self.create_base_serial_overscan_wcs()
|
|
150
|
+
self.parallel_overscan_header = self.create_base_parallel_overscan_wcs()
|
|
151
|
+
|
|
152
|
+
self.is_fits_file_open = False
|
|
153
|
+
|
|
154
|
+
def calculate_readout_time(self, setup: Setup):
|
|
155
|
+
""" Calculate the readout time.
|
|
156
|
+
|
|
157
|
+
The readout time consists of:
|
|
158
|
+
|
|
159
|
+
- clearout for the rows up to v_start;
|
|
160
|
+
- reading (i.e. parallel transfer of the row + serial transfer of all its pixels up to h_end) rows v_start
|
|
161
|
+
to v_end;
|
|
162
|
+
- dumping rows_final_dump.
|
|
163
|
+
|
|
164
|
+
Returns: Readout time for the requested part of a single CCD side [s].
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
time_row_parallel = setup.camera.fee.time_row_parallel
|
|
168
|
+
time_row_clearout = setup.camera.fee.time_row_clearout
|
|
169
|
+
time_pixel_readout = setup.camera.fee.time_pixel_readout
|
|
170
|
+
|
|
171
|
+
time_read_rows = (self.v_end - self.v_start + 1) * (time_row_parallel + (self.h_end + 1) * time_pixel_readout)
|
|
172
|
+
time_dump_rows = (self.v_start + self.rows_final_dump) * time_row_clearout
|
|
173
|
+
|
|
174
|
+
return time_read_rows + time_dump_rows
|
|
175
|
+
|
|
176
|
+
def get_vgd(self):
|
|
177
|
+
""" Extract the VGD voltage from the register map.
|
|
178
|
+
|
|
179
|
+
Return: Configured VGD voltage [V].
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
vgd_19 = self.register_map[('reg_19_config', 'ccd_vgd_config')]
|
|
183
|
+
vgd_20 = self.register_map[('reg_20_config', 'ccd_vgd_config')]
|
|
184
|
+
|
|
185
|
+
return ((vgd_20 << 4) + vgd_19) / 1000 * 5.983
|
|
186
|
+
|
|
187
|
+
def get_ccd2_vrd(self):
|
|
188
|
+
"""
|
|
189
|
+
Extract the VRD voltage for CCD2 from the register map.
|
|
190
|
+
|
|
191
|
+
NOTE: Use this function only for the FM cameras.
|
|
192
|
+
|
|
193
|
+
Return: Configured VRD voltage.
|
|
194
|
+
"""
|
|
195
|
+
vrd_18 = self.register_map[('reg_18_config', 'ccd2_vrd_config')]
|
|
196
|
+
vrd_19 = self.register_map[('reg_19_config', 'ccd2_vrd_config')]
|
|
197
|
+
|
|
198
|
+
return int(f'0x{vrd_19:x}{vrd_18:x}', 16)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def get_ccd3_vrd(self):
|
|
202
|
+
"""
|
|
203
|
+
Extract the VRD voltage for CCD3 from the register map.
|
|
204
|
+
|
|
205
|
+
NOTE: Use this function only for the EM camera.
|
|
206
|
+
|
|
207
|
+
Return: Configured VRD voltage.
|
|
208
|
+
"""
|
|
209
|
+
vrd_18 = self.register_map[('reg_18_config', 'ccd3_vrd_config')]
|
|
210
|
+
vrd_19 = self.register_map[('reg_19_config', 'ccd3_vrd_config')]
|
|
211
|
+
|
|
212
|
+
return int(f'0x{vrd_19:x}{vrd_18:x}', 16)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def init_data_arrays(self):
|
|
216
|
+
""" Initialise data arrays in which the data content of the SpW packets will be dumped.
|
|
217
|
+
|
|
218
|
+
At this point, we have already determined which side(s) of which CCD(s) will be read out. For each of them,
|
|
219
|
+
a placeholder will be foreseen in a dedicated dictionary. The structure of this dictionary is the following,
|
|
220
|
+
in case both sides of all CCDs will be read:
|
|
221
|
+
|
|
222
|
+
data_arrays[fee_side.LEFT_SIDE][1] # left side of CCD1
|
|
223
|
+
data_arrays[fee_side.LEFT_SIDE][2] # left side of CCD2
|
|
224
|
+
data_arrays[fee_side.LEFT_SIDE][3] # left side of CCD3
|
|
225
|
+
data_arrays[fee_side.LEFT_SIDE][4] # left side of CCD4
|
|
226
|
+
data_arrays[fee_side.RIGHT_SIDE][1] # right side of CCD1
|
|
227
|
+
data_arrays[fee_side.RIGHT_SIDE][2] # right side of CCD2
|
|
228
|
+
data_arrays[fee_side.RIGHT_SIDE][3] # right side of CCD3
|
|
229
|
+
data_arrays[fee_side.RIGHT_SIDE][4] # right side of CCD4
|
|
230
|
+
|
|
231
|
+
In case not all CCDs will be read out and/or only one side, placeholders will only be foreseen for the relevant
|
|
232
|
+
CCD data.
|
|
233
|
+
|
|
234
|
+
Returns: Dictionary with placeholders for the data arrays of the selected CCD sides.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
left_side_arrays = {ccd: np.array([], dtype=np.uint16) for ccd in self.selected_ccds}
|
|
238
|
+
self.data_arrays[self.fee_side.LEFT_SIDE] = left_side_arrays
|
|
239
|
+
|
|
240
|
+
right_side_arrays = {ccd: np.array([], dtype=np.uint16) for ccd in self.selected_ccds}
|
|
241
|
+
self.data_arrays[self.fee_side.RIGHT_SIDE] = right_side_arrays
|
|
242
|
+
|
|
243
|
+
def clear_for_next_exposure(self, ccd_number: int, ccd_side):
|
|
244
|
+
""" Indicate that no data has been received yet for the next exposure of the given CCD.
|
|
245
|
+
|
|
246
|
+
At the end of an exposure, when the data have been assembled and written to FITS:
|
|
247
|
+
|
|
248
|
+
- clear the data arrays for the next exposure;
|
|
249
|
+
- indicate that the last packet has not been received yet for the next exposure;
|
|
250
|
+
- clear the timestamp for the next exposure.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
- ccd_number: CCD identifier (1/2/3/4).
|
|
254
|
+
- ccd_side: CCD side from which the last received data originates.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
for ccd_side in self.fee_side:
|
|
258
|
+
self.clear_data_arrays(ccd_number, ccd_side)
|
|
259
|
+
|
|
260
|
+
self.clear_last_packet_received(ccd_number)
|
|
261
|
+
|
|
262
|
+
def clear_data_arrays(self, ccd_number: int, ccd_side):
|
|
263
|
+
""" Clear the data arrays for the given CCD.
|
|
264
|
+
|
|
265
|
+
At the end of an exposure, when the data have been assembled and written to FITS, the data arrays must be
|
|
266
|
+
cleared for the next exposure.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
- ccd_number: CCD identifier (1/2/3/4).
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
self.data_arrays[ccd_side][ccd_number] = np.array([], dtype=np.uint16)
|
|
273
|
+
|
|
274
|
+
def clear_last_packet_received(self, ccd_number: int):
|
|
275
|
+
""" Clear the information about the last packet being received for the given CCD.
|
|
276
|
+
|
|
277
|
+
At the end of an exposure, when the data have been assembled and written to FITS, it must be indicated that the
|
|
278
|
+
last packet has not been received for the next exposure for the given CCD.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
- ccd_number: CCD identifier (1/2/3/4).
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
self.received_last_packet_flags[ccd_number] = [False for el in self.received_last_packet_flags[ccd_number]]
|
|
285
|
+
|
|
286
|
+
# def get_data_array(self, ccd_number: int, ccd_side: fee_side):
|
|
287
|
+
# """ Return the data array for the given side of the given CCD.
|
|
288
|
+
#
|
|
289
|
+
# In this array all SpW data concerning the serial pre-scan, image, serial over-scan, and parallel over-scan
|
|
290
|
+
# will be dumped (in 1D). If the last data packet has been received, the different parts will be extracted and
|
|
291
|
+
# written to FITS.
|
|
292
|
+
#
|
|
293
|
+
# Args:
|
|
294
|
+
# - ccd_number: CCD identifier (1/2/3/4).
|
|
295
|
+
# - ccd_side: CCD side.
|
|
296
|
+
#
|
|
297
|
+
# Returns: Data array for the given side of the given CCD.
|
|
298
|
+
# """
|
|
299
|
+
#
|
|
300
|
+
# return self.data_arrays[ccd_side][ccd_number]
|
|
301
|
+
|
|
302
|
+
def check_readout_mode(self):
|
|
303
|
+
""" For now only checks whether the N-FEE is in the correct mode, i.e. full-image (pattern) mode.
|
|
304
|
+
|
|
305
|
+
In the future, if deemed necessary, windowing (pattern) mode may be implemented as well.
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
if self.ccd_mode_config in [n_fee_mode.FULL_IMAGE_MODE, n_fee_mode.FULL_IMAGE_PATTERN_MODE,
|
|
309
|
+
n_fee_mode.PARALLEL_TRAP_PUMPING_1_MODE, n_fee_mode.PARALLEL_TRAP_PUMPING_2_MODE,
|
|
310
|
+
n_fee_mode.SERIAL_TRAP_PUMPING_1_MODE, n_fee_mode.SERIAL_TRAP_PUMPING_2_MODE]:
|
|
311
|
+
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
if self.ccd_mode_config in [n_fee_mode.WINDOWING_PATTERN_MODE, n_fee_mode.WINDOWING_MODE]:
|
|
315
|
+
|
|
316
|
+
return True
|
|
317
|
+
|
|
318
|
+
else:
|
|
319
|
+
|
|
320
|
+
raise FITSPersistenceError("Construction of FITS files from SpW packets only implemented for full-image "
|
|
321
|
+
"(pattern) mode, windowing (pattern) mode, and parallel/serial trap pumping 1/2 "
|
|
322
|
+
"mode")
|
|
323
|
+
|
|
324
|
+
# def init_window_list(self, window_list: dict):
|
|
325
|
+
# """ Compile the window list.
|
|
326
|
+
#
|
|
327
|
+
# For each of the CCDs that will be read out, execute the following steps:
|
|
328
|
+
#
|
|
329
|
+
# - For each of the CCD sides that will be transmitted, identify which pixels in the compound 2D array (i.e.
|
|
330
|
+
# in which the image and the scan maps are still glued together) will be transmitted. Store their index
|
|
331
|
+
# in the corresponding 1D array in a dictionary.
|
|
332
|
+
# - Store the pixel coordinates of the lower left corner of the window (in the CCD reference frame) of all
|
|
333
|
+
# windows in the image area.
|
|
334
|
+
# """
|
|
335
|
+
#
|
|
336
|
+
# window_indices = {ccd_side: {} for ccd_side in self.selected_ccd_side}
|
|
337
|
+
#
|
|
338
|
+
# for ccd_number in self.selected_ccds:
|
|
339
|
+
#
|
|
340
|
+
# # Get the window information for the current CCD
|
|
341
|
+
#
|
|
342
|
+
# ccd_window_list_obj: WindowList = window_list[ccd_number]
|
|
343
|
+
# ccd_window_list = ccd_window_list_obj.get_window_list() # Ordered set of windows
|
|
344
|
+
# ccd_window_num_columns, ccd_window_num_rows = ccd_window_list_obj.get_window_size() # Window size
|
|
345
|
+
#
|
|
346
|
+
# ccd_window_x_array = np.array([])
|
|
347
|
+
# ccd_window_y_array = np.array([])
|
|
348
|
+
#
|
|
349
|
+
# ccd_window_indices = {ccd_side: np.array([]) for ccd_side in self.selected_ccd_side}
|
|
350
|
+
#
|
|
351
|
+
# for (x_window, y_window, ccd_side_window) in ccd_window_list:
|
|
352
|
+
#
|
|
353
|
+
# # Calculate the index in the 1D array
|
|
354
|
+
#
|
|
355
|
+
# for row in range(y_window, y_window + ccd_window_num_rows + 1):
|
|
356
|
+
#
|
|
357
|
+
# for column in range(x_window, x_window + ccd_window_num_columns + 1):
|
|
358
|
+
#
|
|
359
|
+
# index = row * (self.v_end - self.v_start + 1) + column
|
|
360
|
+
# ccd_window_indices[ccd_side_window] = np.append(ccd_window_indices[ccd_side_window], index)
|
|
361
|
+
#
|
|
362
|
+
# # Store the coordinates of the lower left corner of the windows in the CCD reference frame
|
|
363
|
+
# # (only for the windows in the image area)
|
|
364
|
+
#
|
|
365
|
+
# x_to_append = x_window - CCD_SETTINGS.LENGTH_SERIAL_PRESCAN
|
|
366
|
+
#
|
|
367
|
+
# # TODO Should we check that the window is on the image area?
|
|
368
|
+
#
|
|
369
|
+
# if ccd_side_window == fee_side.F_SIDE:
|
|
370
|
+
#
|
|
371
|
+
# x_to_append = CCD_SETTINGS.NUM_COLUMNS / 2 - 1 - x_to_append
|
|
372
|
+
#
|
|
373
|
+
# if fee_side.E_SIDE in self.selected_ccd_side:
|
|
374
|
+
#
|
|
375
|
+
# x_to_append += CCD_SETTINGS.NUM_COLUMNS / 2
|
|
376
|
+
#
|
|
377
|
+
# ccd_window_x_array = np.append(ccd_window_x_array, x_to_append)
|
|
378
|
+
# ccd_window_y_array = np.append(ccd_window_y_array, y_window)
|
|
379
|
+
#
|
|
380
|
+
# for ccd_side in self.selected_ccd_side:
|
|
381
|
+
#
|
|
382
|
+
# window_indices[ccd_side][ccd_number] = np.sort(np.unique(ccd_window_indices[ccd_side]))
|
|
383
|
+
#
|
|
384
|
+
# rows = fits.Column("Rows", format="I", array=ccd_window_y_array)
|
|
385
|
+
# columns = fits.Column("Columns", format="I", array=ccd_window_x_array)
|
|
386
|
+
# table = fits.BinTableHDU.from_columns([rows, columns])
|
|
387
|
+
# table.header["EXTNAME"] = f"Windows"
|
|
388
|
+
# table.header["EXTVER"] = ccd_number
|
|
389
|
+
# table.header["CCD_ID"] = (ccd_number, "CCD identifier",)
|
|
390
|
+
#
|
|
391
|
+
# # TODO
|
|
392
|
+
#
|
|
393
|
+
# # fits.append(table)
|
|
394
|
+
#
|
|
395
|
+
# # with fits.open(self._filepath, mode="append") as hdul:
|
|
396
|
+
# #
|
|
397
|
+
# # hdul.append(table)
|
|
398
|
+
#
|
|
399
|
+
# return window_indices
|
|
400
|
+
|
|
401
|
+
def create_primary_header(self):
|
|
402
|
+
""" Creates the primary header (i.e. the header of the primary HDU).
|
|
403
|
+
|
|
404
|
+
This contains information that is specific for the camera.
|
|
405
|
+
"""
|
|
406
|
+
|
|
407
|
+
primary_header = fits.PrimaryHDU().header
|
|
408
|
+
|
|
409
|
+
primary_header["LEVEL"] = 1 # Flat structure
|
|
410
|
+
|
|
411
|
+
primary_header["V_START"] = (self.v_start, "Index of 1st row that is transmitted (counting starts at 0)")
|
|
412
|
+
primary_header["V_END"] = (self.v_end, "Index of last row that is transmitted (counting starts at 0)")
|
|
413
|
+
primary_header["H_END"] = (self.h_end, "Number of serial register transfers")
|
|
414
|
+
primary_header["ROWS_FINAL_DUMP"] = (self.rows_final_dump, "Number of rows for clearout after readout")
|
|
415
|
+
primary_header["READ_MODE"] = (n_fee_mode(self.ccd_mode_config).name, "N-FEE operating mode")
|
|
416
|
+
|
|
417
|
+
primary_header["CI_WIDTH"] = (self.register_map["charge_injection_width"],
|
|
418
|
+
"Number of rows in each charge injection region")
|
|
419
|
+
primary_header["CI_GAP"] = (self.register_map["charge_injection_gap"],
|
|
420
|
+
"Number of rows between charge injection regions")
|
|
421
|
+
primary_header["PARALLEL_TOI_PERIOD"] = (self.register_map["parallel_toi_period"],
|
|
422
|
+
"Duration of a parallel overlap period (TOI) [us]")
|
|
423
|
+
primary_header["PARALLEL_CLK_OVERLAP"] = (self.register_map["parallel_clk_overlap"],
|
|
424
|
+
"Extra parallel clock overlap [us]")
|
|
425
|
+
primary_header["CI_EN"] = (self.register_map["charge_injection_width"],
|
|
426
|
+
"Charge injection enabled (1) / disabled (0)")
|
|
427
|
+
primary_header["TRI_LEVEL_CLK_EN"] = (self.register_map["tri_level_clk_en"],
|
|
428
|
+
"Generating bi-level parallel clocks (0) / tri-level parallel clocks (1)")
|
|
429
|
+
primary_header["IMG_CLK_DIR"] = (self.register_map["img_clk_dir"],
|
|
430
|
+
"Generating reverse parallel clocks (1) / normal forward parallel clocks (0)")
|
|
431
|
+
primary_header["REG_CLK_DIR"] = (self.register_map["reg_clk_dir"],
|
|
432
|
+
"Generating reverse serial clocks (1) / normal forward serial clocks (0)")
|
|
433
|
+
primary_header["PACKET_SIZE"] = (self.register_map["packet_size"],
|
|
434
|
+
"Packet size = load bytes + 10 (header bytes)")
|
|
435
|
+
|
|
436
|
+
primary_header["TRAP_PUMP_DWELL_CTR"] = (self.register_map["Trap_Pumping_Dwell_counter"],
|
|
437
|
+
"Dwell timer for trap pumping [ns]")
|
|
438
|
+
primary_header["SENSOR_SEL"] = (self.register_map["sensor_sel"], "CCD port data transmission selection control")
|
|
439
|
+
primary_header["SYNC_SEL"] = (self.register_map["sync_sel"], "Internal (1) / external (0) sync")
|
|
440
|
+
primary_header["DIGITISE_EN"] = (self.register_map["digitise_en"],
|
|
441
|
+
"Digitised data transferred to the N-DPU (1) or not (0) during image mode")
|
|
442
|
+
primary_header["DG_EN"] = (self.register_map["DG_en"], "Dump gate high (1) / low (0)")
|
|
443
|
+
primary_header["CCD_READ_EN"] = (self.register_map["ccd_read_en"], "CCD readout enabled (1) / disabled (0)")
|
|
444
|
+
primary_header["CONV_DLY"] = (self.register_map["conv_dly"],
|
|
445
|
+
"Delay value from rising edge of CCD_R_EF_DRV (where ADC convert pulse "
|
|
446
|
+
"is generated) [ns]")
|
|
447
|
+
primary_header["HIGH_PRECISION_HK_EN"] = (self.register_map["High_precision_HK_en"],
|
|
448
|
+
"Sending high-precision HK (1) / pixel data (0)")
|
|
449
|
+
|
|
450
|
+
primary_header["CCD_VOD"] = (self.register_map["ccd_vod_config"], "Configured VOD")
|
|
451
|
+
|
|
452
|
+
primary_header["CCD1_VRD"] = (self.register_map["ccd1_vrd_config"], "Configured VRD for CCD1")
|
|
453
|
+
if PLATO_CAMERA_IS_EM:
|
|
454
|
+
primary_header["CCD2_VRD"] = (self.register_map["ccd2_vrd_config"], "Configured VRD for CCD2")
|
|
455
|
+
primary_header["CCD3_VRD"] = (self.get_ccd3_vrd(), "Configured VRD for CCD3")
|
|
456
|
+
else:
|
|
457
|
+
primary_header["CCD2_VRD"] = (self.get_ccd2_vrd(), "Configured VRD for CCD2")
|
|
458
|
+
primary_header["CCD3_VRD"] = (self.register_map["ccd3_vrd_config"], "Configured VRD for CCD3")
|
|
459
|
+
|
|
460
|
+
primary_header["CCD4_VRD"] = (self.register_map["ccd4_vrd_config"], "Configured VRD for CCD4")
|
|
461
|
+
primary_header["CCD_VOG"] = (self.register_map["ccd_vog_config"], "Configured VOG")
|
|
462
|
+
primary_header["CCD_VGD"] = (self.get_vgd(), "Configured VGD [V]")
|
|
463
|
+
primary_header["CCD_IG_HI"] = (self.register_map["ccd_ig_hi_config"], "Configured IG-high")
|
|
464
|
+
primary_header["CCD_IG_LO"] = (self.register_map["ccd_ig_lo_config"], "Configured IG-high")
|
|
465
|
+
primary_header["TRK_HLD_HI"] = (self.register_map["trk_hld_hi"], "Track and hold high")
|
|
466
|
+
primary_header["TRK_HLD_LO"] = (self.register_map["trk_hld_lo"], "Track and hold low")
|
|
467
|
+
primary_header["CONT_RST_ON"] = (self.register_map["cont_rst_on"],
|
|
468
|
+
"When 1, FPGA generates continuous reset clock during readout")
|
|
469
|
+
if not PLATO_CAMERA_IS_EM:
|
|
470
|
+
primary_header["CONT_CDSCLP_ON"] = (self.register_map["cont_cdsclp_on"],
|
|
471
|
+
"When 1, FPGA generates continuous CDS clamp during readout")
|
|
472
|
+
primary_header["CONT_ROWCLP_ON"] = (self.register_map["cont_rowclp_on"],
|
|
473
|
+
"When 1, FPGA generates continuous row clamp during readout")
|
|
474
|
+
primary_header["R_CFG1"] = (self.register_map["r_cfg1"], "Clock cycle for Rph3-low, Rph1-high")
|
|
475
|
+
primary_header["R_CFG2"] = (self.register_map["r_cfg2"], "Clock cycle for Rph1-low, Rph2-high")
|
|
476
|
+
primary_header["CDSCLP_LO"] = (self.register_map["cdsclp_lo"], "Clock cycle for cdsclp low")
|
|
477
|
+
if not PLATO_CAMERA_IS_EM:
|
|
478
|
+
primary_header["ADC_PWRDN_EN"] = (self.register_map["adc_pwrdn_en"],
|
|
479
|
+
"ADC power-down enabled (0) / disabled (1)")
|
|
480
|
+
primary_header["CDSCLP_HI"] = (self.register_map["cdsclp_hi"], "Clock cycle for cdsclp high")
|
|
481
|
+
primary_header["ROWCLP_HI"] = (self.register_map["rowclp_hi"], "Clock cycle for rowclp high")
|
|
482
|
+
primary_header["ROWCLP_LO"] = (self.register_map["rowclp_lo"], "Clock cycle for rowclp low")
|
|
483
|
+
# primary_header["SURFACE_INV_CTR"] = (self.register_map["Surface_Inversion_counter"],
|
|
484
|
+
# "Surface inversion counter")
|
|
485
|
+
primary_header["READOUT_PAUSE_CTR"] = (self.register_map["Readout_pause_counter"], "Readout pause counter")
|
|
486
|
+
primary_header["TRAP_PUMP_SHUFFLE_CTR"] = (self.register_map["Trap_Pumping_Shuffle_counter"],
|
|
487
|
+
"Trap pumping shuffle counter")
|
|
488
|
+
|
|
489
|
+
# primary_header["FOCALLEN"] = (FOV_SETTINGS["FOCAL_LENGTH"], "Focal length [mm]")
|
|
490
|
+
|
|
491
|
+
# Additional keywords
|
|
492
|
+
|
|
493
|
+
primary_header["TELESCOP"] = "PLATO"
|
|
494
|
+
if self.camera_id:
|
|
495
|
+
primary_header["INSTRUME"] = (self.camera_id, "Camera ID")
|
|
496
|
+
primary_header["SITENAME"] = (self.site_name, "Name of the test site")
|
|
497
|
+
primary_header["SETUP"] = (self.setup_id, "Setup ID")
|
|
498
|
+
primary_header["CCD_READOUT_ORDER"] = (str(self.ccd_readout_order), "Transmitted CCDs")
|
|
499
|
+
primary_header["CYCLETIME"] = (self.cycle_time, "Image cycle time [s]")
|
|
500
|
+
primary_header["READTIME"] = (self.readout_time,
|
|
501
|
+
"Time needed to read out the requested part for a single CCD side [s]")
|
|
502
|
+
|
|
503
|
+
if self.register_map["sync_sel"] == 1: # See https://github.com/IvS-KULeuven/plato-common-egse/issues/2475
|
|
504
|
+
primary_header["READPERIOD"] = (self.cycle_time + INT_SYNC_TIMING_OFFSET, "Time between frames [s] "
|
|
505
|
+
"(internal sync)")
|
|
506
|
+
texp_cmd = self.cycle_time - self.readout_time
|
|
507
|
+
primary_header["TEXP_CMD"] = (texp_cmd, "Commanded exposure time [s] (internal sync)")
|
|
508
|
+
primary_header["TEXP_EFF"] = (texp_cmd + INT_SYNC_TIMING_OFFSET, "Effective exposure time [s] (internal "
|
|
509
|
+
"sync)")
|
|
510
|
+
|
|
511
|
+
primary_header["CGSE"] = (self.cgse_version, "Version of the Common EGSE")
|
|
512
|
+
|
|
513
|
+
LOGGER.info(f"Obsid in FITS persistence layer: {self.obsid}")
|
|
514
|
+
|
|
515
|
+
if self.obsid is not None:
|
|
516
|
+
|
|
517
|
+
LOGGER.debug(f"{self.obsid = }")
|
|
518
|
+
|
|
519
|
+
primary_header["OBSID"] = (self.obsid, "Observation identifier")
|
|
520
|
+
|
|
521
|
+
primary_header["PRE_SC_N"] = (CCD_SETTINGS.LENGTH_SERIAL_PRESCAN,
|
|
522
|
+
"Number of pixels/columns in the serial pre-scan")
|
|
523
|
+
primary_header["OVR_SC_N"] = (max(0, self.h_end + 1
|
|
524
|
+
- CCD_SETTINGS.LENGTH_SERIAL_PRESCAN - CCD_SETTINGS.NUM_COLUMNS // 2),
|
|
525
|
+
"Number of virtual pixels / columns in the serial over-scan")
|
|
526
|
+
primary_header["OVR_SC_R"] = (max(0, self.v_end + 1 - CCD_SETTINGS.NUM_ROWS),
|
|
527
|
+
"Number of rows in the parallel over-scan")
|
|
528
|
+
primary_header["IMG_REPR"] = ("FOV_IMG", "Right CCD side flipped w.r.t. readout direction")
|
|
529
|
+
|
|
530
|
+
return primary_header
|
|
531
|
+
|
|
532
|
+
def create_base_image_wcs(self):
|
|
533
|
+
""" Create a basic FITS header for the image.
|
|
534
|
+
|
|
535
|
+
Not all information can be filled out at this point (i.c. extension, CCD identifier, number of columns,
|
|
536
|
+
rotation, reference pixel). This will be done at the moment the serial pre-scan will be written to file.
|
|
537
|
+
|
|
538
|
+
Note that, if both CCD sides will be transmitted, the image of both sides will be glued together (the E-side
|
|
539
|
+
must be flipped horizontally first). If only the E-side will be transmitted, its image must be flipped
|
|
540
|
+
horizontally.
|
|
541
|
+
|
|
542
|
+
Returns: Basic FITS header for the image.
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
image_header = fits.ImageHDU().header
|
|
546
|
+
|
|
547
|
+
# Make sure the data is saved as 16-bit
|
|
548
|
+
|
|
549
|
+
image_header["BITPIX"] = 16
|
|
550
|
+
image_header["BZERO"] = 32768
|
|
551
|
+
|
|
552
|
+
# Dimensionality
|
|
553
|
+
|
|
554
|
+
num_rows = min(CCD_SETTINGS["NUM_ROWS"] - 1, self.v_end) - self.v_start + 1
|
|
555
|
+
|
|
556
|
+
image_header["NAXIS"] = (2, "Dimensionality of the image",)
|
|
557
|
+
image_header["NAXIS2"] = (num_rows, "Number of rows in the image",)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# Focal length (this is needed for the conversion to field angles)
|
|
561
|
+
|
|
562
|
+
image_header["FOCALLEN"] = (FOV_SETTINGS["FOCAL_LENGTH"], "Focal length [mm]",) # TODO
|
|
563
|
+
|
|
564
|
+
# Linear coordinate transformation from sub-field to focal-plane coordinates
|
|
565
|
+
|
|
566
|
+
image_header["ctype1"] = ("LINEAR", "Linear coordinate transformation",)
|
|
567
|
+
image_header["ctype2"] = ("LINEAR", "Linear coordinate transformation",)
|
|
568
|
+
|
|
569
|
+
# Focal-plane coordinates are expressed in mm
|
|
570
|
+
|
|
571
|
+
image_header["CUNIT1"] = ("MM", "Target unit in the column direction (mm)",)
|
|
572
|
+
image_header["CUNIT2"] = ("MM", "Target unit in the row direction (mm)",)
|
|
573
|
+
|
|
574
|
+
# Pixel size
|
|
575
|
+
|
|
576
|
+
cdelt = CCD_SETTINGS["PIXEL_SIZE"] / 1000.0 # Pixel size [mm]
|
|
577
|
+
image_header["CDELT1"] = (cdelt, "Pixel size in the x-direction [mm]",)
|
|
578
|
+
image_header["CDELT2"] = (cdelt, "Pixel size in the y-direction [mm]",)
|
|
579
|
+
|
|
580
|
+
# Additional keywords
|
|
581
|
+
|
|
582
|
+
# image_header["TELESCOP"] = (setup["camera_id"], "Camera ID")
|
|
583
|
+
# image_header["INSTRUME"] = (setup["camera_id"], "Camera ID")
|
|
584
|
+
|
|
585
|
+
image_header["TELESCOP"] = "PLATO"
|
|
586
|
+
if self.camera_id:
|
|
587
|
+
image_header["INSTRUME"] = (self.camera_id, "Camera ID")
|
|
588
|
+
image_header["SITENAME"] = (self.site_name, "Name of the test site")
|
|
589
|
+
image_header["SETUP"] = (self.setup_id, "Setup ID")
|
|
590
|
+
if self.obsid is not None:
|
|
591
|
+
image_header["OBSID"] = (self.obsid, "Observation identifier")
|
|
592
|
+
|
|
593
|
+
return image_header
|
|
594
|
+
|
|
595
|
+
def create_base_serial_prescan_wcs(self):
|
|
596
|
+
""" Create a basic FITS header for the serial pre-scan.
|
|
597
|
+
|
|
598
|
+
Not all information can be filled out at this point (i.c. extension, CCD identifier). This will be
|
|
599
|
+
done at the moment the serial pre-scan will be written to file.
|
|
600
|
+
|
|
601
|
+
Note that, if both CCD sides will be transmitted, you will end up having to serial pre-scan maps per exposures
|
|
602
|
+
(one for each side).
|
|
603
|
+
|
|
604
|
+
Returns: Basic FITS header for the serial pre-scan.
|
|
605
|
+
"""
|
|
606
|
+
|
|
607
|
+
serial_prescan_wcs = fits.ImageHDU().header
|
|
608
|
+
|
|
609
|
+
# Make sure the data is saved as 16-bit
|
|
610
|
+
|
|
611
|
+
serial_prescan_wcs["BITPIX"] = 16
|
|
612
|
+
serial_prescan_wcs["BZERO"] = 32768
|
|
613
|
+
|
|
614
|
+
# Dimensionality
|
|
615
|
+
|
|
616
|
+
num_rows = self.v_end - self.v_start + 1
|
|
617
|
+
num_columns = CCD_SETTINGS.LENGTH_SERIAL_PRESCAN
|
|
618
|
+
|
|
619
|
+
serial_prescan_wcs["NAXIS"] = (2, "Dimensionality of the serial pre-scan",)
|
|
620
|
+
serial_prescan_wcs["NAXIS1"] = (num_columns, "Number of columns in the serial pre-scan",)
|
|
621
|
+
serial_prescan_wcs["NAXIS2"] = (num_rows, "Number of rows in the serial pre-scan",)
|
|
622
|
+
|
|
623
|
+
serial_prescan_wcs["TELESCOP"] = "PLATO"
|
|
624
|
+
if self.camera_id:
|
|
625
|
+
serial_prescan_wcs["INSTRUME"] = (self.camera_id, "Camera ID")
|
|
626
|
+
serial_prescan_wcs["SITENAME"] = (self.site_name, "Name of the test site")
|
|
627
|
+
serial_prescan_wcs["SETUP"] = (self.setup_id, "Setup ID")
|
|
628
|
+
if self.obsid is not None:
|
|
629
|
+
serial_prescan_wcs["OBSID"] = (self.obsid, "Observation identifier")
|
|
630
|
+
|
|
631
|
+
return serial_prescan_wcs
|
|
632
|
+
|
|
633
|
+
def create_base_serial_overscan_wcs(self):
|
|
634
|
+
""" Create a basic FITS header for the serial over-scan.
|
|
635
|
+
|
|
636
|
+
Not all information can be filled out at this point (i.c. extension, CCD identifier). This will be
|
|
637
|
+
done at the moment the serial over-scan will be written to file.
|
|
638
|
+
|
|
639
|
+
Note that, if both CCD sides will be transmitted, you will end up having to serial over-scan maps per exposures
|
|
640
|
+
(one for each side).
|
|
641
|
+
|
|
642
|
+
Returns: Basic FITS header for the serial over-scan.
|
|
643
|
+
"""
|
|
644
|
+
|
|
645
|
+
serial_overscan_wcs = fits.ImageHDU().header
|
|
646
|
+
|
|
647
|
+
# Make sure the data is saved as 16-bit
|
|
648
|
+
|
|
649
|
+
serial_overscan_wcs["BITPIX"] = 16
|
|
650
|
+
serial_overscan_wcs["BZERO"] = 32768
|
|
651
|
+
|
|
652
|
+
# Dimensionality
|
|
653
|
+
|
|
654
|
+
num_rows = self.v_end - self.v_start + 1
|
|
655
|
+
num_columns = self.v_end + 1 - CCD_SETTINGS.LENGTH_SERIAL_PRESCAN - CCD_SETTINGS.NUM_COLUMNS / 2
|
|
656
|
+
|
|
657
|
+
serial_overscan_wcs["NAXIS"] = (2, "Dimensionality of the serial over-scan",)
|
|
658
|
+
serial_overscan_wcs["NAXIS1"] = (num_columns, "Number of columns in the serial over-scan",)
|
|
659
|
+
serial_overscan_wcs["NAXIS2"] = (num_rows, "Number of rows in the serial over-scan",)
|
|
660
|
+
|
|
661
|
+
# Site name
|
|
662
|
+
|
|
663
|
+
serial_overscan_wcs["TELESCOP"] = "PLATO"
|
|
664
|
+
if self.camera_id:
|
|
665
|
+
serial_overscan_wcs["INSTRUME"] = (self.camera_id, "Camera ID")
|
|
666
|
+
serial_overscan_wcs["SITENAME"] = (self.site_name, "Name of the test site")
|
|
667
|
+
serial_overscan_wcs["SETUP"] = (self.setup_id, "Setup ID")
|
|
668
|
+
if self.obsid is not None:
|
|
669
|
+
serial_overscan_wcs["OBSID"] = (self.obsid, "Observation identifier")
|
|
670
|
+
|
|
671
|
+
return serial_overscan_wcs
|
|
672
|
+
|
|
673
|
+
def create_base_parallel_overscan_wcs(self):
|
|
674
|
+
""" Create a basic FITS header for the parallel over-scan.
|
|
675
|
+
|
|
676
|
+
Not all information can be filled out at this point (i.c. extension, CCD identifier, number of columns). This
|
|
677
|
+
will be done at the moment the parallel over-scan will be written to file.
|
|
678
|
+
|
|
679
|
+
Note that, if both CCD sides will be transmitted, the parallel over-scan of both sides will be glued together
|
|
680
|
+
(the E-side must be flipped horizontally first). If only the E-side will be transmitted, its parallel
|
|
681
|
+
over-scan must be flipped horizontally.
|
|
682
|
+
|
|
683
|
+
Returns: Basic FITS header for the parallel over-scan.
|
|
684
|
+
"""
|
|
685
|
+
|
|
686
|
+
parallel_overscan_wcs = fits.ImageHDU().header
|
|
687
|
+
|
|
688
|
+
# Make sure the data is saved as 16-bit
|
|
689
|
+
|
|
690
|
+
parallel_overscan_wcs["BITPIX"] = 16
|
|
691
|
+
parallel_overscan_wcs["BZERO"] = 32768
|
|
692
|
+
|
|
693
|
+
# Dimensionality
|
|
694
|
+
|
|
695
|
+
num_rows_parallel_overscan = max(0, self.v_end - CCD_SETTINGS.NUM_ROWS + 1)
|
|
696
|
+
|
|
697
|
+
parallel_overscan_wcs["NAXIS"] = (2, "Dimensionality of the parallel over-scan",)
|
|
698
|
+
parallel_overscan_wcs["NAXIS2"] = (num_rows_parallel_overscan, "Number of rows in the parallel over-scan",)
|
|
699
|
+
|
|
700
|
+
# Site name
|
|
701
|
+
|
|
702
|
+
parallel_overscan_wcs["TELESCOP"] = "PLATO"
|
|
703
|
+
if self.camera_id:
|
|
704
|
+
parallel_overscan_wcs["INSTRUME"] = (self.camera_id, "Camera ID")
|
|
705
|
+
parallel_overscan_wcs["SITENAME"] = (self.site_name, "Name of the test site")
|
|
706
|
+
parallel_overscan_wcs["SETUP"] = (self.setup_id, "Setup ID")
|
|
707
|
+
if self.obsid is not None:
|
|
708
|
+
parallel_overscan_wcs["OBSID"] = (self.obsid, "Observation identifier")
|
|
709
|
+
|
|
710
|
+
return parallel_overscan_wcs
|
|
711
|
+
|
|
712
|
+
def open(self, mode=None):
|
|
713
|
+
|
|
714
|
+
primary_header = self.create_primary_header()
|
|
715
|
+
|
|
716
|
+
# The primary HDU contains only this header and no image data
|
|
717
|
+
|
|
718
|
+
primary_hdu = fits.PrimaryHDU()
|
|
719
|
+
primary_hdu.header = primary_header
|
|
720
|
+
|
|
721
|
+
# The FITS file is created. If the filename is already in use, and exception
|
|
722
|
+
# will be thrown.
|
|
723
|
+
|
|
724
|
+
primary_hdu.writeto(self._filepath)
|
|
725
|
+
|
|
726
|
+
self.is_fits_file_open = True
|
|
727
|
+
|
|
728
|
+
def close(self):
|
|
729
|
+
"""Closes the resource."""
|
|
730
|
+
|
|
731
|
+
self.is_fits_file_open = False
|
|
732
|
+
|
|
733
|
+
def exists(self):
|
|
734
|
+
|
|
735
|
+
return self._filepath.exists()
|
|
736
|
+
|
|
737
|
+
def create(self, data: dict):
|
|
738
|
+
"""Add the given data to the FITS file.
|
|
739
|
+
|
|
740
|
+
The given data is a stream of SpW packets, only part of which contains information should go in the FITS file:
|
|
741
|
+
|
|
742
|
+
- Image (and over-scan) data contain the transmitted readout of the CCD(s);
|
|
743
|
+
- Timecode and housekeeping packets are not needed for this purpose.
|
|
744
|
+
"""
|
|
745
|
+
|
|
746
|
+
for key, spw_packet in data.items():
|
|
747
|
+
|
|
748
|
+
# if isinstance(spw_packet, TimecodePacket):
|
|
749
|
+
#
|
|
750
|
+
# self.timestamp = spw_packet.
|
|
751
|
+
|
|
752
|
+
if key == "Timestamp":
|
|
753
|
+
|
|
754
|
+
self.timestamp = spw_packet
|
|
755
|
+
self.finetime = time_since_epoch_1958(self.timestamp)
|
|
756
|
+
|
|
757
|
+
elif isinstance(spw_packet, DataPacket):
|
|
758
|
+
|
|
759
|
+
try:
|
|
760
|
+
ccd_bin_to_id = self.setup.camera.fee.ccd_numbering.CCD_BIN_TO_ID
|
|
761
|
+
except AttributeError:
|
|
762
|
+
raise SetupError("No entry in the setup for camera.fee.ccd_numbering.CCD_BIN_TO_ID")
|
|
763
|
+
spw_packet_data_type = spw_packet.type
|
|
764
|
+
ccd_number = ccd_bin_to_id[spw_packet_data_type.ccd_number] # 1-4
|
|
765
|
+
ccd_side = self.fee_side(spw_packet_data_type.ccd_side)
|
|
766
|
+
data_array = spw_packet.data_as_ndarray
|
|
767
|
+
|
|
768
|
+
self.data_arrays[ccd_side][ccd_number] = np.append(self.data_arrays[ccd_side][ccd_number], data_array)
|
|
769
|
+
|
|
770
|
+
# If all data has been received for the current exposure of this CCD, the following steps must be
|
|
771
|
+
# performed:
|
|
772
|
+
# - re-shape the 1D data arrays to 2D data arrays;
|
|
773
|
+
# - extract the different pieces of the 2D data arrays (image + scan maps);
|
|
774
|
+
# - for the F-side: flip the image and parallel over-scan horizontally;
|
|
775
|
+
# - if both CCD sides are transmitted, stitch them together (for the image and parallel
|
|
776
|
+
# over-scan);
|
|
777
|
+
# - update the WCS of the different regions;
|
|
778
|
+
# - add to the FITS file.
|
|
779
|
+
|
|
780
|
+
if self.is_all_data_received(spw_packet_data_type, ccd_number, ccd_side):
|
|
781
|
+
|
|
782
|
+
# Write the information to FITS
|
|
783
|
+
|
|
784
|
+
for ccd_side in self.fee_side:
|
|
785
|
+
|
|
786
|
+
try:
|
|
787
|
+
if self.data_arrays[ccd_side][ccd_number].size > 0:
|
|
788
|
+
self.assemble_slice(ccd_number, ccd_side)
|
|
789
|
+
self.frame_number[ccd_number][ccd_side] += 1
|
|
790
|
+
except MissingSpWPacketsError as exc:
|
|
791
|
+
LOGGER.info(exc)
|
|
792
|
+
|
|
793
|
+
# Get ready for the next exposure
|
|
794
|
+
|
|
795
|
+
self.clear_for_next_exposure(ccd_number, ccd_side)
|
|
796
|
+
|
|
797
|
+
def is_all_data_received(self, spw_packet_data_type: DataPacketType, ccd_number: int, ccd_side):
|
|
798
|
+
""" Check if all data has been received for the current exposure.
|
|
799
|
+
|
|
800
|
+
Args:
|
|
801
|
+
- spw_packet_data_type: Last received data packet type.
|
|
802
|
+
- ccd_number: CCD from which the last received data originates (1-4).
|
|
803
|
+
- ccd_side: CCD side from which the last received data originates.
|
|
804
|
+
|
|
805
|
+
Returns: True if all data for the current exposure has been received; False otherwise.
|
|
806
|
+
"""
|
|
807
|
+
|
|
808
|
+
from egse.dpu import got_all_last_packets
|
|
809
|
+
|
|
810
|
+
if spw_packet_data_type.last_packet:
|
|
811
|
+
|
|
812
|
+
packet_type = spw_packet_data_type.packet_type
|
|
813
|
+
|
|
814
|
+
if packet_type == PacketType.DATA_PACKET:
|
|
815
|
+
|
|
816
|
+
if ccd_side == self.fee_side.E_SIDE:
|
|
817
|
+
|
|
818
|
+
self.received_last_packet_flags[ccd_number][0] = True
|
|
819
|
+
|
|
820
|
+
elif ccd_side == self.fee_side.F_SIDE:
|
|
821
|
+
|
|
822
|
+
self.received_last_packet_flags[ccd_number][1] = True
|
|
823
|
+
|
|
824
|
+
elif packet_type == PacketType.OVERSCAN_DATA:
|
|
825
|
+
|
|
826
|
+
if ccd_side == self.fee_side.E_SIDE:
|
|
827
|
+
|
|
828
|
+
self.received_last_packet_flags[ccd_number][2] = True
|
|
829
|
+
|
|
830
|
+
elif ccd_side == self.fee_side.F_SIDE:
|
|
831
|
+
|
|
832
|
+
self.received_last_packet_flags[ccd_number][3] = True
|
|
833
|
+
|
|
834
|
+
return got_all_last_packets(self.received_last_packet_flags[ccd_number], self.expected_last_packet_flags)
|
|
835
|
+
|
|
836
|
+
def got_all_last_packets(self, ccd_number: int, ccd_side):
|
|
837
|
+
""" Check whether all the expected last-packet flags have been seen for the given CCD side.
|
|
838
|
+
|
|
839
|
+
Args:
|
|
840
|
+
- ccd_number: CCD from which the last received data originates (1-4).
|
|
841
|
+
- ccd_side: CCD side from which the last received data originates.
|
|
842
|
+
|
|
843
|
+
Returns: True if the actual and the expected last-packet flags match for the given CCD side of the given CCD;
|
|
844
|
+
False otherwise.
|
|
845
|
+
"""
|
|
846
|
+
|
|
847
|
+
received_last_packet_flags = self.received_last_packet_flags[ccd_number]
|
|
848
|
+
|
|
849
|
+
if ccd_side == self.fee_side.E_SIDE:
|
|
850
|
+
|
|
851
|
+
return received_last_packet_flags[0] == self.expected_last_packet_flags[0] \
|
|
852
|
+
and received_last_packet_flags[2] == self.expected_last_packet_flags[2]
|
|
853
|
+
|
|
854
|
+
elif ccd_side == self.fee_side.F_SIDE:
|
|
855
|
+
|
|
856
|
+
return received_last_packet_flags[1] == self.expected_last_packet_flags[1] \
|
|
857
|
+
and received_last_packet_flags[3] == self.expected_last_packet_flags[3]
|
|
858
|
+
|
|
859
|
+
return False
|
|
860
|
+
|
|
861
|
+
def assemble_slice(self, ccd_number: int, ccd_side):
|
|
862
|
+
""" Assemble the data for the given CCD and write it to FITS.
|
|
863
|
+
|
|
864
|
+
Args:
|
|
865
|
+
- ccd_number: CCD identifier (1/2/3/4).
|
|
866
|
+
"""
|
|
867
|
+
|
|
868
|
+
# Windowing (pattern) mode
|
|
869
|
+
|
|
870
|
+
if self.is_windowing_mode:
|
|
871
|
+
|
|
872
|
+
# self.assemble_slice_windowing_mode(ccd_number, ccd_side)
|
|
873
|
+
pass
|
|
874
|
+
|
|
875
|
+
# Full-image (pattern) mode
|
|
876
|
+
|
|
877
|
+
else:
|
|
878
|
+
|
|
879
|
+
self.assemble_slice_full_image_mode(ccd_number, ccd_side)
|
|
880
|
+
|
|
881
|
+
# def assemble_slice_windowing_mode(self, ccd_number: int):
|
|
882
|
+
# """ Assemble the data for the given CCD and write it to FITS, for windowing mode or windowing pattern mode.
|
|
883
|
+
#
|
|
884
|
+
# This consists of the following steps:
|
|
885
|
+
#
|
|
886
|
+
# - Create a 1D array, filled with NaNs, exactly big enough to fit the image and scan maps;
|
|
887
|
+
# - Insert the data that was acquired (i.e. coming from the windows);
|
|
888
|
+
# - Convert the 1D data arrays to a 2D data array, in which the image and the scan maps are still glued
|
|
889
|
+
# together (do this for each transmitted CCD side);
|
|
890
|
+
# - Extract for each transmitted CCD side the different regions from the 2D array (image, serial pre-scan,
|
|
891
|
+
# serial over-scan, and parallel over-scan);
|
|
892
|
+
# - Append the serial pre-scan of the transmitted CCD side(s) to the FITS file (after completing its header);
|
|
893
|
+
# - Append the serial over-scan of the transmitted CCD side(s) to the FITS file (after completing its header),
|
|
894
|
+
# if present;
|
|
895
|
+
# - In case the F-side is transmitted, flips its image and parallel over-scan horizontally;
|
|
896
|
+
# - In case both sides are transmitted, stitch the two sides together for the image and the parallel
|
|
897
|
+
# over-scan;
|
|
898
|
+
# - Append the parallel over-scan to the FITS file (after completing its header), if present;
|
|
899
|
+
# - Append the image to the FITS file (after completing its header).
|
|
900
|
+
#
|
|
901
|
+
# Args:
|
|
902
|
+
# - ccd_number: CCD identifier (1/2/3/4).
|
|
903
|
+
# """
|
|
904
|
+
#
|
|
905
|
+
# num_rows = self.v_end - self.v_start + 1
|
|
906
|
+
# num_columns = self.h_end + 1
|
|
907
|
+
#
|
|
908
|
+
# num_rows_image = min(CCD_SETTINGS.NUM_ROWS - 1, self.v_end) - self.v_start + 1
|
|
909
|
+
# num_rows_parallel_overscan = max(0, num_rows - num_rows_image)
|
|
910
|
+
#
|
|
911
|
+
# image = np.array([]).reshape(num_rows_image, 0)
|
|
912
|
+
# parallel_overscan = np.array([]).reshape(num_rows_parallel_overscan, 0)
|
|
913
|
+
#
|
|
914
|
+
# for ccd_side in fee_side:
|
|
915
|
+
#
|
|
916
|
+
# if ccd_side in self.data_arrays:
|
|
917
|
+
#
|
|
918
|
+
# data_array = np.array([float("nan")] * (num_rows * num_columns))
|
|
919
|
+
# data_array[self.windows[ccd_side][ccd_number]] = self.data_arrays[ccd_side][ccd_number]
|
|
920
|
+
#
|
|
921
|
+
# side_image, side_serial_prescan, side_serial_overscan, side_parallel_overscan = \
|
|
922
|
+
# self.extract_full_image_mode(data_array)
|
|
923
|
+
#
|
|
924
|
+
# # Serial pre-scan
|
|
925
|
+
#
|
|
926
|
+
# self.append_serial_prescan(side_serial_prescan, ccd_number, ccd_side)
|
|
927
|
+
#
|
|
928
|
+
# # Serial over-scan
|
|
929
|
+
#
|
|
930
|
+
# if self.has_serial_overscan:
|
|
931
|
+
#
|
|
932
|
+
# self.append_serial_overscan(side_serial_overscan, ccd_number, ccd_side)
|
|
933
|
+
#
|
|
934
|
+
# # For the F-side, the image and parallel over-scan must be flipped horizontally
|
|
935
|
+
#
|
|
936
|
+
# if ccd_side == fee_side.F_SIDE:
|
|
937
|
+
#
|
|
938
|
+
# side_image, side_parallel_overscan = self.flip_f_side_full_image_mode(side_image,
|
|
939
|
+
# side_parallel_overscan)
|
|
940
|
+
#
|
|
941
|
+
# # Image (the part that is on this side)
|
|
942
|
+
#
|
|
943
|
+
# image = np.hstack([image, side_image])
|
|
944
|
+
#
|
|
945
|
+
# # Parallel over-scan (the part that is on this side)
|
|
946
|
+
#
|
|
947
|
+
# if self.has_parallel_overscan:
|
|
948
|
+
#
|
|
949
|
+
# parallel_overscan = np.hstack([parallel_overscan, side_parallel_overscan])
|
|
950
|
+
#
|
|
951
|
+
# # Parallel over-scan
|
|
952
|
+
#
|
|
953
|
+
# if self.has_parallel_overscan:
|
|
954
|
+
#
|
|
955
|
+
# self.append_parallel_overscan(parallel_overscan, ccd_number)
|
|
956
|
+
#
|
|
957
|
+
# # Image
|
|
958
|
+
#
|
|
959
|
+
# self.append_image(image, ccd_number)
|
|
960
|
+
|
|
961
|
+
def assemble_slice_full_image_mode(self, ccd_number: int, ccd_side):
|
|
962
|
+
""" Assemble the data for the given CCD and write it to FITS, for full-image mode or full-image pattern mode.
|
|
963
|
+
|
|
964
|
+
This consists of the following steps:
|
|
965
|
+
|
|
966
|
+
- Convert the 1D data arrays to a 2D data array, in which the image and the scan maps are still glued
|
|
967
|
+
together (do this for each transmitted CCD side);
|
|
968
|
+
- Extract for each transmitted CCD side the different regions from the 2D array (image, serial pre-scan,
|
|
969
|
+
serial over-scan, and parallel over-scan);
|
|
970
|
+
- Append the serial pre-scan of the transmitted CCD side(s) to the FITS file (after completing its header);
|
|
971
|
+
- Append the serial over-scan of the transmitted CCD side(s) to the FITS file (after completing its header),
|
|
972
|
+
if present;
|
|
973
|
+
- In case the F-side is transmitted, flips its image and parallel over-scan horizontally;
|
|
974
|
+
- In case both sides are transmitted, stitch the two sides together for the image and the parallel
|
|
975
|
+
over-scan;
|
|
976
|
+
- Append the parallel over-scan to the FITS file (after completing its header), if present;
|
|
977
|
+
- Append the image to the FITS file (after completing its header).
|
|
978
|
+
|
|
979
|
+
Args:
|
|
980
|
+
- ccd_number: CCD identifier (1/2/3/4).
|
|
981
|
+
"""
|
|
982
|
+
|
|
983
|
+
num_rows = self.v_end - self.v_start + 1
|
|
984
|
+
num_columns = self.h_end + 1
|
|
985
|
+
|
|
986
|
+
# Re-shape the 1D array to a 2D array and extract the image and scan maps
|
|
987
|
+
|
|
988
|
+
try:
|
|
989
|
+
data_array = np.reshape(self.data_arrays[ccd_side][ccd_number], (num_rows, num_columns)) # 1D -> 2D
|
|
990
|
+
except ValueError as exc:
|
|
991
|
+
raise MissingSpWPacketsError(f"Incomplete SpW data for frame {self.frame_number[ccd_number][ccd_side]} "
|
|
992
|
+
f"for the {ccd_side.name[0]}-side off CCD{ccd_number}. Check the DPU "
|
|
993
|
+
f"Processing logs for more information.") from exc
|
|
994
|
+
side_image, side_serial_prescan, side_serial_overscan, side_parallel_overscan = \
|
|
995
|
+
self.extract_full_image_mode(data_array)
|
|
996
|
+
|
|
997
|
+
# Serial pre-scan
|
|
998
|
+
|
|
999
|
+
self.append_serial_prescan(side_serial_prescan, ccd_number, ccd_side)
|
|
1000
|
+
|
|
1001
|
+
# Serial over-scan
|
|
1002
|
+
|
|
1003
|
+
if self.has_serial_overscan:
|
|
1004
|
+
|
|
1005
|
+
self.append_serial_overscan(side_serial_overscan, ccd_number, ccd_side)
|
|
1006
|
+
|
|
1007
|
+
# Parallel over-scan
|
|
1008
|
+
|
|
1009
|
+
if self.has_parallel_overscan:
|
|
1010
|
+
|
|
1011
|
+
self.append_parallel_overscan(side_parallel_overscan, ccd_number, ccd_side)
|
|
1012
|
+
|
|
1013
|
+
# Image
|
|
1014
|
+
|
|
1015
|
+
self.append_image(side_image, ccd_number, ccd_side)
|
|
1016
|
+
|
|
1017
|
+
def extract_full_image_mode(self, data_array):
|
|
1018
|
+
"""Extract the image and scan maps from the given data array.
|
|
1019
|
+
|
|
1020
|
+
For the F-side, the image and parallel over-scan still have to be flipped horizontally.
|
|
1021
|
+
|
|
1022
|
+
Args:
|
|
1023
|
+
- data_array: 2D array in which the image data and scan maps are still glued together.
|
|
1024
|
+
|
|
1025
|
+
Returns:
|
|
1026
|
+
- Image data;
|
|
1027
|
+
- Serial pre-scan;
|
|
1028
|
+
- Serial over-scan;
|
|
1029
|
+
- Parallel over-scan.
|
|
1030
|
+
"""
|
|
1031
|
+
|
|
1032
|
+
# Calculate the following indices:
|
|
1033
|
+
# - Column index of the first transmitted image column;
|
|
1034
|
+
# - Column index of the last transmitted image column + 1 (if the serial over-scan is transmitted, this will
|
|
1035
|
+
# be the column index of the first transmitted serial over-scan column);
|
|
1036
|
+
# - Row index of the last transmitted image row + 1 (if the parallel over-scan is transmitted, this will be
|
|
1037
|
+
# the row index of the first transmitted parallel over-scan row).
|
|
1038
|
+
|
|
1039
|
+
start_column_image = CCD_SETTINGS.LENGTH_SERIAL_PRESCAN
|
|
1040
|
+
end_column_image_plus_1 = min(CCD_SETTINGS.LENGTH_SERIAL_PRESCAN + CCD_SETTINGS.NUM_COLUMNS // 2, self.h_end + 1)
|
|
1041
|
+
end_row_image_plus_1 = min(CCD_SETTINGS.NUM_ROWS - 1, self.v_end) - self.v_start + 1
|
|
1042
|
+
|
|
1043
|
+
# Serial pre-scan (all rows, but only the first couple of rows)
|
|
1044
|
+
|
|
1045
|
+
serial_prescan = data_array[:, 0: start_column_image]
|
|
1046
|
+
|
|
1047
|
+
# Serial over-scan (all rows, omit the columns from the serial pre-scan and the serial over-scan)
|
|
1048
|
+
|
|
1049
|
+
if self.has_serial_overscan:
|
|
1050
|
+
|
|
1051
|
+
serial_overscan = data_array[:, end_column_image_plus_1:]
|
|
1052
|
+
|
|
1053
|
+
else:
|
|
1054
|
+
|
|
1055
|
+
serial_overscan = None
|
|
1056
|
+
|
|
1057
|
+
# Image (omit the rows from the parallel over-scan
|
|
1058
|
+
# and the columns from the serial pre-scan and the serial over-scan)
|
|
1059
|
+
|
|
1060
|
+
image = data_array[0: end_row_image_plus_1, start_column_image: end_column_image_plus_1]
|
|
1061
|
+
|
|
1062
|
+
# Parallel over-scan
|
|
1063
|
+
|
|
1064
|
+
if self.has_parallel_overscan:
|
|
1065
|
+
|
|
1066
|
+
parallel_overscan = data_array[end_row_image_plus_1:, start_column_image: end_column_image_plus_1]
|
|
1067
|
+
|
|
1068
|
+
else:
|
|
1069
|
+
|
|
1070
|
+
parallel_overscan = None
|
|
1071
|
+
|
|
1072
|
+
return image, serial_prescan, serial_overscan, parallel_overscan
|
|
1073
|
+
|
|
1074
|
+
def append_serial_prescan(self, serial_prescan, ccd_number: int, ccd_side: str):
|
|
1075
|
+
""" Append the given serial pre-scan to the FITS file (after completing its header).
|
|
1076
|
+
|
|
1077
|
+
Args:
|
|
1078
|
+
- serial_prescan: Serial pre-scan.
|
|
1079
|
+
- ccd_number: CCD identifier.
|
|
1080
|
+
- ccd_side: CCD side.
|
|
1081
|
+
"""
|
|
1082
|
+
|
|
1083
|
+
extension = f"SPRE_{ccd_number}_{self.fee_side(ccd_side).name[0]}"
|
|
1084
|
+
self.serial_prescan_header["EXTNAME"] = extension
|
|
1085
|
+
self.serial_prescan_header["EXTVER"] = self.frame_number[ccd_number][ccd_side]
|
|
1086
|
+
self.serial_prescan_header["CCD_ID"] = (ccd_number, "CCD identifier",)
|
|
1087
|
+
self.serial_prescan_header["SENSOR_SEL"] = (self.fee_side(ccd_side).name[0], "CCD side")
|
|
1088
|
+
self.serial_prescan_header["DATE-OBS"] = (self.timestamp, "Timestamp for 1st frame",)
|
|
1089
|
+
self.serial_prescan_header["FINETIME"] = (self.finetime, "Finetime representation of DATE-OBS",)
|
|
1090
|
+
|
|
1091
|
+
if ccd_side in [self.fee_side.RIGHT_SIDE.name, self.fee_side.RIGHT_SIDE]:
|
|
1092
|
+
serial_prescan = np.fliplr(serial_prescan)
|
|
1093
|
+
|
|
1094
|
+
fits.append(str(self._filepath), serial_prescan, self.serial_prescan_header)
|
|
1095
|
+
|
|
1096
|
+
def append_serial_overscan(self, serial_overscan, ccd_number: int, ccd_side: str):
|
|
1097
|
+
""" Append the given serial over-scan to the FITS file (after completing its header).
|
|
1098
|
+
|
|
1099
|
+
Args:
|
|
1100
|
+
- serial_overscan: Serial over-scan.
|
|
1101
|
+
- ccd_number: CCD identifier.
|
|
1102
|
+
- ccd_side: CCD side.
|
|
1103
|
+
"""
|
|
1104
|
+
|
|
1105
|
+
extension = f"SOVER_{ccd_number}_{self.fee_side(ccd_side).name[0]}"
|
|
1106
|
+
self.serial_overscan_header["EXTNAME"] = extension
|
|
1107
|
+
self.serial_overscan_header["EXTVER"] = self.frame_number[ccd_number][ccd_side]
|
|
1108
|
+
self.serial_overscan_header["CCD_ID"] = (ccd_number, "CCD identifier",)
|
|
1109
|
+
self.serial_overscan_header["SENSOR_SEL"] = (self.fee_side(ccd_side).name[0], "CCD side")
|
|
1110
|
+
self.serial_overscan_header["NAXIS1"] = (serial_overscan.shape[1], "Number of columns in the serial over-scan",)
|
|
1111
|
+
self.serial_overscan_header["DATE-OBS"] = (self.timestamp, "Timestamp for 1st frame",)
|
|
1112
|
+
self.serial_overscan_header["FINETIME"] = (self.finetime, "Finetime representation of DATE-OBS")
|
|
1113
|
+
|
|
1114
|
+
if ccd_side in [self.fee_side.RIGHT_SIDE.name, self.fee_side.RIGHT_SIDE]:
|
|
1115
|
+
serial_overscan = np.fliplr(serial_overscan)
|
|
1116
|
+
|
|
1117
|
+
fits.append(str(self._filepath), serial_overscan, self.serial_overscan_header)
|
|
1118
|
+
|
|
1119
|
+
def append_parallel_overscan(self, parallel_overscan, ccd_number: int, ccd_side: str):
|
|
1120
|
+
""" Append the given parallel over-scan to the FITS file (after completing its header).
|
|
1121
|
+
|
|
1122
|
+
Args:
|
|
1123
|
+
- parallel_overscan: Parallel over-scan.
|
|
1124
|
+
- ccd_number: CCD identifier.
|
|
1125
|
+
- ccd_side: CCD side.
|
|
1126
|
+
"""
|
|
1127
|
+
|
|
1128
|
+
extension = f"POVER_{ccd_number}_{self.fee_side(ccd_side).name[0]}"
|
|
1129
|
+
self.parallel_overscan_header["EXTNAME"] = extension
|
|
1130
|
+
self.parallel_overscan_header["EXTVER"] = self.frame_number[ccd_number][ccd_side]
|
|
1131
|
+
self.parallel_overscan_header["CCD_ID"] = (ccd_number, "CCD identifier",)
|
|
1132
|
+
self.parallel_overscan_header["SENSOR_SEL"] = (self.fee_side(ccd_side).name[0], "CCD side")
|
|
1133
|
+
self.parallel_overscan_header["NAXIS1"] = (parallel_overscan.shape[1],
|
|
1134
|
+
"Number of columns in the parallel over-scan",)
|
|
1135
|
+
self.parallel_overscan_header["DATE-OBS"] = (self.timestamp, "Timestamp for 1st frame",)
|
|
1136
|
+
self.parallel_overscan_header["FINETIME"] = (self.finetime, "Finetime representation of DATE-OBS",)
|
|
1137
|
+
|
|
1138
|
+
if ccd_side in [self.fee_side.RIGHT_SIDE.name, self.fee_side.RIGHT_SIDE]:
|
|
1139
|
+
parallel_overscan = np.fliplr(parallel_overscan)
|
|
1140
|
+
|
|
1141
|
+
fits.append(str(self._filepath), parallel_overscan, self.parallel_overscan_header)
|
|
1142
|
+
|
|
1143
|
+
def append_image(self, image, ccd_number: int, ccd_side: str):
|
|
1144
|
+
""" Append the given image to the FITS file (after completing its header).
|
|
1145
|
+
|
|
1146
|
+
Args:
|
|
1147
|
+
- image: Image.
|
|
1148
|
+
- ccd_number: CCD identifier (1/2/3/4).
|
|
1149
|
+
- ccd_side: CCD side.
|
|
1150
|
+
"""
|
|
1151
|
+
|
|
1152
|
+
extension = f"IMAGE_{ccd_number}_{self.fee_side(ccd_side).name[0]}"
|
|
1153
|
+
self.image_header["EXTNAME"] = extension
|
|
1154
|
+
self.image_header["EXTVER"] = self.frame_number[ccd_number][ccd_side]
|
|
1155
|
+
self.image_header["CCD_ID"] = (ccd_number, "CCD identifier",)
|
|
1156
|
+
self.image_header["SENSOR_SEL"] = (self.fee_side(ccd_side).name[0], "CCD side")
|
|
1157
|
+
self.image_header["NAXIS1"] = (image.shape[1], "Number of columns in the image",)
|
|
1158
|
+
|
|
1159
|
+
ccd_orientation_degrees = CCD_SETTINGS.ORIENTATION[ccd_number - 1]
|
|
1160
|
+
ccd_orientation_radians = radians(ccd_orientation_degrees)
|
|
1161
|
+
|
|
1162
|
+
self.image_header["CROTA2"] = (ccd_orientation_degrees, "CCD orientation angle [degrees]",)
|
|
1163
|
+
|
|
1164
|
+
cdelt = CCD_SETTINGS["PIXEL_SIZE"] / 1000.0 # Pixel size [mm]
|
|
1165
|
+
|
|
1166
|
+
self.image_header["CD1_1"] = (cdelt * cos(ccd_orientation_radians),
|
|
1167
|
+
"Pixel size x cos(CCD orientation angle)",)
|
|
1168
|
+
self.image_header["CD1_2"] = (-cdelt * sin(ccd_orientation_radians),
|
|
1169
|
+
"-Pixel size x sin(CCD orientation angle)",)
|
|
1170
|
+
self.image_header["CD2_1"] = (cdelt * sin(ccd_orientation_radians),
|
|
1171
|
+
"Pixel size x sin(CCD orientation angle)",)
|
|
1172
|
+
self.image_header["CD2_2"] = (cdelt * cos(ccd_orientation_radians),
|
|
1173
|
+
"Pixel size x cos(CCD orientation angle)",)
|
|
1174
|
+
|
|
1175
|
+
zeropoint_x, zeropoint_y = -np.array(CCD_SETTINGS.ZEROPOINT)
|
|
1176
|
+
crval1 = zeropoint_x * cos(ccd_orientation_radians) - zeropoint_y * sin(ccd_orientation_radians)
|
|
1177
|
+
crval2 = zeropoint_x * sin(ccd_orientation_radians) + zeropoint_y * cos(ccd_orientation_radians)
|
|
1178
|
+
|
|
1179
|
+
self.image_header["CRVAL1"] = (crval1, "FP x-coordinate of the CCD origin [mm]",)
|
|
1180
|
+
self.image_header["CRVAL2"] = (crval2, "FP y-coordinate of the CCD origin [mm]",)
|
|
1181
|
+
|
|
1182
|
+
self.image_header["CRPIX2"] = (-self.v_start,
|
|
1183
|
+
"CCD origin row wrt 1st transmitted row",)
|
|
1184
|
+
|
|
1185
|
+
if ccd_side in [self.fee_side.LEFT_SIDE.name, self.fee_side.LEFT_SIDE]:
|
|
1186
|
+
self.image_header["CRPIX1"] = (0, "CCD origin column wrt lower left corner",)
|
|
1187
|
+
|
|
1188
|
+
if ccd_side in [self.fee_side.RIGHT_SIDE.name, self.fee_side.RIGHT_SIDE]:
|
|
1189
|
+
image = np.fliplr(image)
|
|
1190
|
+
self.image_header["CRPIX1"] = (-CCD_SETTINGS.NUM_ROWS + image.shape[1],
|
|
1191
|
+
"CCD origin column wrt lower left corner")
|
|
1192
|
+
|
|
1193
|
+
self.image_header["DATE-OBS"] = (self.timestamp, "Timestamp for 1st frame",)
|
|
1194
|
+
self.image_header["FINETIME"] = (self.finetime, "Finetime representation of DATE-OBS",)
|
|
1195
|
+
|
|
1196
|
+
fits.append(str(self._filepath), image, self.image_header)
|
|
1197
|
+
|
|
1198
|
+
def read(self, select=None):
|
|
1199
|
+
"""Returns a list of all entries in the persistence store.
|
|
1200
|
+
|
|
1201
|
+
The list can be filtered based on a selection from the `select` argument which
|
|
1202
|
+
should be a Callable object.
|
|
1203
|
+
|
|
1204
|
+
Args:
|
|
1205
|
+
- select (Callable): a filter function to narrow down the list of all entries.
|
|
1206
|
+
|
|
1207
|
+
Returns: List or generator for all entries in the persistence store.
|
|
1208
|
+
"""
|
|
1209
|
+
raise NotImplementedError("Persistence layers must implement a read method")
|
|
1210
|
+
|
|
1211
|
+
def update(self, idx, data):
|
|
1212
|
+
|
|
1213
|
+
pass
|
|
1214
|
+
|
|
1215
|
+
def delete(self, idx):
|
|
1216
|
+
|
|
1217
|
+
LOGGER.warning("The delete functionality is not implemented for the CSV persistence layer.")
|
|
1218
|
+
|
|
1219
|
+
def get_filepath(self):
|
|
1220
|
+
|
|
1221
|
+
return self._filepath
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plato-fits
|
|
3
|
+
Version: 2024.1.3
|
|
4
|
+
Summary: FITS Persistence implementation for CGSE
|
|
5
|
+
Author: IVS KU Leuven
|
|
6
|
+
Maintainer-email: Rik Huygen <rik.huygen@kuleuven.be>, Sara Regibo <sara.regibo@kuleuven.be>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: CGSE,Common-EGSE,hardware testing,software framework
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Requires-Dist: astropy
|
|
11
|
+
Requires-Dist: cgse-common
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Requires-Dist: plato-spw
|
|
14
|
+
Provides-Extra: test
|
|
15
|
+
Requires-Dist: pytest; extra == 'test'
|
|
16
|
+
Requires-Dist: pytest-cov; extra == 'test'
|
|
17
|
+
Requires-Dist: pytest-mock; extra == 'test'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# FITS plugin for the CGSE storage manager
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
egse/plugins/storage/fits.py,sha256=GlZSXCaFIihCKOj0dALcbzjeBcucxtImKGMtniFIvVk,56994
|
|
2
|
+
plato_fits-2024.1.3.dist-info/METADATA,sha256=UqN1NUdof3_f4D89DMCtHn5At9y1gyJs1Jto-huBJ1o,659
|
|
3
|
+
plato_fits-2024.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
4
|
+
plato_fits-2024.1.3.dist-info/entry_points.txt,sha256=q-XB-6IMXSeoxPNRDU-EFHgAyI0K6GzujNAyEdrp3zM,115
|
|
5
|
+
plato_fits-2024.1.3.dist-info/RECORD,,
|