plato-fits 0.11.5__tar.gz → 0.12.0__tar.gz
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.
- {plato_fits-0.11.5 → plato_fits-0.12.0}/PKG-INFO +2 -2
- {plato_fits-0.11.5 → plato_fits-0.12.0}/pyproject.toml +2 -2
- {plato_fits-0.11.5 → plato_fits-0.12.0}/src/egse/plugins/storage/fits.py +350 -204
- {plato_fits-0.11.5 → plato_fits-0.12.0}/.gitignore +0 -0
- {plato_fits-0.11.5 → plato_fits-0.12.0}/README.md +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plato-fits
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Summary: FITS Persistence implementation for CGSE
|
|
5
5
|
Author: IvS KU Leuven
|
|
6
6
|
Maintainer-email: Rik Huygen <rik.huygen@kuleuven.be>, Sara Regibo <sara.regibo@kuleuven.be>
|
|
7
7
|
License-Expression: MIT
|
|
8
8
|
Keywords: CGSE,Common-EGSE,hardware testing,software framework
|
|
9
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
10
|
Requires-Dist: astropy>=6.0.1
|
|
11
11
|
Requires-Dist: cgse-common
|
|
12
12
|
Requires-Dist: plato-spw
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "plato-fits"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.12.0"
|
|
4
4
|
description = "FITS Persistence implementation for CGSE"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "IvS KU Leuven"}
|
|
@@ -10,7 +10,7 @@ maintainers = [
|
|
|
10
10
|
{name = "Sara Regibo", email = "sara.regibo@kuleuven.be"}
|
|
11
11
|
]
|
|
12
12
|
readme = {"file" = "README.md", "content-type" = "text/markdown"}
|
|
13
|
-
requires-python = ">=3.
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
14
|
license = "MIT"
|
|
15
15
|
keywords = [
|
|
16
16
|
"CGSE",
|
|
@@ -20,19 +20,20 @@ CCD_SETTINGS = Settings.load("CCD")
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class FITSPersistenceError(Exception):
|
|
23
|
-
"""
|
|
23
|
+
"""Error for the FITS persistence layer."""
|
|
24
|
+
|
|
24
25
|
pass
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
class FITS(PersistenceLayer):
|
|
28
|
-
"""
|
|
29
|
+
"""Persistence layer that saves (image) data in a FITS file."""
|
|
29
30
|
|
|
30
31
|
extension = "fits"
|
|
31
32
|
|
|
32
|
-
warnings.simplefilter(
|
|
33
|
+
warnings.simplefilter("ignore", category=AstropyWarning)
|
|
33
34
|
|
|
34
35
|
def __init__(self, filename: str, prep: dict):
|
|
35
|
-
"""
|
|
36
|
+
"""Initialisation of the FITS persistence layer.
|
|
36
37
|
|
|
37
38
|
This consists of the following steps:
|
|
38
39
|
|
|
@@ -89,15 +90,17 @@ class FITS(PersistenceLayer):
|
|
|
89
90
|
self.data_arrays = {}
|
|
90
91
|
self.init_data_arrays()
|
|
91
92
|
|
|
92
|
-
self.frame_number = {
|
|
93
|
+
self.frame_number = {
|
|
94
|
+
ccd: {self.fee_side.LEFT_SIDE: 0, self.fee_side.RIGHT_SIDE: 0} for ccd in self.selected_ccds
|
|
95
|
+
}
|
|
93
96
|
self.timestamp = None
|
|
94
97
|
self.finetime = None
|
|
95
98
|
|
|
96
99
|
# How can you know whether or not all data for a given CCD has been received?
|
|
97
100
|
# (this depends on whether or not there is parallel over-scan data and on which CCD side(s) will be transmitted)
|
|
98
101
|
|
|
99
|
-
self.expected_last_packet_flags = prep["expected_last_packet_flags"]
|
|
100
|
-
self.received_last_packet_flags = {ccd: [False] * 4 for ccd in self.selected_ccds}
|
|
102
|
+
self.expected_last_packet_flags = prep["expected_last_packet_flags"] # To be received
|
|
103
|
+
self.received_last_packet_flags = {ccd: [False] * 4 for ccd in self.selected_ccds} # Actually received
|
|
101
104
|
|
|
102
105
|
LOGGER.debug(f"Init last packets flag: {self.received_last_packet_flags}")
|
|
103
106
|
|
|
@@ -112,28 +115,27 @@ class FITS(PersistenceLayer):
|
|
|
112
115
|
|
|
113
116
|
# Define the values of the WCS keywords
|
|
114
117
|
|
|
115
|
-
|
|
116
|
-
self.
|
|
117
|
-
self.
|
|
118
|
-
self.
|
|
119
|
-
self.
|
|
120
|
-
self.
|
|
121
|
-
self.cgse_version = prep["cgse_version"] # Version of the Common EGSE
|
|
118
|
+
self.v_start = prep["v_start"] # First transmitted row
|
|
119
|
+
self.v_end = prep["v_end"] # Last transmitted row
|
|
120
|
+
self.h_end = prep["h_end"] # Last transmitted column
|
|
121
|
+
self.rows_final_dump = prep["rows_final_dump"] # Number of rows to be dumped after readout
|
|
122
|
+
self.cycle_time = prep["cycle_time"] # Image cycle time [s]
|
|
123
|
+
self.cgse_version = prep["cgse_version"] # Version of the Common EGSE
|
|
122
124
|
self.obsid = prep["obsid"]
|
|
123
125
|
|
|
124
|
-
self.register_map = prep["register_map"]
|
|
126
|
+
self.register_map = prep["register_map"] # Register map
|
|
125
127
|
|
|
126
128
|
# Read information from the setup
|
|
127
129
|
|
|
128
130
|
self.setup: Setup = prep["setup"]
|
|
129
131
|
|
|
130
|
-
self.site_name = self.setup["site_id"]
|
|
131
|
-
self.setup_id = self.setup.get_id()
|
|
132
|
-
self.camera_id = self.setup.camera.get("ID")
|
|
132
|
+
self.site_name = self.setup["site_id"] # Site ID
|
|
133
|
+
self.setup_id = self.setup.get_id() # Setup ID
|
|
134
|
+
self.camera_id = self.setup.camera.get("ID") # Camera ID (None if not present in the setup)
|
|
133
135
|
|
|
134
|
-
self.readout_time = self.calculate_readout_time(self.setup)
|
|
136
|
+
self.readout_time = self.calculate_readout_time(self.setup) # Readout time [s]
|
|
135
137
|
try:
|
|
136
|
-
self.exposure_time = self.cycle_time - self.readout_time
|
|
138
|
+
self.exposure_time = self.cycle_time - self.readout_time # Exposure time [s]
|
|
137
139
|
except TypeError:
|
|
138
140
|
# Image cycle time is unknown (None)
|
|
139
141
|
self.exposure_time = None
|
|
@@ -152,7 +154,7 @@ class FITS(PersistenceLayer):
|
|
|
152
154
|
self.is_fits_file_open = False
|
|
153
155
|
|
|
154
156
|
def calculate_readout_time(self, setup: Setup):
|
|
155
|
-
"""
|
|
157
|
+
"""Calculate the readout time.
|
|
156
158
|
|
|
157
159
|
The readout time consists of:
|
|
158
160
|
|
|
@@ -174,13 +176,13 @@ class FITS(PersistenceLayer):
|
|
|
174
176
|
return time_read_rows + time_dump_rows
|
|
175
177
|
|
|
176
178
|
def get_vgd(self):
|
|
177
|
-
"""
|
|
179
|
+
"""Extract the VGD voltage from the register map.
|
|
178
180
|
|
|
179
181
|
Return: Configured VGD voltage [V].
|
|
180
182
|
"""
|
|
181
183
|
|
|
182
|
-
vgd_19 = self.register_map[(
|
|
183
|
-
vgd_20 = self.register_map[(
|
|
184
|
+
vgd_19 = self.register_map[("reg_19_config", "ccd_vgd_config")]
|
|
185
|
+
vgd_20 = self.register_map[("reg_20_config", "ccd_vgd_config")]
|
|
184
186
|
|
|
185
187
|
return ((vgd_20 << 4) + vgd_19) / 1000 * 5.983
|
|
186
188
|
|
|
@@ -192,11 +194,10 @@ class FITS(PersistenceLayer):
|
|
|
192
194
|
|
|
193
195
|
Return: Configured VRD voltage.
|
|
194
196
|
"""
|
|
195
|
-
vrd_18 = self.register_map[(
|
|
196
|
-
vrd_19 = self.register_map[(
|
|
197
|
-
|
|
198
|
-
return int(f'0x{vrd_19:x}{vrd_18:x}', 16)
|
|
197
|
+
vrd_18 = self.register_map[("reg_18_config", "ccd2_vrd_config")]
|
|
198
|
+
vrd_19 = self.register_map[("reg_19_config", "ccd2_vrd_config")]
|
|
199
199
|
|
|
200
|
+
return int(f"0x{vrd_19:x}{vrd_18:x}", 16)
|
|
200
201
|
|
|
201
202
|
def get_ccd3_vrd(self):
|
|
202
203
|
"""
|
|
@@ -206,14 +207,13 @@ class FITS(PersistenceLayer):
|
|
|
206
207
|
|
|
207
208
|
Return: Configured VRD voltage.
|
|
208
209
|
"""
|
|
209
|
-
vrd_18 = self.register_map[(
|
|
210
|
-
vrd_19 = self.register_map[(
|
|
211
|
-
|
|
212
|
-
return int(f'0x{vrd_19:x}{vrd_18:x}', 16)
|
|
210
|
+
vrd_18 = self.register_map[("reg_18_config", "ccd3_vrd_config")]
|
|
211
|
+
vrd_19 = self.register_map[("reg_19_config", "ccd3_vrd_config")]
|
|
213
212
|
|
|
213
|
+
return int(f"0x{vrd_19:x}{vrd_18:x}", 16)
|
|
214
214
|
|
|
215
215
|
def init_data_arrays(self):
|
|
216
|
-
"""
|
|
216
|
+
"""Initialise data arrays in which the data content of the SpW packets will be dumped.
|
|
217
217
|
|
|
218
218
|
At this point, we have already determined which side(s) of which CCD(s) will be read out. For each of them,
|
|
219
219
|
a placeholder will be foreseen in a dedicated dictionary. The structure of this dictionary is the following,
|
|
@@ -241,7 +241,7 @@ class FITS(PersistenceLayer):
|
|
|
241
241
|
self.data_arrays[self.fee_side.RIGHT_SIDE] = right_side_arrays
|
|
242
242
|
|
|
243
243
|
def clear_for_next_exposure(self, ccd_number: int, ccd_side):
|
|
244
|
-
"""
|
|
244
|
+
"""Indicate that no data has been received yet for the next exposure of the given CCD.
|
|
245
245
|
|
|
246
246
|
At the end of an exposure, when the data have been assembled and written to FITS:
|
|
247
247
|
|
|
@@ -260,7 +260,7 @@ class FITS(PersistenceLayer):
|
|
|
260
260
|
self.clear_last_packet_received(ccd_number)
|
|
261
261
|
|
|
262
262
|
def clear_data_arrays(self, ccd_number: int, ccd_side):
|
|
263
|
-
"""
|
|
263
|
+
"""Clear the data arrays for the given CCD.
|
|
264
264
|
|
|
265
265
|
At the end of an exposure, when the data have been assembled and written to FITS, the data arrays must be
|
|
266
266
|
cleared for the next exposure.
|
|
@@ -272,7 +272,7 @@ class FITS(PersistenceLayer):
|
|
|
272
272
|
self.data_arrays[ccd_side][ccd_number] = np.array([], dtype=np.uint16)
|
|
273
273
|
|
|
274
274
|
def clear_last_packet_received(self, ccd_number: int):
|
|
275
|
-
"""
|
|
275
|
+
"""Clear the information about the last packet being received for the given CCD.
|
|
276
276
|
|
|
277
277
|
At the end of an exposure, when the data have been assembled and written to FITS, it must be indicated that the
|
|
278
278
|
last packet has not been received for the next exposure for the given CCD.
|
|
@@ -300,26 +300,30 @@ class FITS(PersistenceLayer):
|
|
|
300
300
|
# return self.data_arrays[ccd_side][ccd_number]
|
|
301
301
|
|
|
302
302
|
def check_readout_mode(self):
|
|
303
|
-
"""
|
|
303
|
+
"""For now only checks whether the N-FEE is in the correct mode, i.e. full-image (pattern) mode.
|
|
304
304
|
|
|
305
305
|
In the future, if deemed necessary, windowing (pattern) mode may be implemented as well.
|
|
306
306
|
"""
|
|
307
307
|
|
|
308
|
-
if self.ccd_mode_config in [
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
308
|
+
if self.ccd_mode_config in [
|
|
309
|
+
n_fee_mode.FULL_IMAGE_MODE,
|
|
310
|
+
n_fee_mode.FULL_IMAGE_PATTERN_MODE,
|
|
311
|
+
n_fee_mode.PARALLEL_TRAP_PUMPING_1_MODE,
|
|
312
|
+
n_fee_mode.PARALLEL_TRAP_PUMPING_2_MODE,
|
|
313
|
+
n_fee_mode.SERIAL_TRAP_PUMPING_1_MODE,
|
|
314
|
+
n_fee_mode.SERIAL_TRAP_PUMPING_2_MODE,
|
|
315
|
+
]:
|
|
312
316
|
return False
|
|
313
317
|
|
|
314
318
|
if self.ccd_mode_config in [n_fee_mode.WINDOWING_PATTERN_MODE, n_fee_mode.WINDOWING_MODE]:
|
|
315
|
-
|
|
316
319
|
return True
|
|
317
320
|
|
|
318
321
|
else:
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
322
|
+
raise FITSPersistenceError(
|
|
323
|
+
"Construction of FITS files from SpW packets only implemented for full-image "
|
|
324
|
+
"(pattern) mode, windowing (pattern) mode, and parallel/serial trap pumping 1/2 "
|
|
325
|
+
"mode"
|
|
326
|
+
)
|
|
323
327
|
|
|
324
328
|
# def init_window_list(self, window_list: dict):
|
|
325
329
|
# """ Compile the window list.
|
|
@@ -399,14 +403,14 @@ class FITS(PersistenceLayer):
|
|
|
399
403
|
# return window_indices
|
|
400
404
|
|
|
401
405
|
def create_primary_header(self):
|
|
402
|
-
"""
|
|
406
|
+
"""Creates the primary header (i.e. the header of the primary HDU).
|
|
403
407
|
|
|
404
408
|
This contains information that is specific for the camera.
|
|
405
409
|
"""
|
|
406
410
|
|
|
407
411
|
primary_header = fits.PrimaryHDU().header
|
|
408
412
|
|
|
409
|
-
primary_header["LEVEL"] = 1
|
|
413
|
+
primary_header["LEVEL"] = 1 # Flat structure
|
|
410
414
|
|
|
411
415
|
primary_header["V_START"] = (self.v_start, "Index of 1st row that is transmitted (counting starts at 0)")
|
|
412
416
|
primary_header["V_END"] = (self.v_end, "Index of last row that is transmitted (counting starts at 0)")
|
|
@@ -414,38 +418,63 @@ class FITS(PersistenceLayer):
|
|
|
414
418
|
primary_header["ROWS_FINAL_DUMP"] = (self.rows_final_dump, "Number of rows for clearout after readout")
|
|
415
419
|
primary_header["READ_MODE"] = (n_fee_mode(self.ccd_mode_config).name, "N-FEE operating mode")
|
|
416
420
|
|
|
417
|
-
primary_header["CI_WIDTH"] = (
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
primary_header["
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
primary_header["
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
primary_header["
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
primary_header["
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
421
|
+
primary_header["CI_WIDTH"] = (
|
|
422
|
+
self.register_map["charge_injection_width"],
|
|
423
|
+
"Number of rows in each charge injection region",
|
|
424
|
+
)
|
|
425
|
+
primary_header["CI_GAP"] = (
|
|
426
|
+
self.register_map["charge_injection_gap"],
|
|
427
|
+
"Number of rows between charge injection regions",
|
|
428
|
+
)
|
|
429
|
+
primary_header["PARALLEL_TOI_PERIOD"] = (
|
|
430
|
+
self.register_map["parallel_toi_period"],
|
|
431
|
+
"Duration of a parallel overlap period (TOI) [us]",
|
|
432
|
+
)
|
|
433
|
+
primary_header["PARALLEL_CLK_OVERLAP"] = (
|
|
434
|
+
self.register_map["parallel_clk_overlap"],
|
|
435
|
+
"Extra parallel clock overlap [us]",
|
|
436
|
+
)
|
|
437
|
+
primary_header["CI_EN"] = (
|
|
438
|
+
self.register_map["charge_injection_width"],
|
|
439
|
+
"Charge injection enabled (1) / disabled (0)",
|
|
440
|
+
)
|
|
441
|
+
primary_header["TRI_LEVEL_CLK_EN"] = (
|
|
442
|
+
self.register_map["tri_level_clk_en"],
|
|
443
|
+
"Generating bi-level parallel clocks (0) / tri-level parallel clocks (1)",
|
|
444
|
+
)
|
|
445
|
+
primary_header["IMG_CLK_DIR"] = (
|
|
446
|
+
self.register_map["img_clk_dir"],
|
|
447
|
+
"Generating reverse parallel clocks (1) / normal forward parallel clocks (0)",
|
|
448
|
+
)
|
|
449
|
+
primary_header["REG_CLK_DIR"] = (
|
|
450
|
+
self.register_map["reg_clk_dir"],
|
|
451
|
+
"Generating reverse serial clocks (1) / normal forward serial clocks (0)",
|
|
452
|
+
)
|
|
453
|
+
primary_header["PACKET_SIZE"] = (
|
|
454
|
+
self.register_map["packet_size"],
|
|
455
|
+
"Packet size = load bytes + 10 (header bytes)",
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
primary_header["TRAP_PUMP_DWELL_CTR"] = (
|
|
459
|
+
self.register_map["Trap_Pumping_Dwell_counter"],
|
|
460
|
+
"Dwell timer for trap pumping [ns]",
|
|
461
|
+
)
|
|
438
462
|
primary_header["SENSOR_SEL"] = (self.register_map["sensor_sel"], "CCD port data transmission selection control")
|
|
439
463
|
primary_header["SYNC_SEL"] = (self.register_map["sync_sel"], "Internal (1) / external (0) sync")
|
|
440
|
-
primary_header["DIGITISE_EN"] = (
|
|
441
|
-
|
|
464
|
+
primary_header["DIGITISE_EN"] = (
|
|
465
|
+
self.register_map["digitise_en"],
|
|
466
|
+
"Digitised data transferred to the N-DPU (1) or not (0) during image mode",
|
|
467
|
+
)
|
|
442
468
|
primary_header["DG_EN"] = (self.register_map["DG_en"], "Dump gate high (1) / low (0)")
|
|
443
469
|
primary_header["CCD_READ_EN"] = (self.register_map["ccd_read_en"], "CCD readout enabled (1) / disabled (0)")
|
|
444
|
-
primary_header["CONV_DLY"] = (
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
470
|
+
primary_header["CONV_DLY"] = (
|
|
471
|
+
self.register_map["conv_dly"],
|
|
472
|
+
"Delay value from rising edge of CCD_R_EF_DRV (where ADC convert pulse is generated) [ns]",
|
|
473
|
+
)
|
|
474
|
+
primary_header["HIGH_PRECISION_HK_EN"] = (
|
|
475
|
+
self.register_map["High_precision_HK_en"],
|
|
476
|
+
"Sending high-precision HK (1) / pixel data (0)",
|
|
477
|
+
)
|
|
449
478
|
|
|
450
479
|
primary_header["CCD_VOD"] = (self.register_map["ccd_vod_config"], "Configured VOD")
|
|
451
480
|
|
|
@@ -464,27 +493,37 @@ class FITS(PersistenceLayer):
|
|
|
464
493
|
primary_header["CCD_IG_LO"] = (self.register_map["ccd_ig_lo_config"], "Configured IG-high")
|
|
465
494
|
primary_header["TRK_HLD_HI"] = (self.register_map["trk_hld_hi"], "Track and hold high")
|
|
466
495
|
primary_header["TRK_HLD_LO"] = (self.register_map["trk_hld_lo"], "Track and hold low")
|
|
467
|
-
primary_header["CONT_RST_ON"] = (
|
|
468
|
-
|
|
496
|
+
primary_header["CONT_RST_ON"] = (
|
|
497
|
+
self.register_map["cont_rst_on"],
|
|
498
|
+
"When 1, FPGA generates continuous reset clock during readout",
|
|
499
|
+
)
|
|
469
500
|
if not PLATO_CAMERA_IS_EM:
|
|
470
|
-
primary_header["CONT_CDSCLP_ON"] = (
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
501
|
+
primary_header["CONT_CDSCLP_ON"] = (
|
|
502
|
+
self.register_map["cont_cdsclp_on"],
|
|
503
|
+
"When 1, FPGA generates continuous CDS clamp during readout",
|
|
504
|
+
)
|
|
505
|
+
primary_header["CONT_ROWCLP_ON"] = (
|
|
506
|
+
self.register_map["cont_rowclp_on"],
|
|
507
|
+
"When 1, FPGA generates continuous row clamp during readout",
|
|
508
|
+
)
|
|
474
509
|
primary_header["R_CFG1"] = (self.register_map["r_cfg1"], "Clock cycle for Rph3-low, Rph1-high")
|
|
475
510
|
primary_header["R_CFG2"] = (self.register_map["r_cfg2"], "Clock cycle for Rph1-low, Rph2-high")
|
|
476
511
|
primary_header["CDSCLP_LO"] = (self.register_map["cdsclp_lo"], "Clock cycle for cdsclp low")
|
|
477
512
|
if not PLATO_CAMERA_IS_EM:
|
|
478
|
-
primary_header["ADC_PWRDN_EN"] = (
|
|
479
|
-
|
|
513
|
+
primary_header["ADC_PWRDN_EN"] = (
|
|
514
|
+
self.register_map["adc_pwrdn_en"],
|
|
515
|
+
"ADC power-down enabled (0) / disabled (1)",
|
|
516
|
+
)
|
|
480
517
|
primary_header["CDSCLP_HI"] = (self.register_map["cdsclp_hi"], "Clock cycle for cdsclp high")
|
|
481
518
|
primary_header["ROWCLP_HI"] = (self.register_map["rowclp_hi"], "Clock cycle for rowclp high")
|
|
482
519
|
primary_header["ROWCLP_LO"] = (self.register_map["rowclp_lo"], "Clock cycle for rowclp low")
|
|
483
520
|
# primary_header["SURFACE_INV_CTR"] = (self.register_map["Surface_Inversion_counter"],
|
|
484
521
|
# "Surface inversion counter")
|
|
485
522
|
primary_header["READOUT_PAUSE_CTR"] = (self.register_map["Readout_pause_counter"], "Readout pause counter")
|
|
486
|
-
primary_header["TRAP_PUMP_SHUFFLE_CTR"] = (
|
|
487
|
-
|
|
523
|
+
primary_header["TRAP_PUMP_SHUFFLE_CTR"] = (
|
|
524
|
+
self.register_map["Trap_Pumping_Shuffle_counter"],
|
|
525
|
+
"Trap pumping shuffle counter",
|
|
526
|
+
)
|
|
488
527
|
|
|
489
528
|
# primary_header["FOCALLEN"] = (FOV_SETTINGS["FOCAL_LENGTH"], "Focal length [mm]")
|
|
490
529
|
|
|
@@ -497,40 +536,50 @@ class FITS(PersistenceLayer):
|
|
|
497
536
|
primary_header["SETUP"] = (self.setup_id, "Setup ID")
|
|
498
537
|
primary_header["CCD_READOUT_ORDER"] = (str(self.ccd_readout_order), "Transmitted CCDs")
|
|
499
538
|
primary_header["CYCLETIME"] = (self.cycle_time, "Image cycle time [s]")
|
|
500
|
-
primary_header["READTIME"] = (
|
|
501
|
-
|
|
539
|
+
primary_header["READTIME"] = (
|
|
540
|
+
self.readout_time,
|
|
541
|
+
"Time needed to read out the requested part for a single CCD side [s]",
|
|
542
|
+
)
|
|
502
543
|
|
|
503
544
|
if self.register_map["sync_sel"] == 1: # See https://github.com/IvS-KULeuven/plato-common-egse/issues/2475
|
|
504
|
-
primary_header["READPERIOD"] = (
|
|
505
|
-
|
|
545
|
+
primary_header["READPERIOD"] = (
|
|
546
|
+
self.cycle_time + INT_SYNC_TIMING_OFFSET,
|
|
547
|
+
"Time between frames [s] (internal sync)",
|
|
548
|
+
)
|
|
506
549
|
texp_cmd = self.cycle_time - self.readout_time
|
|
507
550
|
primary_header["TEXP_CMD"] = (texp_cmd, "Commanded exposure time [s] (internal sync)")
|
|
508
|
-
primary_header["TEXP_EFF"] = (
|
|
509
|
-
|
|
551
|
+
primary_header["TEXP_EFF"] = (
|
|
552
|
+
texp_cmd + INT_SYNC_TIMING_OFFSET,
|
|
553
|
+
"Effective exposure time [s] (internal sync)",
|
|
554
|
+
)
|
|
510
555
|
|
|
511
556
|
primary_header["CGSE"] = (self.cgse_version, "Version of the Common EGSE")
|
|
512
557
|
|
|
513
558
|
LOGGER.info(f"Obsid in FITS persistence layer: {self.obsid}")
|
|
514
559
|
|
|
515
560
|
if self.obsid is not None:
|
|
516
|
-
|
|
517
561
|
LOGGER.debug(f"{self.obsid = }")
|
|
518
562
|
|
|
519
563
|
primary_header["OBSID"] = (self.obsid, "Observation identifier")
|
|
520
564
|
|
|
521
|
-
primary_header["PRE_SC_N"] = (
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
565
|
+
primary_header["PRE_SC_N"] = (
|
|
566
|
+
CCD_SETTINGS.LENGTH_SERIAL_PRESCAN,
|
|
567
|
+
"Number of pixels/columns in the serial pre-scan",
|
|
568
|
+
)
|
|
569
|
+
primary_header["OVR_SC_N"] = (
|
|
570
|
+
max(0, self.h_end + 1 - CCD_SETTINGS.LENGTH_SERIAL_PRESCAN - CCD_SETTINGS.NUM_COLUMNS // 2),
|
|
571
|
+
"Number of virtual pixels / columns in the serial over-scan",
|
|
572
|
+
)
|
|
573
|
+
primary_header["OVR_SC_R"] = (
|
|
574
|
+
max(0, self.v_end + 1 - CCD_SETTINGS.NUM_ROWS),
|
|
575
|
+
"Number of rows in the parallel over-scan",
|
|
576
|
+
)
|
|
528
577
|
primary_header["IMG_REPR"] = ("FOV_IMG", "Right CCD side flipped w.r.t. readout direction")
|
|
529
578
|
|
|
530
579
|
return primary_header
|
|
531
580
|
|
|
532
581
|
def create_base_image_wcs(self):
|
|
533
|
-
"""
|
|
582
|
+
"""Create a basic FITS header for the image.
|
|
534
583
|
|
|
535
584
|
Not all information can be filled out at this point (i.c. extension, CCD identifier, number of columns,
|
|
536
585
|
rotation, reference pixel). This will be done at the moment the serial pre-scan will be written to file.
|
|
@@ -553,29 +602,55 @@ class FITS(PersistenceLayer):
|
|
|
553
602
|
|
|
554
603
|
num_rows = min(CCD_SETTINGS["NUM_ROWS"] - 1, self.v_end) - self.v_start + 1
|
|
555
604
|
|
|
556
|
-
image_header["NAXIS"] = (
|
|
557
|
-
|
|
558
|
-
|
|
605
|
+
image_header["NAXIS"] = (
|
|
606
|
+
2,
|
|
607
|
+
"Dimensionality of the image",
|
|
608
|
+
)
|
|
609
|
+
image_header["NAXIS2"] = (
|
|
610
|
+
num_rows,
|
|
611
|
+
"Number of rows in the image",
|
|
612
|
+
)
|
|
559
613
|
|
|
560
614
|
# Focal length (this is needed for the conversion to field angles)
|
|
561
615
|
|
|
562
|
-
image_header["FOCALLEN"] = (
|
|
616
|
+
image_header["FOCALLEN"] = (
|
|
617
|
+
FOV_SETTINGS["FOCAL_LENGTH"],
|
|
618
|
+
"Focal length [mm]",
|
|
619
|
+
) # TODO
|
|
563
620
|
|
|
564
621
|
# Linear coordinate transformation from sub-field to focal-plane coordinates
|
|
565
622
|
|
|
566
|
-
image_header["ctype1"] = (
|
|
567
|
-
|
|
623
|
+
image_header["ctype1"] = (
|
|
624
|
+
"LINEAR",
|
|
625
|
+
"Linear coordinate transformation",
|
|
626
|
+
)
|
|
627
|
+
image_header["ctype2"] = (
|
|
628
|
+
"LINEAR",
|
|
629
|
+
"Linear coordinate transformation",
|
|
630
|
+
)
|
|
568
631
|
|
|
569
632
|
# Focal-plane coordinates are expressed in mm
|
|
570
633
|
|
|
571
|
-
image_header["CUNIT1"] = (
|
|
572
|
-
|
|
634
|
+
image_header["CUNIT1"] = (
|
|
635
|
+
"MM",
|
|
636
|
+
"Target unit in the column direction (mm)",
|
|
637
|
+
)
|
|
638
|
+
image_header["CUNIT2"] = (
|
|
639
|
+
"MM",
|
|
640
|
+
"Target unit in the row direction (mm)",
|
|
641
|
+
)
|
|
573
642
|
|
|
574
643
|
# Pixel size
|
|
575
644
|
|
|
576
645
|
cdelt = CCD_SETTINGS["PIXEL_SIZE"] / 1000.0 # Pixel size [mm]
|
|
577
|
-
image_header["CDELT1"] = (
|
|
578
|
-
|
|
646
|
+
image_header["CDELT1"] = (
|
|
647
|
+
cdelt,
|
|
648
|
+
"Pixel size in the x-direction [mm]",
|
|
649
|
+
)
|
|
650
|
+
image_header["CDELT2"] = (
|
|
651
|
+
cdelt,
|
|
652
|
+
"Pixel size in the y-direction [mm]",
|
|
653
|
+
)
|
|
579
654
|
|
|
580
655
|
# Additional keywords
|
|
581
656
|
|
|
@@ -593,7 +668,7 @@ class FITS(PersistenceLayer):
|
|
|
593
668
|
return image_header
|
|
594
669
|
|
|
595
670
|
def create_base_serial_prescan_wcs(self):
|
|
596
|
-
"""
|
|
671
|
+
"""Create a basic FITS header for the serial pre-scan.
|
|
597
672
|
|
|
598
673
|
Not all information can be filled out at this point (i.c. extension, CCD identifier). This will be
|
|
599
674
|
done at the moment the serial pre-scan will be written to file.
|
|
@@ -616,9 +691,18 @@ class FITS(PersistenceLayer):
|
|
|
616
691
|
num_rows = self.v_end - self.v_start + 1
|
|
617
692
|
num_columns = CCD_SETTINGS.LENGTH_SERIAL_PRESCAN
|
|
618
693
|
|
|
619
|
-
serial_prescan_wcs["NAXIS"] = (
|
|
620
|
-
|
|
621
|
-
|
|
694
|
+
serial_prescan_wcs["NAXIS"] = (
|
|
695
|
+
2,
|
|
696
|
+
"Dimensionality of the serial pre-scan",
|
|
697
|
+
)
|
|
698
|
+
serial_prescan_wcs["NAXIS1"] = (
|
|
699
|
+
num_columns,
|
|
700
|
+
"Number of columns in the serial pre-scan",
|
|
701
|
+
)
|
|
702
|
+
serial_prescan_wcs["NAXIS2"] = (
|
|
703
|
+
num_rows,
|
|
704
|
+
"Number of rows in the serial pre-scan",
|
|
705
|
+
)
|
|
622
706
|
|
|
623
707
|
serial_prescan_wcs["TELESCOP"] = "PLATO"
|
|
624
708
|
if self.camera_id:
|
|
@@ -631,7 +715,7 @@ class FITS(PersistenceLayer):
|
|
|
631
715
|
return serial_prescan_wcs
|
|
632
716
|
|
|
633
717
|
def create_base_serial_overscan_wcs(self):
|
|
634
|
-
"""
|
|
718
|
+
"""Create a basic FITS header for the serial over-scan.
|
|
635
719
|
|
|
636
720
|
Not all information can be filled out at this point (i.c. extension, CCD identifier). This will be
|
|
637
721
|
done at the moment the serial over-scan will be written to file.
|
|
@@ -654,9 +738,18 @@ class FITS(PersistenceLayer):
|
|
|
654
738
|
num_rows = self.v_end - self.v_start + 1
|
|
655
739
|
num_columns = self.v_end + 1 - CCD_SETTINGS.LENGTH_SERIAL_PRESCAN - CCD_SETTINGS.NUM_COLUMNS / 2
|
|
656
740
|
|
|
657
|
-
serial_overscan_wcs["NAXIS"] = (
|
|
658
|
-
|
|
659
|
-
|
|
741
|
+
serial_overscan_wcs["NAXIS"] = (
|
|
742
|
+
2,
|
|
743
|
+
"Dimensionality of the serial over-scan",
|
|
744
|
+
)
|
|
745
|
+
serial_overscan_wcs["NAXIS1"] = (
|
|
746
|
+
num_columns,
|
|
747
|
+
"Number of columns in the serial over-scan",
|
|
748
|
+
)
|
|
749
|
+
serial_overscan_wcs["NAXIS2"] = (
|
|
750
|
+
num_rows,
|
|
751
|
+
"Number of rows in the serial over-scan",
|
|
752
|
+
)
|
|
660
753
|
|
|
661
754
|
# Site name
|
|
662
755
|
|
|
@@ -671,7 +764,7 @@ class FITS(PersistenceLayer):
|
|
|
671
764
|
return serial_overscan_wcs
|
|
672
765
|
|
|
673
766
|
def create_base_parallel_overscan_wcs(self):
|
|
674
|
-
"""
|
|
767
|
+
"""Create a basic FITS header for the parallel over-scan.
|
|
675
768
|
|
|
676
769
|
Not all information can be filled out at this point (i.c. extension, CCD identifier, number of columns). This
|
|
677
770
|
will be done at the moment the parallel over-scan will be written to file.
|
|
@@ -694,8 +787,14 @@ class FITS(PersistenceLayer):
|
|
|
694
787
|
|
|
695
788
|
num_rows_parallel_overscan = max(0, self.v_end - CCD_SETTINGS.NUM_ROWS + 1)
|
|
696
789
|
|
|
697
|
-
parallel_overscan_wcs["NAXIS"] = (
|
|
698
|
-
|
|
790
|
+
parallel_overscan_wcs["NAXIS"] = (
|
|
791
|
+
2,
|
|
792
|
+
"Dimensionality of the parallel over-scan",
|
|
793
|
+
)
|
|
794
|
+
parallel_overscan_wcs["NAXIS2"] = (
|
|
795
|
+
num_rows_parallel_overscan,
|
|
796
|
+
"Number of rows in the parallel over-scan",
|
|
797
|
+
)
|
|
699
798
|
|
|
700
799
|
# Site name
|
|
701
800
|
|
|
@@ -710,7 +809,6 @@ class FITS(PersistenceLayer):
|
|
|
710
809
|
return parallel_overscan_wcs
|
|
711
810
|
|
|
712
811
|
def open(self, mode=None):
|
|
713
|
-
|
|
714
812
|
primary_header = self.create_primary_header()
|
|
715
813
|
|
|
716
814
|
# The primary HDU contains only this header and no image data
|
|
@@ -731,7 +829,6 @@ class FITS(PersistenceLayer):
|
|
|
731
829
|
self.is_fits_file_open = False
|
|
732
830
|
|
|
733
831
|
def exists(self):
|
|
734
|
-
|
|
735
832
|
return self._filepath.exists()
|
|
736
833
|
|
|
737
834
|
def create(self, data: dict):
|
|
@@ -744,24 +841,21 @@ class FITS(PersistenceLayer):
|
|
|
744
841
|
"""
|
|
745
842
|
|
|
746
843
|
for key, spw_packet in data.items():
|
|
747
|
-
|
|
748
844
|
# if isinstance(spw_packet, TimecodePacket):
|
|
749
845
|
#
|
|
750
846
|
# self.timestamp = spw_packet.
|
|
751
847
|
|
|
752
848
|
if key == "Timestamp":
|
|
753
|
-
|
|
754
849
|
self.timestamp = spw_packet
|
|
755
850
|
self.finetime = time_since_epoch_1958(self.timestamp)
|
|
756
851
|
|
|
757
852
|
elif isinstance(spw_packet, DataPacket):
|
|
758
|
-
|
|
759
853
|
try:
|
|
760
854
|
ccd_bin_to_id = self.setup.camera.fee.ccd_numbering.CCD_BIN_TO_ID
|
|
761
855
|
except AttributeError:
|
|
762
856
|
raise SetupError("No entry in the setup for camera.fee.ccd_numbering.CCD_BIN_TO_ID")
|
|
763
857
|
spw_packet_data_type = spw_packet.type
|
|
764
|
-
ccd_number = ccd_bin_to_id[spw_packet_data_type.ccd_number]
|
|
858
|
+
ccd_number = ccd_bin_to_id[spw_packet_data_type.ccd_number] # 1-4
|
|
765
859
|
ccd_side = self.fee_side(spw_packet_data_type.ccd_side)
|
|
766
860
|
data_array = spw_packet.data_as_ndarray
|
|
767
861
|
|
|
@@ -778,11 +872,9 @@ class FITS(PersistenceLayer):
|
|
|
778
872
|
# - add to the FITS file.
|
|
779
873
|
|
|
780
874
|
if self.is_all_data_received(spw_packet_data_type, ccd_number, ccd_side):
|
|
781
|
-
|
|
782
875
|
# Write the information to FITS
|
|
783
876
|
|
|
784
877
|
for ccd_side in self.fee_side:
|
|
785
|
-
|
|
786
878
|
try:
|
|
787
879
|
if self.data_arrays[ccd_side][ccd_number].size > 0:
|
|
788
880
|
self.assemble_slice(ccd_number, ccd_side)
|
|
@@ -795,7 +887,7 @@ class FITS(PersistenceLayer):
|
|
|
795
887
|
self.clear_for_next_exposure(ccd_number, ccd_side)
|
|
796
888
|
|
|
797
889
|
def is_all_data_received(self, spw_packet_data_type: DataPacketType, ccd_number: int, ccd_side):
|
|
798
|
-
"""
|
|
890
|
+
"""Check if all data has been received for the current exposure.
|
|
799
891
|
|
|
800
892
|
Args:
|
|
801
893
|
- spw_packet_data_type: Last received data packet type.
|
|
@@ -808,33 +900,26 @@ class FITS(PersistenceLayer):
|
|
|
808
900
|
from egse.dpu import got_all_last_packets
|
|
809
901
|
|
|
810
902
|
if spw_packet_data_type.last_packet:
|
|
811
|
-
|
|
812
903
|
packet_type = spw_packet_data_type.packet_type
|
|
813
904
|
|
|
814
905
|
if packet_type == PacketType.DATA_PACKET:
|
|
815
|
-
|
|
816
906
|
if ccd_side == self.fee_side.E_SIDE:
|
|
817
|
-
|
|
818
907
|
self.received_last_packet_flags[ccd_number][0] = True
|
|
819
908
|
|
|
820
909
|
elif ccd_side == self.fee_side.F_SIDE:
|
|
821
|
-
|
|
822
910
|
self.received_last_packet_flags[ccd_number][1] = True
|
|
823
911
|
|
|
824
912
|
elif packet_type == PacketType.OVERSCAN_DATA:
|
|
825
|
-
|
|
826
913
|
if ccd_side == self.fee_side.E_SIDE:
|
|
827
|
-
|
|
828
914
|
self.received_last_packet_flags[ccd_number][2] = True
|
|
829
915
|
|
|
830
916
|
elif ccd_side == self.fee_side.F_SIDE:
|
|
831
|
-
|
|
832
917
|
self.received_last_packet_flags[ccd_number][3] = True
|
|
833
918
|
|
|
834
919
|
return got_all_last_packets(self.received_last_packet_flags[ccd_number], self.expected_last_packet_flags)
|
|
835
920
|
|
|
836
921
|
def got_all_last_packets(self, ccd_number: int, ccd_side):
|
|
837
|
-
"""
|
|
922
|
+
"""Check whether all the expected last-packet flags have been seen for the given CCD side.
|
|
838
923
|
|
|
839
924
|
Args:
|
|
840
925
|
- ccd_number: CCD from which the last received data originates (1-4).
|
|
@@ -847,19 +932,21 @@ class FITS(PersistenceLayer):
|
|
|
847
932
|
received_last_packet_flags = self.received_last_packet_flags[ccd_number]
|
|
848
933
|
|
|
849
934
|
if ccd_side == self.fee_side.E_SIDE:
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
935
|
+
return (
|
|
936
|
+
received_last_packet_flags[0] == self.expected_last_packet_flags[0]
|
|
937
|
+
and received_last_packet_flags[2] == self.expected_last_packet_flags[2]
|
|
938
|
+
)
|
|
853
939
|
|
|
854
940
|
elif ccd_side == self.fee_side.F_SIDE:
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
941
|
+
return (
|
|
942
|
+
received_last_packet_flags[1] == self.expected_last_packet_flags[1]
|
|
943
|
+
and received_last_packet_flags[3] == self.expected_last_packet_flags[3]
|
|
944
|
+
)
|
|
858
945
|
|
|
859
946
|
return False
|
|
860
947
|
|
|
861
948
|
def assemble_slice(self, ccd_number: int, ccd_side):
|
|
862
|
-
"""
|
|
949
|
+
"""Assemble the data for the given CCD and write it to FITS.
|
|
863
950
|
|
|
864
951
|
Args:
|
|
865
952
|
- ccd_number: CCD identifier (1/2/3/4).
|
|
@@ -868,14 +955,12 @@ class FITS(PersistenceLayer):
|
|
|
868
955
|
# Windowing (pattern) mode
|
|
869
956
|
|
|
870
957
|
if self.is_windowing_mode:
|
|
871
|
-
|
|
872
958
|
# self.assemble_slice_windowing_mode(ccd_number, ccd_side)
|
|
873
959
|
pass
|
|
874
960
|
|
|
875
961
|
# Full-image (pattern) mode
|
|
876
962
|
|
|
877
963
|
else:
|
|
878
|
-
|
|
879
964
|
self.assemble_slice_full_image_mode(ccd_number, ccd_side)
|
|
880
965
|
|
|
881
966
|
# def assemble_slice_windowing_mode(self, ccd_number: int):
|
|
@@ -959,7 +1044,7 @@ class FITS(PersistenceLayer):
|
|
|
959
1044
|
# self.append_image(image, ccd_number)
|
|
960
1045
|
|
|
961
1046
|
def assemble_slice_full_image_mode(self, ccd_number: int, ccd_side):
|
|
962
|
-
"""
|
|
1047
|
+
"""Assemble the data for the given CCD and write it to FITS, for full-image mode or full-image pattern mode.
|
|
963
1048
|
|
|
964
1049
|
This consists of the following steps:
|
|
965
1050
|
|
|
@@ -986,13 +1071,16 @@ class FITS(PersistenceLayer):
|
|
|
986
1071
|
# Re-shape the 1D array to a 2D array and extract the image and scan maps
|
|
987
1072
|
|
|
988
1073
|
try:
|
|
989
|
-
data_array = np.reshape(self.data_arrays[ccd_side][ccd_number], (num_rows, num_columns))
|
|
1074
|
+
data_array = np.reshape(self.data_arrays[ccd_side][ccd_number], (num_rows, num_columns)) # 1D -> 2D
|
|
990
1075
|
except ValueError as exc:
|
|
991
|
-
raise MissingSpWPacketsError(
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1076
|
+
raise MissingSpWPacketsError(
|
|
1077
|
+
f"Incomplete SpW data for frame {self.frame_number[ccd_number][ccd_side]} "
|
|
1078
|
+
f"for the {ccd_side.name[0]}-side off CCD{ccd_number}. Check the DPU "
|
|
1079
|
+
f"Processing logs for more information."
|
|
1080
|
+
) from exc
|
|
1081
|
+
side_image, side_serial_prescan, side_serial_overscan, side_parallel_overscan = self.extract_full_image_mode(
|
|
1082
|
+
data_array
|
|
1083
|
+
)
|
|
996
1084
|
|
|
997
1085
|
# Serial pre-scan
|
|
998
1086
|
|
|
@@ -1001,13 +1089,11 @@ class FITS(PersistenceLayer):
|
|
|
1001
1089
|
# Serial over-scan
|
|
1002
1090
|
|
|
1003
1091
|
if self.has_serial_overscan:
|
|
1004
|
-
|
|
1005
1092
|
self.append_serial_overscan(side_serial_overscan, ccd_number, ccd_side)
|
|
1006
1093
|
|
|
1007
1094
|
# Parallel over-scan
|
|
1008
1095
|
|
|
1009
1096
|
if self.has_parallel_overscan:
|
|
1010
|
-
|
|
1011
1097
|
self.append_parallel_overscan(side_parallel_overscan, ccd_number, ccd_side)
|
|
1012
1098
|
|
|
1013
1099
|
# Image
|
|
@@ -1037,42 +1123,40 @@ class FITS(PersistenceLayer):
|
|
|
1037
1123
|
# the row index of the first transmitted parallel over-scan row).
|
|
1038
1124
|
|
|
1039
1125
|
start_column_image = CCD_SETTINGS.LENGTH_SERIAL_PRESCAN
|
|
1040
|
-
end_column_image_plus_1 = min(
|
|
1126
|
+
end_column_image_plus_1 = min(
|
|
1127
|
+
CCD_SETTINGS.LENGTH_SERIAL_PRESCAN + CCD_SETTINGS.NUM_COLUMNS // 2, self.h_end + 1
|
|
1128
|
+
)
|
|
1041
1129
|
end_row_image_plus_1 = min(CCD_SETTINGS.NUM_ROWS - 1, self.v_end) - self.v_start + 1
|
|
1042
1130
|
|
|
1043
1131
|
# Serial pre-scan (all rows, but only the first couple of rows)
|
|
1044
1132
|
|
|
1045
|
-
serial_prescan = data_array[:, 0:
|
|
1133
|
+
serial_prescan = data_array[:, 0:start_column_image]
|
|
1046
1134
|
|
|
1047
1135
|
# Serial over-scan (all rows, omit the columns from the serial pre-scan and the serial over-scan)
|
|
1048
1136
|
|
|
1049
1137
|
if self.has_serial_overscan:
|
|
1050
|
-
|
|
1051
1138
|
serial_overscan = data_array[:, end_column_image_plus_1:]
|
|
1052
1139
|
|
|
1053
1140
|
else:
|
|
1054
|
-
|
|
1055
1141
|
serial_overscan = None
|
|
1056
1142
|
|
|
1057
1143
|
# Image (omit the rows from the parallel over-scan
|
|
1058
1144
|
# and the columns from the serial pre-scan and the serial over-scan)
|
|
1059
1145
|
|
|
1060
|
-
image = data_array[0:
|
|
1146
|
+
image = data_array[0:end_row_image_plus_1, start_column_image:end_column_image_plus_1]
|
|
1061
1147
|
|
|
1062
1148
|
# Parallel over-scan
|
|
1063
1149
|
|
|
1064
1150
|
if self.has_parallel_overscan:
|
|
1065
|
-
|
|
1066
|
-
parallel_overscan = data_array[end_row_image_plus_1:, start_column_image: end_column_image_plus_1]
|
|
1151
|
+
parallel_overscan = data_array[end_row_image_plus_1:, start_column_image:end_column_image_plus_1]
|
|
1067
1152
|
|
|
1068
1153
|
else:
|
|
1069
|
-
|
|
1070
1154
|
parallel_overscan = None
|
|
1071
1155
|
|
|
1072
1156
|
return image, serial_prescan, serial_overscan, parallel_overscan
|
|
1073
1157
|
|
|
1074
1158
|
def append_serial_prescan(self, serial_prescan, ccd_number: int, ccd_side: str):
|
|
1075
|
-
"""
|
|
1159
|
+
"""Append the given serial pre-scan to the FITS file (after completing its header).
|
|
1076
1160
|
|
|
1077
1161
|
Args:
|
|
1078
1162
|
- serial_prescan: Serial pre-scan.
|
|
@@ -1083,10 +1167,19 @@ class FITS(PersistenceLayer):
|
|
|
1083
1167
|
extension = f"SPRE_{ccd_number}_{self.fee_side(ccd_side).name[0]}"
|
|
1084
1168
|
self.serial_prescan_header["EXTNAME"] = extension
|
|
1085
1169
|
self.serial_prescan_header["EXTVER"] = self.frame_number[ccd_number][ccd_side]
|
|
1086
|
-
self.serial_prescan_header["CCD_ID"] = (
|
|
1170
|
+
self.serial_prescan_header["CCD_ID"] = (
|
|
1171
|
+
ccd_number,
|
|
1172
|
+
"CCD identifier",
|
|
1173
|
+
)
|
|
1087
1174
|
self.serial_prescan_header["SENSOR_SEL"] = (self.fee_side(ccd_side).name[0], "CCD side")
|
|
1088
|
-
self.serial_prescan_header["DATE-OBS"] = (
|
|
1089
|
-
|
|
1175
|
+
self.serial_prescan_header["DATE-OBS"] = (
|
|
1176
|
+
self.timestamp,
|
|
1177
|
+
"Timestamp for 1st frame",
|
|
1178
|
+
)
|
|
1179
|
+
self.serial_prescan_header["FINETIME"] = (
|
|
1180
|
+
self.finetime,
|
|
1181
|
+
"Finetime representation of DATE-OBS",
|
|
1182
|
+
)
|
|
1090
1183
|
|
|
1091
1184
|
if ccd_side in [self.fee_side.RIGHT_SIDE.name, self.fee_side.RIGHT_SIDE]:
|
|
1092
1185
|
serial_prescan = np.fliplr(serial_prescan)
|
|
@@ -1094,7 +1187,7 @@ class FITS(PersistenceLayer):
|
|
|
1094
1187
|
fits.append(str(self._filepath), serial_prescan, self.serial_prescan_header)
|
|
1095
1188
|
|
|
1096
1189
|
def append_serial_overscan(self, serial_overscan, ccd_number: int, ccd_side: str):
|
|
1097
|
-
"""
|
|
1190
|
+
"""Append the given serial over-scan to the FITS file (after completing its header).
|
|
1098
1191
|
|
|
1099
1192
|
Args:
|
|
1100
1193
|
- serial_overscan: Serial over-scan.
|
|
@@ -1105,10 +1198,19 @@ class FITS(PersistenceLayer):
|
|
|
1105
1198
|
extension = f"SOVER_{ccd_number}_{self.fee_side(ccd_side).name[0]}"
|
|
1106
1199
|
self.serial_overscan_header["EXTNAME"] = extension
|
|
1107
1200
|
self.serial_overscan_header["EXTVER"] = self.frame_number[ccd_number][ccd_side]
|
|
1108
|
-
self.serial_overscan_header["CCD_ID"] = (
|
|
1201
|
+
self.serial_overscan_header["CCD_ID"] = (
|
|
1202
|
+
ccd_number,
|
|
1203
|
+
"CCD identifier",
|
|
1204
|
+
)
|
|
1109
1205
|
self.serial_overscan_header["SENSOR_SEL"] = (self.fee_side(ccd_side).name[0], "CCD side")
|
|
1110
|
-
self.serial_overscan_header["NAXIS1"] = (
|
|
1111
|
-
|
|
1206
|
+
self.serial_overscan_header["NAXIS1"] = (
|
|
1207
|
+
serial_overscan.shape[1],
|
|
1208
|
+
"Number of columns in the serial over-scan",
|
|
1209
|
+
)
|
|
1210
|
+
self.serial_overscan_header["DATE-OBS"] = (
|
|
1211
|
+
self.timestamp,
|
|
1212
|
+
"Timestamp for 1st frame",
|
|
1213
|
+
)
|
|
1112
1214
|
self.serial_overscan_header["FINETIME"] = (self.finetime, "Finetime representation of DATE-OBS")
|
|
1113
1215
|
|
|
1114
1216
|
if ccd_side in [self.fee_side.RIGHT_SIDE.name, self.fee_side.RIGHT_SIDE]:
|
|
@@ -1117,7 +1219,7 @@ class FITS(PersistenceLayer):
|
|
|
1117
1219
|
fits.append(str(self._filepath), serial_overscan, self.serial_overscan_header)
|
|
1118
1220
|
|
|
1119
1221
|
def append_parallel_overscan(self, parallel_overscan, ccd_number: int, ccd_side: str):
|
|
1120
|
-
"""
|
|
1222
|
+
"""Append the given parallel over-scan to the FITS file (after completing its header).
|
|
1121
1223
|
|
|
1122
1224
|
Args:
|
|
1123
1225
|
- parallel_overscan: Parallel over-scan.
|
|
@@ -1128,12 +1230,23 @@ class FITS(PersistenceLayer):
|
|
|
1128
1230
|
extension = f"POVER_{ccd_number}_{self.fee_side(ccd_side).name[0]}"
|
|
1129
1231
|
self.parallel_overscan_header["EXTNAME"] = extension
|
|
1130
1232
|
self.parallel_overscan_header["EXTVER"] = self.frame_number[ccd_number][ccd_side]
|
|
1131
|
-
self.parallel_overscan_header["CCD_ID"] = (
|
|
1233
|
+
self.parallel_overscan_header["CCD_ID"] = (
|
|
1234
|
+
ccd_number,
|
|
1235
|
+
"CCD identifier",
|
|
1236
|
+
)
|
|
1132
1237
|
self.parallel_overscan_header["SENSOR_SEL"] = (self.fee_side(ccd_side).name[0], "CCD side")
|
|
1133
|
-
self.parallel_overscan_header["NAXIS1"] = (
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1238
|
+
self.parallel_overscan_header["NAXIS1"] = (
|
|
1239
|
+
parallel_overscan.shape[1],
|
|
1240
|
+
"Number of columns in the parallel over-scan",
|
|
1241
|
+
)
|
|
1242
|
+
self.parallel_overscan_header["DATE-OBS"] = (
|
|
1243
|
+
self.timestamp,
|
|
1244
|
+
"Timestamp for 1st frame",
|
|
1245
|
+
)
|
|
1246
|
+
self.parallel_overscan_header["FINETIME"] = (
|
|
1247
|
+
self.finetime,
|
|
1248
|
+
"Finetime representation of DATE-OBS",
|
|
1249
|
+
)
|
|
1137
1250
|
|
|
1138
1251
|
if ccd_side in [self.fee_side.RIGHT_SIDE.name, self.fee_side.RIGHT_SIDE]:
|
|
1139
1252
|
parallel_overscan = np.fliplr(parallel_overscan)
|
|
@@ -1141,7 +1254,7 @@ class FITS(PersistenceLayer):
|
|
|
1141
1254
|
fits.append(str(self._filepath), parallel_overscan, self.parallel_overscan_header)
|
|
1142
1255
|
|
|
1143
1256
|
def append_image(self, image, ccd_number: int, ccd_side: str):
|
|
1144
|
-
"""
|
|
1257
|
+
"""Append the given image to the FITS file (after completing its header).
|
|
1145
1258
|
|
|
1146
1259
|
Args:
|
|
1147
1260
|
- image: Image.
|
|
@@ -1152,46 +1265,82 @@ class FITS(PersistenceLayer):
|
|
|
1152
1265
|
extension = f"IMAGE_{ccd_number}_{self.fee_side(ccd_side).name[0]}"
|
|
1153
1266
|
self.image_header["EXTNAME"] = extension
|
|
1154
1267
|
self.image_header["EXTVER"] = self.frame_number[ccd_number][ccd_side]
|
|
1155
|
-
self.image_header["CCD_ID"] = (
|
|
1268
|
+
self.image_header["CCD_ID"] = (
|
|
1269
|
+
ccd_number,
|
|
1270
|
+
"CCD identifier",
|
|
1271
|
+
)
|
|
1156
1272
|
self.image_header["SENSOR_SEL"] = (self.fee_side(ccd_side).name[0], "CCD side")
|
|
1157
|
-
self.image_header["NAXIS1"] = (
|
|
1273
|
+
self.image_header["NAXIS1"] = (
|
|
1274
|
+
image.shape[1],
|
|
1275
|
+
"Number of columns in the image",
|
|
1276
|
+
)
|
|
1158
1277
|
|
|
1159
1278
|
ccd_orientation_degrees = CCD_SETTINGS.ORIENTATION[ccd_number - 1]
|
|
1160
1279
|
ccd_orientation_radians = radians(ccd_orientation_degrees)
|
|
1161
1280
|
|
|
1162
|
-
self.image_header["CROTA2"] = (
|
|
1281
|
+
self.image_header["CROTA2"] = (
|
|
1282
|
+
ccd_orientation_degrees,
|
|
1283
|
+
"CCD orientation angle [degrees]",
|
|
1284
|
+
)
|
|
1163
1285
|
|
|
1164
1286
|
cdelt = CCD_SETTINGS["PIXEL_SIZE"] / 1000.0 # Pixel size [mm]
|
|
1165
1287
|
|
|
1166
|
-
self.image_header["CD1_1"] = (
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
self.image_header["
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1288
|
+
self.image_header["CD1_1"] = (
|
|
1289
|
+
cdelt * cos(ccd_orientation_radians),
|
|
1290
|
+
"Pixel size x cos(CCD orientation angle)",
|
|
1291
|
+
)
|
|
1292
|
+
self.image_header["CD1_2"] = (
|
|
1293
|
+
-cdelt * sin(ccd_orientation_radians),
|
|
1294
|
+
"-Pixel size x sin(CCD orientation angle)",
|
|
1295
|
+
)
|
|
1296
|
+
self.image_header["CD2_1"] = (
|
|
1297
|
+
cdelt * sin(ccd_orientation_radians),
|
|
1298
|
+
"Pixel size x sin(CCD orientation angle)",
|
|
1299
|
+
)
|
|
1300
|
+
self.image_header["CD2_2"] = (
|
|
1301
|
+
cdelt * cos(ccd_orientation_radians),
|
|
1302
|
+
"Pixel size x cos(CCD orientation angle)",
|
|
1303
|
+
)
|
|
1174
1304
|
|
|
1175
1305
|
zeropoint_x, zeropoint_y = -np.array(CCD_SETTINGS.ZEROPOINT)
|
|
1176
1306
|
crval1 = zeropoint_x * cos(ccd_orientation_radians) - zeropoint_y * sin(ccd_orientation_radians)
|
|
1177
1307
|
crval2 = zeropoint_x * sin(ccd_orientation_radians) + zeropoint_y * cos(ccd_orientation_radians)
|
|
1178
1308
|
|
|
1179
|
-
self.image_header["CRVAL1"] = (
|
|
1180
|
-
|
|
1309
|
+
self.image_header["CRVAL1"] = (
|
|
1310
|
+
crval1,
|
|
1311
|
+
"FP x-coordinate of the CCD origin [mm]",
|
|
1312
|
+
)
|
|
1313
|
+
self.image_header["CRVAL2"] = (
|
|
1314
|
+
crval2,
|
|
1315
|
+
"FP y-coordinate of the CCD origin [mm]",
|
|
1316
|
+
)
|
|
1181
1317
|
|
|
1182
|
-
self.image_header["CRPIX2"] = (
|
|
1183
|
-
|
|
1318
|
+
self.image_header["CRPIX2"] = (
|
|
1319
|
+
-self.v_start,
|
|
1320
|
+
"CCD origin row wrt 1st transmitted row",
|
|
1321
|
+
)
|
|
1184
1322
|
|
|
1185
1323
|
if ccd_side in [self.fee_side.LEFT_SIDE.name, self.fee_side.LEFT_SIDE]:
|
|
1186
|
-
self.image_header["CRPIX1"] = (
|
|
1324
|
+
self.image_header["CRPIX1"] = (
|
|
1325
|
+
0,
|
|
1326
|
+
"CCD origin column wrt lower left corner",
|
|
1327
|
+
)
|
|
1187
1328
|
|
|
1188
1329
|
if ccd_side in [self.fee_side.RIGHT_SIDE.name, self.fee_side.RIGHT_SIDE]:
|
|
1189
1330
|
image = np.fliplr(image)
|
|
1190
|
-
self.image_header["CRPIX1"] = (
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1331
|
+
self.image_header["CRPIX1"] = (
|
|
1332
|
+
-CCD_SETTINGS.NUM_ROWS + image.shape[1],
|
|
1333
|
+
"CCD origin column wrt lower left corner",
|
|
1334
|
+
)
|
|
1335
|
+
|
|
1336
|
+
self.image_header["DATE-OBS"] = (
|
|
1337
|
+
self.timestamp,
|
|
1338
|
+
"Timestamp for 1st frame",
|
|
1339
|
+
)
|
|
1340
|
+
self.image_header["FINETIME"] = (
|
|
1341
|
+
self.finetime,
|
|
1342
|
+
"Finetime representation of DATE-OBS",
|
|
1343
|
+
)
|
|
1195
1344
|
|
|
1196
1345
|
fits.append(str(self._filepath), image, self.image_header)
|
|
1197
1346
|
|
|
@@ -1209,13 +1358,10 @@ class FITS(PersistenceLayer):
|
|
|
1209
1358
|
raise NotImplementedError("Persistence layers must implement a read method")
|
|
1210
1359
|
|
|
1211
1360
|
def update(self, idx, data):
|
|
1212
|
-
|
|
1213
1361
|
pass
|
|
1214
1362
|
|
|
1215
1363
|
def delete(self, idx):
|
|
1216
|
-
|
|
1217
1364
|
LOGGER.warning("The delete functionality is not implemented for the CSV persistence layer.")
|
|
1218
1365
|
|
|
1219
1366
|
def get_filepath(self):
|
|
1220
|
-
|
|
1221
1367
|
return self._filepath
|
|
File without changes
|
|
File without changes
|