cgse 2024.7.0__py3-none-any.whl → 2025.0.2__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.
Files changed (664) hide show
  1. README.md +27 -0
  2. bump.py +85 -0
  3. cgse-2025.0.2.dist-info/METADATA +38 -0
  4. cgse-2025.0.2.dist-info/RECORD +5 -0
  5. {cgse-2024.7.0.dist-info → cgse-2025.0.2.dist-info}/WHEEL +1 -2
  6. cgse-2024.7.0.dist-info/COPYING +0 -674
  7. cgse-2024.7.0.dist-info/COPYING.LESSER +0 -165
  8. cgse-2024.7.0.dist-info/METADATA +0 -144
  9. cgse-2024.7.0.dist-info/RECORD +0 -660
  10. cgse-2024.7.0.dist-info/entry_points.txt +0 -75
  11. cgse-2024.7.0.dist-info/top_level.txt +0 -2
  12. egse/__init__.py +0 -12
  13. egse/__main__.py +0 -32
  14. egse/aeu/aeu.py +0 -5238
  15. egse/aeu/aeu_awg.yaml +0 -265
  16. egse/aeu/aeu_crio.yaml +0 -273
  17. egse/aeu/aeu_cs.py +0 -627
  18. egse/aeu/aeu_devif.py +0 -321
  19. egse/aeu/aeu_main_ui.py +0 -903
  20. egse/aeu/aeu_metrics.py +0 -131
  21. egse/aeu/aeu_protocol.py +0 -463
  22. egse/aeu/aeu_psu.yaml +0 -204
  23. egse/aeu/aeu_ui.py +0 -873
  24. egse/aeu/arbdata/FccdRead.arb +0 -2
  25. egse/aeu/arbdata/FccdRead_min_points.arb +0 -2
  26. egse/aeu/arbdata/HeaterSync_FccdRead.arb +0 -2
  27. egse/aeu/arbdata/HeaterSync_ccdRead25.arb +0 -2
  28. egse/aeu/arbdata/HeaterSync_ccdRead31_25.arb +0 -2
  29. egse/aeu/arbdata/HeaterSync_ccdRead37_50.arb +0 -2
  30. egse/aeu/arbdata/HeaterSync_ccdRead43_75.arb +0 -2
  31. egse/aeu/arbdata/HeaterSync_ccdRead50.arb +0 -2
  32. egse/aeu/arbdata/Heater_FccdRead_min_points.arb +0 -2
  33. egse/aeu/arbdata/ccdRead25.arb +0 -2
  34. egse/aeu/arbdata/ccdRead25_150ms.arb +0 -2
  35. egse/aeu/arbdata/ccdRead31_25.arb +0 -2
  36. egse/aeu/arbdata/ccdRead31_25_150ms.arb +0 -2
  37. egse/aeu/arbdata/ccdRead37_50.arb +0 -2
  38. egse/aeu/arbdata/ccdRead37_50_150ms.arb +0 -2
  39. egse/aeu/arbdata/ccdRead43_75.arb +0 -2
  40. egse/aeu/arbdata/ccdRead43_75_150ms.arb +0 -2
  41. egse/aeu/arbdata/ccdRead50.arb +0 -2
  42. egse/aeu/arbdata/ccdRead50_150ms.arb +0 -2
  43. egse/alert/__init__.py +0 -1049
  44. egse/alert/alertman.yaml +0 -37
  45. egse/alert/alertman_cs.py +0 -233
  46. egse/alert/alertman_ui.py +0 -600
  47. egse/alert/gsm/beaglebone.py +0 -138
  48. egse/alert/gsm/beaglebone.yaml +0 -51
  49. egse/alert/gsm/beaglebone_cs.py +0 -108
  50. egse/alert/gsm/beaglebone_devif.py +0 -122
  51. egse/alert/gsm/beaglebone_protocol.py +0 -46
  52. egse/bits.py +0 -318
  53. egse/camera.py +0 -44
  54. egse/collimator/__init__.py +0 -0
  55. egse/collimator/fcul/__init__.py +0 -0
  56. egse/collimator/fcul/ogse.py +0 -1077
  57. egse/collimator/fcul/ogse.yaml +0 -14
  58. egse/collimator/fcul/ogse_cs.py +0 -154
  59. egse/collimator/fcul/ogse_devif.py +0 -358
  60. egse/collimator/fcul/ogse_protocol.py +0 -132
  61. egse/collimator/fcul/ogse_sim.py +0 -431
  62. egse/collimator/fcul/ogse_ui.py +0 -1108
  63. egse/command.py +0 -699
  64. egse/config.py +0 -410
  65. egse/confman/__init__.py +0 -1058
  66. egse/confman/confman.yaml +0 -70
  67. egse/confman/confman_cs.py +0 -240
  68. egse/confman/confman_ui.py +0 -381
  69. egse/confman/setup_ui.py +0 -565
  70. egse/control.py +0 -632
  71. egse/coordinates/__init__.py +0 -534
  72. egse/coordinates/avoidance.py +0 -100
  73. egse/coordinates/cslmodel.py +0 -127
  74. egse/coordinates/laser_tracker_to_dict.py +0 -122
  75. egse/coordinates/point.py +0 -707
  76. egse/coordinates/pyplot.py +0 -194
  77. egse/coordinates/referenceFrame.py +0 -1279
  78. egse/coordinates/refmodel.py +0 -737
  79. egse/coordinates/rotationMatrix.py +0 -85
  80. egse/coordinates/transform3d_addon.py +0 -419
  81. egse/csl/__init__.py +0 -50
  82. egse/csl/commanding.py +0 -78
  83. egse/csl/icons/hexapod-connected-selected.svg +0 -30
  84. egse/csl/icons/hexapod-connected.svg +0 -30
  85. egse/csl/icons/hexapod-homing-selected.svg +0 -68
  86. egse/csl/icons/hexapod-homing.svg +0 -68
  87. egse/csl/icons/hexapod-retract-selected.svg +0 -56
  88. egse/csl/icons/hexapod-retract.svg +0 -51
  89. egse/csl/icons/hexapod-zero-selected.svg +0 -56
  90. egse/csl/icons/hexapod-zero.svg +0 -56
  91. egse/csl/icons/logo-puna.svg +0 -92
  92. egse/csl/icons/stop.svg +0 -1
  93. egse/csl/initialisation.py +0 -102
  94. egse/csl/mech_pos_settings.yaml +0 -18
  95. egse/das.py +0 -1240
  96. egse/das.yaml +0 -7
  97. egse/data/conf/SETUP_CSL_00000_170620_150000.yaml +0 -5
  98. egse/data/conf/SETUP_CSL_00001_170620_151010.yaml +0 -69
  99. egse/data/conf/SETUP_CSL_00002_170620_151020.yaml +0 -69
  100. egse/data/conf/SETUP_CSL_00003_170620_151030.yaml +0 -69
  101. egse/data/conf/SETUP_CSL_00004_170620_151040.yaml +0 -69
  102. egse/data/conf/SETUP_CSL_00005_170620_151050.yaml +0 -69
  103. egse/data/conf/SETUP_CSL_00006_170620_151060.yaml +0 -69
  104. egse/data/conf/SETUP_CSL_00007_170620_151070.yaml +0 -69
  105. egse/data/conf/SETUP_CSL_00008_170620_151080.yaml +0 -75
  106. egse/data/conf/SETUP_CSL_00010_210308_083016.yaml +0 -138
  107. egse/data/conf/SETUP_INTA_00000_170620_150000.yaml +0 -4
  108. egse/data/conf/SETUP_SRON_00000_170620_150000.yaml +0 -4
  109. egse/decorators.py +0 -514
  110. egse/device.py +0 -269
  111. egse/dpu/__init__.py +0 -2698
  112. egse/dpu/ccd_ui.py +0 -514
  113. egse/dpu/dpu.py +0 -783
  114. egse/dpu/dpu.yaml +0 -153
  115. egse/dpu/dpu_cs.py +0 -272
  116. egse/dpu/dpu_ui.py +0 -671
  117. egse/dpu/fitsgen.py +0 -2096
  118. egse/dpu/fitsgen_ui.py +0 -399
  119. egse/dpu/hdf5_model.py +0 -332
  120. egse/dpu/hdf5_ui.py +0 -277
  121. egse/dpu/hdf5_viewer.py +0 -506
  122. egse/dpu/hk_ui.py +0 -468
  123. egse/dpu_commands.py +0 -81
  124. egse/dsi/__init__.py +0 -33
  125. egse/dsi/_libesl.py +0 -232
  126. egse/dsi/constants.py +0 -296
  127. egse/dsi/esl.py +0 -630
  128. egse/dsi/rmap.py +0 -444
  129. egse/dsi/rmapci.py +0 -39
  130. egse/dsi/spw.py +0 -335
  131. egse/dsi/spw_state.py +0 -29
  132. egse/dummy.py +0 -318
  133. egse/dyndummy.py +0 -179
  134. egse/env.py +0 -278
  135. egse/exceptions.py +0 -88
  136. egse/fdir/__init__.py +0 -26
  137. egse/fdir/fdir_manager.py +0 -85
  138. egse/fdir/fdir_manager.yaml +0 -37
  139. egse/fdir/fdir_manager_controller.py +0 -136
  140. egse/fdir/fdir_manager_cs.py +0 -164
  141. egse/fdir/fdir_manager_interface.py +0 -15
  142. egse/fdir/fdir_remote.py +0 -73
  143. egse/fdir/fdir_remote.yaml +0 -30
  144. egse/fdir/fdir_remote_controller.py +0 -30
  145. egse/fdir/fdir_remote_cs.py +0 -94
  146. egse/fdir/fdir_remote_interface.py +0 -9
  147. egse/fdir/fdir_remote_popup.py +0 -26
  148. egse/fee/__init__.py +0 -106
  149. egse/fee/f_fee_register.yaml +0 -43
  150. egse/fee/feesim.py +0 -914
  151. egse/fee/n_fee_hk.py +0 -768
  152. egse/fee/nfee.py +0 -188
  153. egse/filterwheel/__init__.py +0 -4
  154. egse/filterwheel/eksma/__init__.py +0 -49
  155. egse/filterwheel/eksma/fw8smc4.py +0 -657
  156. egse/filterwheel/eksma/fw8smc4.yaml +0 -121
  157. egse/filterwheel/eksma/fw8smc4_cs.py +0 -144
  158. egse/filterwheel/eksma/fw8smc4_devif.py +0 -473
  159. egse/filterwheel/eksma/fw8smc4_protocol.py +0 -82
  160. egse/filterwheel/eksma/fw8smc4_ui.py +0 -940
  161. egse/filterwheel/eksma/fw8smc5.py +0 -115
  162. egse/filterwheel/eksma/fw8smc5.yaml +0 -105
  163. egse/filterwheel/eksma/fw8smc5_controller.py +0 -307
  164. egse/filterwheel/eksma/fw8smc5_cs.py +0 -141
  165. egse/filterwheel/eksma/fw8smc5_interface.py +0 -65
  166. egse/filterwheel/eksma/fw8smc5_simulator.py +0 -29
  167. egse/filterwheel/eksma/fw8smc5_ui.py +0 -1065
  168. egse/filterwheel/eksma/testpythonfw.py +0 -215
  169. egse/fov/__init__.py +0 -65
  170. egse/fov/fov_hk.py +0 -710
  171. egse/fov/fov_ui.py +0 -859
  172. egse/fov/fov_ui_controller.py +0 -140
  173. egse/fov/fov_ui_model.py +0 -200
  174. egse/fov/fov_ui_view.py +0 -345
  175. egse/gimbal/__init__.py +0 -32
  176. egse/gimbal/symetrie/__init__.py +0 -26
  177. egse/gimbal/symetrie/alpha.py +0 -586
  178. egse/gimbal/symetrie/generic_gimbal_ui.py +0 -1521
  179. egse/gimbal/symetrie/gimbal.py +0 -877
  180. egse/gimbal/symetrie/gimbal.yaml +0 -168
  181. egse/gimbal/symetrie/gimbal_cs.py +0 -183
  182. egse/gimbal/symetrie/gimbal_protocol.py +0 -138
  183. egse/gimbal/symetrie/gimbal_ui.py +0 -361
  184. egse/gimbal/symetrie/pmac.py +0 -1006
  185. egse/gimbal/symetrie/pmac_regex.py +0 -83
  186. egse/graph.py +0 -132
  187. egse/gui/__init__.py +0 -47
  188. egse/gui/buttons.py +0 -378
  189. egse/gui/focalplane.py +0 -1285
  190. egse/gui/formatter.py +0 -10
  191. egse/gui/led.py +0 -162
  192. egse/gui/limitswitch.py +0 -143
  193. egse/gui/mechanisms.py +0 -587
  194. egse/gui/states.py +0 -148
  195. egse/gui/stripchart.py +0 -729
  196. egse/gui/styles.qss +0 -48
  197. egse/gui/switch.py +0 -112
  198. egse/h5.py +0 -274
  199. egse/help/__init__.py +0 -0
  200. egse/help/help_ui.py +0 -126
  201. egse/hexapod/__init__.py +0 -32
  202. egse/hexapod/symetrie/__init__.py +0 -137
  203. egse/hexapod/symetrie/alpha.py +0 -874
  204. egse/hexapod/symetrie/dynalpha.py +0 -1387
  205. egse/hexapod/symetrie/hexapod_ui.py +0 -1516
  206. egse/hexapod/symetrie/pmac.py +0 -1010
  207. egse/hexapod/symetrie/pmac_regex.py +0 -83
  208. egse/hexapod/symetrie/puna.py +0 -1167
  209. egse/hexapod/symetrie/puna.yaml +0 -193
  210. egse/hexapod/symetrie/puna_cs.py +0 -195
  211. egse/hexapod/symetrie/puna_protocol.py +0 -134
  212. egse/hexapod/symetrie/puna_ui.py +0 -433
  213. egse/hexapod/symetrie/punaplus.py +0 -107
  214. egse/hexapod/symetrie/zonda.py +0 -872
  215. egse/hexapod/symetrie/zonda.yaml +0 -337
  216. egse/hexapod/symetrie/zonda_cs.py +0 -172
  217. egse/hexapod/symetrie/zonda_devif.py +0 -414
  218. egse/hexapod/symetrie/zonda_protocol.py +0 -123
  219. egse/hexapod/symetrie/zonda_ui.py +0 -449
  220. egse/hk.py +0 -791
  221. egse/icons/aeu-cs-start.svg +0 -117
  222. egse/icons/aeu-cs-stop.svg +0 -118
  223. egse/icons/aeu-cs.svg +0 -107
  224. egse/icons/aeu_cs-started.svg +0 -112
  225. egse/icons/aeu_cs-stopped.svg +0 -112
  226. egse/icons/aeu_cs.svg +0 -55
  227. egse/icons/alert.svg +0 -1
  228. egse/icons/arrow-double-left.png +0 -0
  229. egse/icons/arrow-double-right.png +0 -0
  230. egse/icons/arrow-up.svg +0 -11
  231. egse/icons/backward.svg +0 -1
  232. egse/icons/busy.svg +0 -1
  233. egse/icons/cleaning.svg +0 -115
  234. egse/icons/color-scheme.svg +0 -1
  235. egse/icons/cs-connected-alert.svg +0 -91
  236. egse/icons/cs-connected-disabled.svg +0 -43
  237. egse/icons/cs-connected.svg +0 -89
  238. egse/icons/cs-not-connected.svg +0 -44
  239. egse/icons/double-left-arrow.svg +0 -1
  240. egse/icons/double-right-arrow.svg +0 -1
  241. egse/icons/erase-disabled.svg +0 -19
  242. egse/icons/erase.svg +0 -59
  243. egse/icons/fitsgen-start.svg +0 -47
  244. egse/icons/fitsgen-stop.svg +0 -48
  245. egse/icons/fitsgen.svg +0 -1
  246. egse/icons/forward.svg +0 -1
  247. egse/icons/fov-hk-start.svg +0 -33
  248. egse/icons/fov-hk-stop.svg +0 -37
  249. egse/icons/fov-hk.svg +0 -1
  250. egse/icons/front-desk.svg +0 -1
  251. egse/icons/home-actioned.svg +0 -15
  252. egse/icons/home-disabled.svg +0 -15
  253. egse/icons/home.svg +0 -13
  254. egse/icons/info.svg +0 -1
  255. egse/icons/invalid.png +0 -0
  256. egse/icons/led-green.svg +0 -20
  257. egse/icons/led-grey.svg +0 -20
  258. egse/icons/led-orange.svg +0 -20
  259. egse/icons/led-red.svg +0 -20
  260. egse/icons/led-square-green.svg +0 -134
  261. egse/icons/led-square-grey.svg +0 -134
  262. egse/icons/led-square-orange.svg +0 -134
  263. egse/icons/led-square-red.svg +0 -134
  264. egse/icons/limit-switch-all-green.svg +0 -115
  265. egse/icons/limit-switch-all-red.svg +0 -117
  266. egse/icons/limit-switch-el+.svg +0 -116
  267. egse/icons/limit-switch-el-.svg +0 -117
  268. egse/icons/location-marker.svg +0 -1
  269. egse/icons/logo-dpu.svg +0 -48
  270. egse/icons/logo-gimbal.svg +0 -112
  271. egse/icons/logo-huber.svg +0 -23
  272. egse/icons/logo-ogse.svg +0 -31
  273. egse/icons/logo-puna.svg +0 -92
  274. egse/icons/logo-tcs.svg +0 -29
  275. egse/icons/logo-zonda.svg +0 -66
  276. egse/icons/maximize.svg +0 -1
  277. egse/icons/meter.svg +0 -1
  278. egse/icons/more.svg +0 -45
  279. egse/icons/n-fee-hk-start.svg +0 -24
  280. egse/icons/n-fee-hk-stop.svg +0 -25
  281. egse/icons/n-fee-hk.svg +0 -83
  282. egse/icons/observing-off.svg +0 -46
  283. egse/icons/observing-on.svg +0 -46
  284. egse/icons/open-document-hdf5.png +0 -0
  285. egse/icons/open-document-hdf5.svg +0 -21
  286. egse/icons/ops-mode.svg +0 -1
  287. egse/icons/play-green.svg +0 -17
  288. egse/icons/plugged-disabled.svg +0 -27
  289. egse/icons/plugged.svg +0 -21
  290. egse/icons/pm_ui.svg +0 -1
  291. egse/icons/power-button-green.svg +0 -27
  292. egse/icons/power-button-red.svg +0 -27
  293. egse/icons/power-button.svg +0 -27
  294. egse/icons/radar.svg +0 -1
  295. egse/icons/radioactive.svg +0 -2
  296. egse/icons/reload.svg +0 -1
  297. egse/icons/remote-control-off.svg +0 -28
  298. egse/icons/remote-control-on.svg +0 -28
  299. egse/icons/repeat-blue.svg +0 -15
  300. egse/icons/repeat.svg +0 -1
  301. egse/icons/settings.svg +0 -1
  302. egse/icons/shrink.svg +0 -1
  303. egse/icons/shutter.svg +0 -1
  304. egse/icons/sign-off.svg +0 -1
  305. egse/icons/sign-on.svg +0 -1
  306. egse/icons/sim-mode.svg +0 -1
  307. egse/icons/small-buttons-go.svg +0 -20
  308. egse/icons/small-buttons-minus.svg +0 -51
  309. egse/icons/small-buttons-plus.svg +0 -51
  310. egse/icons/sponge.svg +0 -220
  311. egse/icons/start-button-disabled.svg +0 -84
  312. egse/icons/start-button.svg +0 -50
  313. egse/icons/stop-button-disabled.svg +0 -84
  314. egse/icons/stop-button.svg +0 -50
  315. egse/icons/stop-red.svg +0 -17
  316. egse/icons/stop.svg +0 -1
  317. egse/icons/switch-disabled-square.svg +0 -87
  318. egse/icons/switch-disabled.svg +0 -15
  319. egse/icons/switch-off-square.svg +0 -87
  320. egse/icons/switch-off.svg +0 -72
  321. egse/icons/switch-on-square.svg +0 -87
  322. egse/icons/switch-on.svg +0 -61
  323. egse/icons/temperature-control.svg +0 -44
  324. egse/icons/th_ui_logo.svg +0 -1
  325. egse/icons/unplugged.svg +0 -23
  326. egse/icons/unvalid.png +0 -0
  327. egse/icons/user-interface.svg +0 -1
  328. egse/icons/vacuum.svg +0 -1
  329. egse/icons/valid.png +0 -0
  330. egse/icons/zoom-to-pixel-dark.svg +0 -64
  331. egse/icons/zoom-to-pixel-white.svg +0 -36
  332. egse/images/big-rotation-stage.png +0 -0
  333. egse/images/connected-100.png +0 -0
  334. egse/images/cross.svg +0 -6
  335. egse/images/disconnected-100.png +0 -0
  336. egse/images/gui-icon.png +0 -0
  337. egse/images/home.svg +0 -6
  338. egse/images/info-icon.png +0 -0
  339. egse/images/led-black.svg +0 -89
  340. egse/images/led-green.svg +0 -85
  341. egse/images/led-orange.svg +0 -85
  342. egse/images/led-red.svg +0 -85
  343. egse/images/load-icon.png +0 -0
  344. egse/images/load-setup.png +0 -0
  345. egse/images/load.png +0 -0
  346. egse/images/pause.png +0 -0
  347. egse/images/play-button.svg +0 -8
  348. egse/images/play.png +0 -0
  349. egse/images/process-status.png +0 -0
  350. egse/images/restart.png +0 -0
  351. egse/images/search.png +0 -0
  352. egse/images/sma.png +0 -0
  353. egse/images/start.png +0 -0
  354. egse/images/stop-button.svg +0 -8
  355. egse/images/stop.png +0 -0
  356. egse/images/switch-off.svg +0 -48
  357. egse/images/switch-on.svg +0 -48
  358. egse/images/undo.png +0 -0
  359. egse/images/update-button.svg +0 -11
  360. egse/imageviewer/exposureselection.py +0 -475
  361. egse/imageviewer/imageviewer.py +0 -198
  362. egse/imageviewer/matchfocalplane.py +0 -179
  363. egse/imageviewer/subfieldposition.py +0 -133
  364. egse/lampcontrol/__init__.py +0 -4
  365. egse/lampcontrol/beaglebone/beaglebone.py +0 -178
  366. egse/lampcontrol/beaglebone/beaglebone.yaml +0 -62
  367. egse/lampcontrol/beaglebone/beaglebone_cs.py +0 -106
  368. egse/lampcontrol/beaglebone/beaglebone_devif.py +0 -150
  369. egse/lampcontrol/beaglebone/beaglebone_protocol.py +0 -73
  370. egse/lampcontrol/energetiq/__init__.py +0 -22
  371. egse/lampcontrol/energetiq/eq99.yaml +0 -98
  372. egse/lampcontrol/energetiq/lampEQ99.py +0 -283
  373. egse/lampcontrol/energetiq/lampEQ99_cs.py +0 -128
  374. egse/lampcontrol/energetiq/lampEQ99_devif.py +0 -158
  375. egse/lampcontrol/energetiq/lampEQ99_encode_decode_errors.py +0 -73
  376. egse/lampcontrol/energetiq/lampEQ99_protocol.py +0 -71
  377. egse/lampcontrol/energetiq/lampEQ99_ui.py +0 -465
  378. egse/lib/CentOS-7/EtherSpaceLink_v34_86.dylib +0 -0
  379. egse/lib/CentOS-8/ESL-RMAP_v34_86.dylib +0 -0
  380. egse/lib/CentOS-8/EtherSpaceLink_v34_86.dylib +0 -0
  381. egse/lib/Debian/ESL-RMAP_v34_86.dylib +0 -0
  382. egse/lib/Debian/EtherSpaceLink_v34_86.dylib +0 -0
  383. egse/lib/Debian/libetherspacelink_v35_21.dylib +0 -0
  384. egse/lib/Linux/ESL-RMAP_v34_86.dylib +0 -0
  385. egse/lib/Linux/EtherSpaceLink_v34_86.dylib +0 -0
  386. egse/lib/Ubuntu-20/ESL-RMAP_v34_86.dylib +0 -0
  387. egse/lib/Ubuntu-20/EtherSpaceLink_v34_86.dylib +0 -0
  388. egse/lib/gssw/python3-gssw_2.2.3+31f63c9f-1_all.deb +0 -0
  389. egse/lib/ximc/__pycache__/pyximc.cpython-38 2.pyc +0 -0
  390. egse/lib/ximc/__pycache__/pyximc.cpython-38.pyc +0 -0
  391. egse/lib/ximc/libximc.framework/Frameworks/libbindy.dylib +0 -0
  392. egse/lib/ximc/libximc.framework/Frameworks/libxiwrapper.dylib +0 -0
  393. egse/lib/ximc/libximc.framework/Headers/ximc.h +0 -5510
  394. egse/lib/ximc/libximc.framework/Resources/Info.plist +0 -42
  395. egse/lib/ximc/libximc.framework/Resources/keyfile.sqlite +0 -0
  396. egse/lib/ximc/libximc.framework/libbindy.so +0 -0
  397. egse/lib/ximc/libximc.framework/libximc +0 -0
  398. egse/lib/ximc/libximc.framework/libximc.so +0 -0
  399. egse/lib/ximc/libximc.framework/libximc.so.7.0.0 +0 -0
  400. egse/lib/ximc/libximc.framework/libxiwrapper.so +0 -0
  401. egse/lib/ximc/pyximc.py +0 -922
  402. egse/listener.py +0 -179
  403. egse/logger/__init__.py +0 -243
  404. egse/logger/log_cs.py +0 -321
  405. egse/metrics.py +0 -102
  406. egse/mixin.py +0 -464
  407. egse/monitoring.py +0 -95
  408. egse/ni/alarms/__init__.py +0 -26
  409. egse/ni/alarms/cdaq9375.py +0 -300
  410. egse/ni/alarms/cdaq9375.yaml +0 -89
  411. egse/ni/alarms/cdaq9375_cs.py +0 -130
  412. egse/ni/alarms/cdaq9375_devif.py +0 -183
  413. egse/ni/alarms/cdaq9375_protocol.py +0 -48
  414. egse/obs_inspection.py +0 -165
  415. egse/observer.py +0 -41
  416. egse/obsid.py +0 -163
  417. egse/powermeter/__init__.py +0 -0
  418. egse/powermeter/ni/__init__.py +0 -38
  419. egse/powermeter/ni/cdaq9184.py +0 -224
  420. egse/powermeter/ni/cdaq9184.yaml +0 -73
  421. egse/powermeter/ni/cdaq9184_cs.py +0 -130
  422. egse/powermeter/ni/cdaq9184_devif.py +0 -201
  423. egse/powermeter/ni/cdaq9184_protocol.py +0 -48
  424. egse/powermeter/ni/cdaq9184_ui.py +0 -544
  425. egse/powermeter/thorlabs/__init__.py +0 -25
  426. egse/powermeter/thorlabs/pm100a.py +0 -380
  427. egse/powermeter/thorlabs/pm100a.yaml +0 -132
  428. egse/powermeter/thorlabs/pm100a_cs.py +0 -136
  429. egse/powermeter/thorlabs/pm100a_devif.py +0 -127
  430. egse/powermeter/thorlabs/pm100a_protocol.py +0 -80
  431. egse/powermeter/thorlabs/pm100a_ui.py +0 -725
  432. egse/process.py +0 -451
  433. egse/procman/__init__.py +0 -834
  434. egse/procman/cannot_start_process_popup.py +0 -43
  435. egse/procman/procman.yaml +0 -49
  436. egse/procman/procman_cs.py +0 -201
  437. egse/procman/procman_ui.py +0 -2081
  438. egse/protocol.py +0 -605
  439. egse/proxy.py +0 -531
  440. egse/randomwalk.py +0 -140
  441. egse/reg.py +0 -585
  442. egse/reload.py +0 -122
  443. egse/reprocess.py +0 -693
  444. egse/resource.py +0 -333
  445. egse/rmap.py +0 -406
  446. egse/rst.py +0 -135
  447. egse/search.py +0 -182
  448. egse/serialdevice.py +0 -190
  449. egse/services.py +0 -247
  450. egse/services.yaml +0 -68
  451. egse/settings.py +0 -379
  452. egse/settings.yaml +0 -980
  453. egse/setup.py +0 -1181
  454. egse/shutter/__init__.py +0 -0
  455. egse/shutter/thorlabs/__init__.py +0 -19
  456. egse/shutter/thorlabs/ksc101.py +0 -205
  457. egse/shutter/thorlabs/ksc101.yaml +0 -105
  458. egse/shutter/thorlabs/ksc101_cs.py +0 -136
  459. egse/shutter/thorlabs/ksc101_devif.py +0 -201
  460. egse/shutter/thorlabs/ksc101_protocol.py +0 -71
  461. egse/shutter/thorlabs/ksc101_ui.py +0 -548
  462. egse/shutter/thorlabs/sc10.py +0 -82
  463. egse/shutter/thorlabs/sc10.yaml +0 -52
  464. egse/shutter/thorlabs/sc10_controller.py +0 -81
  465. egse/shutter/thorlabs/sc10_cs.py +0 -108
  466. egse/shutter/thorlabs/sc10_interface.py +0 -25
  467. egse/shutter/thorlabs/sc10_simulator.py +0 -30
  468. egse/simulator.py +0 -41
  469. egse/slack.py +0 -61
  470. egse/socketdevice.py +0 -218
  471. egse/sockets.py +0 -218
  472. egse/spw.py +0 -1401
  473. egse/stages/__init__.py +0 -12
  474. egse/stages/aerotech/ensemble.py +0 -245
  475. egse/stages/aerotech/ensemble.yaml +0 -205
  476. egse/stages/aerotech/ensemble_controller.py +0 -275
  477. egse/stages/aerotech/ensemble_cs.py +0 -110
  478. egse/stages/aerotech/ensemble_interface.py +0 -132
  479. egse/stages/aerotech/ensemble_parameters.py +0 -433
  480. egse/stages/aerotech/ensemble_simulator.py +0 -27
  481. egse/stages/aerotech/mgse_sim.py +0 -188
  482. egse/stages/arun/smd3.py +0 -110
  483. egse/stages/arun/smd3.yaml +0 -68
  484. egse/stages/arun/smd3_controller.py +0 -470
  485. egse/stages/arun/smd3_cs.py +0 -112
  486. egse/stages/arun/smd3_interface.py +0 -53
  487. egse/stages/arun/smd3_simulator.py +0 -27
  488. egse/stages/arun/smd3_stop.py +0 -16
  489. egse/stages/huber/__init__.py +0 -49
  490. egse/stages/huber/smc9300.py +0 -920
  491. egse/stages/huber/smc9300.yaml +0 -63
  492. egse/stages/huber/smc9300_cs.py +0 -178
  493. egse/stages/huber/smc9300_devif.py +0 -345
  494. egse/stages/huber/smc9300_protocol.py +0 -113
  495. egse/stages/huber/smc9300_sim.py +0 -547
  496. egse/stages/huber/smc9300_ui.py +0 -973
  497. egse/state.py +0 -173
  498. egse/statemachine.py +0 -274
  499. egse/storage/__init__.py +0 -1067
  500. egse/storage/persistence.py +0 -2295
  501. egse/storage/storage.yaml +0 -79
  502. egse/storage/storage_cs.py +0 -231
  503. egse/styles/dark.qss +0 -343
  504. egse/styles/default.qss +0 -48
  505. egse/synoptics/__init__.py +0 -417
  506. egse/synoptics/syn.yaml +0 -9
  507. egse/synoptics/syn_cs.py +0 -195
  508. egse/system.py +0 -1611
  509. egse/tcs/__init__.py +0 -14
  510. egse/tcs/tcs.py +0 -879
  511. egse/tcs/tcs.yaml +0 -14
  512. egse/tcs/tcs_cs.py +0 -202
  513. egse/tcs/tcs_devif.py +0 -292
  514. egse/tcs/tcs_protocol.py +0 -180
  515. egse/tcs/tcs_sim.py +0 -177
  516. egse/tcs/tcs_ui.py +0 -543
  517. egse/tdms.py +0 -171
  518. egse/tempcontrol/__init__.py +0 -23
  519. egse/tempcontrol/agilent/agilent34970.py +0 -109
  520. egse/tempcontrol/agilent/agilent34970.yaml +0 -44
  521. egse/tempcontrol/agilent/agilent34970_cs.py +0 -114
  522. egse/tempcontrol/agilent/agilent34970_devif.py +0 -182
  523. egse/tempcontrol/agilent/agilent34970_protocol.py +0 -96
  524. egse/tempcontrol/agilent/agilent34972.py +0 -111
  525. egse/tempcontrol/agilent/agilent34972.yaml +0 -44
  526. egse/tempcontrol/agilent/agilent34972_cs.py +0 -115
  527. egse/tempcontrol/agilent/agilent34972_devif.py +0 -189
  528. egse/tempcontrol/agilent/agilent34972_protocol.py +0 -98
  529. egse/tempcontrol/beaglebone/beaglebone.py +0 -341
  530. egse/tempcontrol/beaglebone/beaglebone.yaml +0 -110
  531. egse/tempcontrol/beaglebone/beaglebone_cs.py +0 -117
  532. egse/tempcontrol/beaglebone/beaglebone_protocol.py +0 -134
  533. egse/tempcontrol/beaglebone/beaglebone_ui.py +0 -674
  534. egse/tempcontrol/digalox/digalox.py +0 -115
  535. egse/tempcontrol/digalox/digalox.yaml +0 -36
  536. egse/tempcontrol/digalox/digalox_cs.py +0 -108
  537. egse/tempcontrol/digalox/digalox_protocol.py +0 -56
  538. egse/tempcontrol/keithley/__init__.py +0 -33
  539. egse/tempcontrol/keithley/daq6510.py +0 -662
  540. egse/tempcontrol/keithley/daq6510.yaml +0 -105
  541. egse/tempcontrol/keithley/daq6510_cs.py +0 -163
  542. egse/tempcontrol/keithley/daq6510_devif.py +0 -343
  543. egse/tempcontrol/keithley/daq6510_protocol.py +0 -79
  544. egse/tempcontrol/keithley/daq6510_sim.py +0 -186
  545. egse/tempcontrol/lakeshore/__init__.py +0 -33
  546. egse/tempcontrol/lakeshore/lsci.py +0 -361
  547. egse/tempcontrol/lakeshore/lsci.yaml +0 -162
  548. egse/tempcontrol/lakeshore/lsci_cs.py +0 -174
  549. egse/tempcontrol/lakeshore/lsci_devif.py +0 -292
  550. egse/tempcontrol/lakeshore/lsci_protocol.py +0 -76
  551. egse/tempcontrol/lakeshore/lsci_ui.py +0 -387
  552. egse/tempcontrol/ni/__init__.py +0 -0
  553. egse/tempcontrol/spid/spid.py +0 -109
  554. egse/tempcontrol/spid/spid.yaml +0 -81
  555. egse/tempcontrol/spid/spid_controller.py +0 -279
  556. egse/tempcontrol/spid/spid_cs.py +0 -136
  557. egse/tempcontrol/spid/spid_protocol.py +0 -107
  558. egse/tempcontrol/spid/spid_ui.py +0 -723
  559. egse/tempcontrol/srs/__init__.py +0 -22
  560. egse/tempcontrol/srs/ptc10.py +0 -867
  561. egse/tempcontrol/srs/ptc10.yaml +0 -227
  562. egse/tempcontrol/srs/ptc10_cs.py +0 -128
  563. egse/tempcontrol/srs/ptc10_devif.py +0 -116
  564. egse/tempcontrol/srs/ptc10_protocol.py +0 -39
  565. egse/tempcontrol/srs/ptc10_ui.py +0 -906
  566. egse/ups/apc/apc.py +0 -236
  567. egse/ups/apc/apc.yaml +0 -45
  568. egse/ups/apc/apc_cs.py +0 -101
  569. egse/ups/apc/apc_protocol.py +0 -125
  570. egse/user.yaml +0 -7
  571. egse/vacuum/beaglebone/beaglebone.py +0 -149
  572. egse/vacuum/beaglebone/beaglebone.yaml +0 -44
  573. egse/vacuum/beaglebone/beaglebone_cs.py +0 -108
  574. egse/vacuum/beaglebone/beaglebone_devif.py +0 -159
  575. egse/vacuum/beaglebone/beaglebone_protocol.py +0 -192
  576. egse/vacuum/beaglebone/beaglebone_ui.py +0 -638
  577. egse/vacuum/instrutech/igm402.py +0 -91
  578. egse/vacuum/instrutech/igm402.yaml +0 -90
  579. egse/vacuum/instrutech/igm402_controller.py +0 -124
  580. egse/vacuum/instrutech/igm402_cs.py +0 -108
  581. egse/vacuum/instrutech/igm402_interface.py +0 -49
  582. egse/vacuum/instrutech/igm402_simulator.py +0 -36
  583. egse/vacuum/keller/kellerBus.py +0 -256
  584. egse/vacuum/keller/leo3.py +0 -100
  585. egse/vacuum/keller/leo3.yaml +0 -38
  586. egse/vacuum/keller/leo3_controller.py +0 -81
  587. egse/vacuum/keller/leo3_cs.py +0 -101
  588. egse/vacuum/keller/leo3_interface.py +0 -33
  589. egse/vacuum/mks/evision.py +0 -86
  590. egse/vacuum/mks/evision.yaml +0 -75
  591. egse/vacuum/mks/evision_cs.py +0 -101
  592. egse/vacuum/mks/evision_devif.py +0 -313
  593. egse/vacuum/mks/evision_interface.py +0 -60
  594. egse/vacuum/mks/evision_simulator.py +0 -24
  595. egse/vacuum/mks/evision_ui.py +0 -701
  596. egse/vacuum/pfeiffer/acp40.py +0 -87
  597. egse/vacuum/pfeiffer/acp40.yaml +0 -60
  598. egse/vacuum/pfeiffer/acp40_controller.py +0 -117
  599. egse/vacuum/pfeiffer/acp40_cs.py +0 -109
  600. egse/vacuum/pfeiffer/acp40_interface.py +0 -40
  601. egse/vacuum/pfeiffer/acp40_simulator.py +0 -37
  602. egse/vacuum/pfeiffer/tc400.py +0 -87
  603. egse/vacuum/pfeiffer/tc400.yaml +0 -83
  604. egse/vacuum/pfeiffer/tc400_controller.py +0 -136
  605. egse/vacuum/pfeiffer/tc400_cs.py +0 -109
  606. egse/vacuum/pfeiffer/tc400_interface.py +0 -70
  607. egse/vacuum/pfeiffer/tc400_simulator.py +0 -35
  608. egse/vacuum/pfeiffer/tpg261.py +0 -80
  609. egse/vacuum/pfeiffer/tpg261.yaml +0 -66
  610. egse/vacuum/pfeiffer/tpg261_controller.py +0 -150
  611. egse/vacuum/pfeiffer/tpg261_cs.py +0 -109
  612. egse/vacuum/pfeiffer/tpg261_interface.py +0 -59
  613. egse/vacuum/pfeiffer/tpg261_simulator.py +0 -23
  614. egse/version.py +0 -174
  615. egse/visitedpositions.py +0 -398
  616. egse/windowing.py +0 -213
  617. egse/zmq/__init__.py +0 -28
  618. egse/zmq/spw.py +0 -160
  619. egse/zmq_ser.py +0 -41
  620. scripts/alerts/cold.yaml +0 -278
  621. scripts/alerts/example_alerts.yaml +0 -54
  622. scripts/alerts/transition.yaml +0 -14
  623. scripts/alerts/warm.yaml +0 -49
  624. scripts/analyse_n_fee_hk_data.py +0 -52
  625. scripts/check_hdf5_files.py +0 -192
  626. scripts/check_register_sync.py +0 -47
  627. scripts/check_tcs_calib_coef.py +0 -90
  628. scripts/correct_ccd_cold_temperature_cal.py +0 -157
  629. scripts/create_hdf5_report.py +0 -293
  630. scripts/csl_model.py +0 -420
  631. scripts/csl_restore_setup.py +0 -229
  632. scripts/export-grafana-dashboards.py +0 -49
  633. scripts/fdir/cs_recovery/fdir_cs_recovery.py +0 -54
  634. scripts/fdir/fdir_table.yaml +0 -70
  635. scripts/fdir/fdir_test_recovery.py +0 -10
  636. scripts/fdir/hw_recovery/fdir_agilent_hw_recovery.py +0 -73
  637. scripts/fdir/limit_recovery/fdir_agilent_limit.py +0 -61
  638. scripts/fdir/limit_recovery/fdir_bb_heater_limit.py +0 -59
  639. scripts/fdir/limit_recovery/fdir_ensemble_limit.py +0 -33
  640. scripts/fdir/limit_recovery/fdir_pressure_limit_recovery.py +0 -71
  641. scripts/fix_csv.py +0 -80
  642. scripts/ias/correct_ccd_temp_cal_elfique.py +0 -43
  643. scripts/ias/correct_ccd_temp_cal_floreffe.py +0 -43
  644. scripts/ias/correct_trp_swap_achel.py +0 -199
  645. scripts/inta/correct_ccd_temp_cal_duvel.py +0 -43
  646. scripts/inta/correct_ccd_temp_cal_gueuze.py +0 -43
  647. scripts/n_fee_supply_voltage_calculation.py +0 -92
  648. scripts/playground.py +0 -30
  649. scripts/print_hdf5_hk_data.py +0 -68
  650. scripts/print_register_map.py +0 -43
  651. scripts/remove_lines_between_matches.py +0 -188
  652. scripts/sron/commanding/control_heaters.py +0 -44
  653. scripts/sron/commanding/pumpdown.py +0 -46
  654. scripts/sron/commanding/set_pid_setpoint.py +0 -19
  655. scripts/sron/commanding/shutdown_bbb_heaters.py +0 -10
  656. scripts/sron/commanding/shutdown_pumps.py +0 -33
  657. scripts/sron/correct_mgse_coordinates_brigand_chimay.py +0 -272
  658. scripts/sron/correct_trp_swap_brigand.py +0 -204
  659. scripts/sron/gimbal_conversions.py +0 -75
  660. scripts/sron/tm_gen/tm_gen_agilent.py +0 -37
  661. scripts/sron/tm_gen/tm_gen_heaters.py +0 -4
  662. scripts/sron/tm_gen/tm_gen_spid.py +0 -13
  663. scripts/update_operational_cgse.py +0 -268
  664. scripts/update_operational_cgse_old.py +0 -273
egse/dpu/fitsgen.py DELETED
@@ -1,2096 +0,0 @@
1
- """
2
- This module define the FITS generation process.
3
-
4
- The FITS generation process connects to the monitoring channel of the DPU control server and starts processing
5
- HDF5 files as soon as they are available. The FITS generation can also be started off-line to process a list
6
- of HDF5 files or to process a given Observation (OBSID).
7
-
8
- """
9
- import glob
10
- import logging
11
- import multiprocessing
12
- import os
13
- import pickle
14
- import sys
15
- import threading
16
- import time
17
- from datetime import datetime
18
- from datetime import timedelta
19
- from enum import Enum
20
- from enum import EnumMeta
21
- from itertools import chain
22
- from pathlib import Path
23
- from pathlib import PosixPath
24
- from typing import List
25
- from typing import Mapping
26
-
27
- import click
28
- import invoke
29
- import natsort
30
- import numpy as np
31
- import persistqueue
32
- import rich
33
- import zmq
34
- from astropy.io import fits
35
- from h5py import File
36
- from h5py._hl.attrs import AttributeManager
37
- from scipy.interpolate import interp1d
38
-
39
- import egse
40
- from egse import h5
41
- from egse.config import find_file
42
- from egse.config import find_files
43
- from egse.control import time_in_ms
44
- from egse.dpu import DPUMonitoring
45
- from egse.dpu import get_expected_last_packet_flags
46
- from egse.dpu.dpu_cs import is_dpu_cs_active
47
- from egse.env import get_data_storage_location
48
- from egse.exceptions import Abort
49
- from egse.fee import convert_ccd_order_value
50
- from egse.fee import n_fee_mode
51
- from egse.fee.nfee import HousekeepingData
52
- from egse.hk import HKError
53
- from egse.hk import get_housekeeping
54
- from egse.obsid import LAB_SETUP_TEST
55
- from egse.obsid import ObservationIdentifier
56
- from egse.obsid import TEST_LAB
57
- from egse.obsid import obsid_from_storage
58
- from egse.reg import RegisterMap
59
- from egse.settings import Settings
60
- from egse.setup import Setup
61
- from egse.setup import load_setup
62
- from egse.spw import SpaceWirePacket
63
- from egse.storage import is_storage_manager_active
64
- from egse.storage.persistence import FITS
65
- from egse.storage.persistence import HDF5
66
- from egse.synoptics import ORIGIN as SYN_ORIGIN
67
- from egse.synoptics import get_synoptics_table
68
- from egse.system import format_datetime
69
- from egse.system import read_last_line
70
- from egse.system import time_since_epoch_1958
71
- from egse.zmq_ser import bind_address
72
- from egse.zmq_ser import connect_address
73
-
74
- LOGGER = logging.getLogger(__name__)
75
-
76
- N_FEE_SETTINGS = Settings.load("N-FEE")
77
- CCD_SETTINGS = Settings.load("CCD")
78
- SITE = Settings.load("SITE")
79
- CTRL_SETTINGS = Settings.load("FITS Generator Control Server")
80
- STORAGE_SETTINGS = Settings.load("Storage Control Server")
81
- DPU_SETTINGS = Settings.load("DPU")
82
-
83
- TIMEOUT_RECV = 1.0 # seconds
84
-
85
-
86
- def get_cycle_time(n_fee_state: Mapping, obsid=None, data_dir=None):
87
- """ Return the image cycle time.
88
-
89
- In the given N-FEE state parameters or register map, we check whether we are in internal or external sync:
90
-
91
- - Internal sync: Read the image cycle time from the given N-FEE state parameters or register map;
92
- - External sync: Get the image cycle time from the AEU (AWG2). In case of off-line FITS generation (i.e. from
93
- the HDF5 files), the image cycle time (for the specified obsid) is taken from the AEU housekeeping (AWG2).
94
- In case of on-line FITS generation, the image cycle time is queried from the AEU AWG2.
95
-
96
- Args:
97
- - n_fee_state: N-FEE state parameters or register map.
98
- - obsid: Observation identifier for which the image cycle time is read from the AEU housekeeping.
99
-
100
- Returns: Image cycle time [s].
101
- """
102
-
103
- # Internal sync -> use sync period from the N-FEE state
104
-
105
- if n_fee_state["sync_sel"] == 1:
106
- return n_fee_state["int_sync_period"] / 1000. # [ms] -> [s]
107
-
108
- # External sync -> use AEU sync pulses
109
-
110
- else:
111
- if obsid:
112
- try:
113
- return float(get_housekeeping("GAEU_EXT_CYCLE_TIME", obsid=obsid, data_dir=data_dir)[1])
114
- except HKError as exc: # See GitHub issue #2025
115
- LOGGER.warning("No HK available for AWG2 (using default cycle time of 25s)", exc)
116
- return 25.0
117
- else:
118
- return None
119
-
120
-
121
- def get_cgse_version(obsid=None, data_dir=None):
122
- """ Returns the version of the Common EGSE with which the FITS file was created.
123
-
124
- Args:
125
- - obsid: Observation identifier for which the version of the Common EGSE is read from the Configuration
126
- Manager housekeeping.
127
- """
128
-
129
- try:
130
- return None if obsid is None else get_housekeeping("CM_CGSE_VERSION", obsid=obsid, data_dir=data_dir)[1]
131
- except HKError:
132
- return None
133
-
134
-
135
- class FITSGenerator:
136
-
137
- def __init__(self):
138
- """ Generation of FITS files from HDF5 files with SpW packets.
139
-
140
- In a separate thread, the DPU monitoring puts the name of new HDF5 files with SpW packets in the queue. The
141
- FITS generator accesses this queue (FIFO) and stores the information in a FITS file.
142
-
143
- When there is a change in crucial parameters, the current FITS file (if any) will be closed and a new one will
144
- be created as soon as data packet start coming in (when the N-FEE is in full-image or full-image pattern mode).
145
- """
146
-
147
- # Queue with the full path of the HDF5 files that still need to be processed.
148
-
149
- self.hdf5_filename_queue = persistqueue.Queue(f"{get_data_storage_location()}/{DPU_SETTINGS['HDF5_QUEUE']}")
150
-
151
- # Name of the FITS file currently being written
152
- # (None if the N-FEE is not in full-image mode or in full-image pattern mode)
153
-
154
- self.fits_images_filename = None
155
- self.fits_cube_filename = None
156
-
157
- # Name of the HDF5 file currently being processed
158
-
159
- self.hdf5_filename = None
160
-
161
- # The last obsid that was/is being processed
162
-
163
- self.last_processed_obsid = None
164
-
165
- # Keep track of what was the N-FEE mode and what were the crucial parameters at the previous long pulse
166
- # (When we have checked whether a change has been detected, these values will be overwritten with the new ones)
167
-
168
- self.ccd_mode_config = None
169
- self.v_start = None
170
- self.v_end = None
171
- self.h_end = None
172
- self.ccd_readout_order = None
173
- # self.sensor_sel = None
174
- self.rows_final_dump = None
175
- self.setup = load_setup()
176
- self.sensor_sel_enum = self.setup.camera.fee.sensor_sel.enum
177
- self.fee_side = self.setup.camera.fee.ccd_sides.enum
178
- self.camera_name = self.setup.camera.ID
179
-
180
- self.config_slicing_num_cycles = 0 # Configured slicing parameter
181
- self.processed_num_cycles = 0 # HDF5 files with image data processed for current FITS file
182
-
183
- # self._quit_event = multiprocessing.Event()
184
-
185
- self.keep_processing_queue = True
186
-
187
- # The DPU monitoring should populate the queue in a separate thread
188
-
189
- self.dpu_monitoring_thread = threading.Thread(target=self.fill_queue)
190
- self.dpu_monitoring_thread.daemon = True
191
- self.dpu_monitoring_thread.start()
192
-
193
- # Processing the content of the queue should be done in a separate thread
194
-
195
- self.process_queue_thread = threading.Thread(target=self.process_queue)
196
- self.process_queue_thread.daemon = True
197
- self.process_queue_thread.start()
198
-
199
- def fill_queue(self):
200
- """
201
- The DPU monitoring fills the queue.
202
-
203
- Each time an HDF5 file with SpW packets is closed, the DPU monitoring puts the full path of this file on the
204
- queue.
205
- """
206
-
207
- dpu_monitoring_timeout = 30 # seconds
208
-
209
- with DPUMonitoring() as dpu_monitoring:
210
-
211
- start_time = time.time()
212
-
213
- while self.keep_processing_queue:
214
-
215
- try:
216
- hdf5_filename = dpu_monitoring.wait_for_hdf5_filename(retries=0, timeout=1.0)
217
- self.hdf5_filename_queue.put(hdf5_filename)
218
- start_time = time.time()
219
- except TimeoutError:
220
- if time.time() - start_time > dpu_monitoring_timeout:
221
- LOGGER.warning(f"DPU monitoring timeout, "
222
- f"no HDF5 filename received after {dpu_monitoring_timeout} seconds.")
223
- start_time = time.time()
224
-
225
- LOGGER.info(f"Broke out of monitoring loop {self.keep_processing_queue=}.")
226
-
227
- def run(self):
228
- """ Process the content of the queue.
229
-
230
- When there is a filename in the queue, take it from the queue:
231
-
232
- - If there is a change in crucial parameters, close the current FITS file (if any).
233
- - If there is a change in crucial parameter and the N-FEE is in full-image mode or in full-image pattern
234
- mode, or the N-FEE goes to full-image mode or full-image pattern mode, a new FITS file will be created.
235
- - The content of the HDF5 files will be extracted and passed to the FITS persistence layer as SpW packets.
236
- """
237
- zcontext = zmq.Context.instance()
238
-
239
- monitoring_socket = zcontext.socket(zmq.PUB)
240
- monitoring_socket.bind(bind_address(CTRL_SETTINGS.PROTOCOL, CTRL_SETTINGS.MONITORING_PORT,))
241
-
242
- endpoint = bind_address(CTRL_SETTINGS.PROTOCOL, CTRL_SETTINGS.COMMANDING_PORT)
243
- commander = zcontext.socket(zmq.REP)
244
- commander.bind(endpoint)
245
-
246
- poller = zmq.Poller()
247
- poller.register(commander, zmq.POLLIN)
248
-
249
- last_time = time_in_ms()
250
-
251
- try:
252
- while True:
253
-
254
- if _check_commander_status(commander, poller):
255
-
256
- self.keep_processing_queue = False
257
- break
258
-
259
- if time_in_ms() - last_time >= 1000:
260
- last_time = time_in_ms()
261
-
262
- monitoring_info = {"hdf5": self.hdf5_filename,
263
- "fits": self.fits_cube_filename or self.fits_images_filename,
264
- "last obsid (being) processed": self.last_processed_obsid}
265
- pickle_string = pickle.dumps(monitoring_info)
266
- monitoring_socket.send(pickle_string)
267
-
268
- except KeyboardInterrupt:
269
- click.echo("KeyboardInterrupt caught!")
270
-
271
- self.keep_processing_queue = False
272
-
273
- # Clean up all open sockets and running threads
274
-
275
- poller.unregister(commander)
276
-
277
- LOGGER.info("Shutting down FITS generation")
278
-
279
- commander.close(linger=0)
280
- LOGGER.info("Commander closed.")
281
-
282
- monitoring_socket.close(linger=0)
283
- LOGGER.info("Monitoring socket closed.")
284
-
285
- # Check if the Monitoring and Processing Threads are finished
286
- # Since the default timeout on the DPU Monitoring is set to 30s for some reason, this may take some time
287
-
288
- LOGGER.info("Joining worker threads, this may take some time...")
289
- self.dpu_monitoring_thread.join()
290
- self.process_queue_thread.join()
291
- LOGGER.info("Worker threads terminated.")
292
-
293
- del self.hdf5_filename_queue
294
- LOGGER.info("HDF5 filename Queue deleted.")
295
-
296
- del zcontext
297
-
298
- def __del__(self):
299
- egse.logger.close_all_zmq_handlers()
300
-
301
- zmq.Context.instance().term()
302
- logging.getLogger().info("ZMQ Context terminated.")
303
-
304
- def process_queue(self):
305
-
306
- location = get_data_storage_location()
307
-
308
- syn_obsid = None
309
-
310
- while self.keep_processing_queue:
311
-
312
- # There is an HDF5 file ready for processing
313
-
314
- if not self.hdf5_filename_queue.empty():
315
-
316
- try:
317
-
318
- # Get the first item in the queue (FIFO) and open it
319
-
320
- item = self.hdf5_filename_queue.get()
321
- # LOGGER.info(f"HFD5 filename Queue {item = }")
322
- self.hdf5_filename = hdf5_filename = item[0]
323
- self.hdf5_filename_queue.task_done()
324
-
325
- LOGGER.info(f"Processing file {hdf5_filename}")
326
-
327
- with h5.get_file(hdf5_filename, mode="r", locking=False) as hdf5_file:
328
-
329
- LOGGER.info(f"Opened file {hdf5_filename}")
330
-
331
- # Check whether there is data in the HDF5
332
- # (if there is no data in the HDF5 file, nothing has to be done and you can go to the next file)
333
-
334
- try:
335
-
336
- # Slicing
337
-
338
- try:
339
- slicing_num_cycles = hdf5_file["dpu"].attrs["slicing_num_cycles"]
340
- if slicing_num_cycles != self.config_slicing_num_cycles:
341
- LOGGER.debug(f"Slicing parameter changed: {self.config_slicing_num_cycles} "
342
- f"-> {slicing_num_cycles}")
343
- self.close_fits()
344
- self.config_slicing_num_cycles = slicing_num_cycles
345
- except KeyError:
346
- self.config_slicing_num_cycles = 0
347
- LOGGER.debug("No slicing")
348
-
349
- # Obsid
350
-
351
- try:
352
- obsid = hdf5_file["obsid"][()].decode()
353
- # LOGGER.info(f"OBSID from HDF5 file: {obsid = }")
354
- obsid = ObservationIdentifier.create_from_string(obsid, order=LAB_SETUP_TEST)
355
- # LOGGER.info(f"OBSID from string: {obsid = !s}")
356
-
357
- self.last_processed_obsid = obsid_from_storage(
358
- obsid, data_dir=location, camera_name=self.camera_name)
359
- except (KeyError, ValueError) as exc:
360
- # KeyError: when no obsid is included in the HDF5 file
361
- # ValueError: when the format of the obsid does not match LAB_SETUP_TEST
362
- # Uncomment the following line when you need more debug info
363
- # LOGGER.warning(f"Exception caught: {exc.__class__.__name__} - {exc}", exc_info=False)
364
- obsid = None
365
- except AttributeError as exc:
366
- # AttributeError: when is this raised ??
367
- LOGGER.warning(f"Exception caught: {exc.__class__.__name__} - {exc}", exc_info=False)
368
- LOGGER.error(f"No data present for obsid {str(obsid)} in the obs folder, terminating ...")
369
- self.keep_processing_queue = False
370
- break
371
-
372
- register_map = RegisterMap("N-FEE", memory_map=h5.get_data(hdf5_file["register"]))
373
-
374
- # Loop over all groups in the current HDF5 file and check whether the "data" group is
375
- # present
376
-
377
- has_data = False
378
-
379
- for group in h5.groups(hdf5_file):
380
-
381
- if "data" in group.keys():
382
-
383
- has_data = True
384
-
385
- n_fee_state = group["data"].attrs
386
-
387
- # Check whether there is a change in crucial parameters or in the N-FEE mode
388
-
389
- if self.crucial_parameter_change(n_fee_state):
390
-
391
- self.close_fits()
392
-
393
- if in_data_acquisition_mode(n_fee_state):
394
-
395
- if self.fits_images_filename is None:
396
-
397
- # Start writing to a new FITS file
398
-
399
- self.fits_images_filename = construct_images_filename(
400
- hdf5_filename, obsid, location=location, camera_name=self.camera_name
401
- )
402
-
403
- ccd_readout_order = convert_ccd_order_value(self.ccd_readout_order)
404
-
405
- prep = {
406
- "v_start": self.v_start,
407
- "v_end": self.v_end,
408
- "h_end": self.h_end,
409
- "rows_final_dump": self.rows_final_dump,
410
- "ccd_mode_config": self.ccd_mode_config,
411
- "ccd_readout_order": ccd_readout_order, # CCD numbering [1-4]
412
- "expected_last_packet_flags": get_expected_last_packet_flags(
413
- n_fee_state, self.sensor_sel_enum),
414
- "obsid": str(obsid),
415
- "cycle_time": get_cycle_time(n_fee_state, obsid=obsid),
416
- "cgse_version": get_cgse_version(obsid=obsid),
417
- "setup": self.setup,
418
- "register_map": register_map,
419
- }
420
-
421
- persistence = FITS(self.fits_images_filename, prep)
422
- persistence.open()
423
-
424
- # See https://github.com/IvS-KULeuven/plato-common-egse/issues/901
425
- # timecode = group["timecode"]
426
- # spw_packet = SpaceWirePacket.create_packet(h5.get_data(timecode))
427
-
428
- timestamp = group["timecode"].attrs["timestamp"]
429
- persistence.create({"Timestamp": timestamp})
430
-
431
- data = group["data"]
432
- sorted_datasets = natsort.natsorted(data.items(), key=lambda x: x[0])
433
-
434
- persistence.expected_last_packet_flags = get_expected_last_packet_flags(
435
- n_fee_state, self.sensor_sel_enum)
436
-
437
- for identifier, dataset in sorted_datasets:
438
-
439
- spw_packet = SpaceWirePacket.create_packet(h5.get_data(dataset))
440
- persistence.create({f"SpW packet {identifier}": spw_packet})
441
-
442
- if has_data:
443
- self.processed_num_cycles += 1
444
- syn_obsid = obsid
445
-
446
- if self.config_slicing_num_cycles != 0 and \
447
- self.processed_num_cycles == self.config_slicing_num_cycles:
448
- self.close_fits()
449
-
450
- else:
451
-
452
- self.close_fits()
453
- self.clear_crucial_parameters()
454
-
455
- # When the previous HDF5 file still pertained to an observation and the current one doesn't,
456
- # it means that the observation has just finished and all FITS files have been generated. It
457
- # is only at this point that the synoptics can be included in the FITS headers.
458
-
459
- if syn_obsid is not None and obsid is None:
460
- LOGGER.info(f"Adding synoptics for {syn_obsid}")
461
- add_synoptics(syn_obsid, fits_dir=location, syn_dir=location, fee_side=self.fee_side)
462
- syn_obsid = None
463
- except KeyError:
464
- LOGGER.debug("KeyError occurred when accessing data in all groups of the HDF5 file.")
465
-
466
- except IndexError:
467
- LOGGER.debug("Queue contained an emtpy entry")
468
- except RuntimeError as exc:
469
- LOGGER.debug(f"Unable to open HDF5 file: {exc}")
470
-
471
- def clear_crucial_parameters(self):
472
- """ Clear the crucial parameters."""
473
-
474
- self.v_start = None
475
- self.v_end = None
476
- self.h_end = None
477
- self.rows_final_dump = None
478
- self.ccd_readout_order = None
479
- self.ccd_mode_config = None
480
-
481
- def close_fits(self):
482
-
483
- if self.fits_images_filename is not None:
484
-
485
- self.fits_cube_filename = construct_cube_filename(self.fits_images_filename)
486
- convert_to_cubes(self.fits_images_filename, self.setup)
487
- self.fits_cube_filename = None
488
-
489
- # Stop writing to the current FITS file
490
-
491
- self.fits_images_filename = None
492
-
493
- # Reset the number of HDF5 files with image data processed for current FITS file
494
-
495
- self.processed_num_cycles = 0
496
-
497
- def crucial_parameter_change(self, n_fee_state: AttributeManager):
498
- """ Check for a change in crucial parameters.
499
-
500
- Crucial parameters are:
501
-
502
- - ccd_mode_config: readout mode;
503
- - v_start (int) and v_end(int): index of the first and the last row being transmitted;
504
- - h_end (int): index of the last serial readout of the readout register;
505
- - ccd_readout_order: CCDs that will be read out;
506
- # - sensor_sel: which side(s) of the CCD(s) that will be read out;
507
-
508
- Args:
509
- - n_fee_stage: N-FEE stae parameters.
510
-
511
- Returns: True if a change in crucial parameters has been detected; False otherwise.
512
- """
513
-
514
- ccd_mode_config = n_fee_state["ccd_mode_config"]
515
- v_start = n_fee_state["v_start"]
516
- v_end = n_fee_state["v_end"]
517
- h_end = n_fee_state["h_end"]
518
- ccd_readout_order = n_fee_state["ccd_readout_order"]
519
- rows_final_dump = n_fee_state["n_final_dump"]
520
-
521
- crucial_parameter_change = False
522
-
523
- if v_start != self.v_start:
524
-
525
- LOGGER.info(f"Change in v_start: {self.v_start} -> {v_start}")
526
-
527
- self.v_start = v_start
528
- crucial_parameter_change = True
529
-
530
- if v_end != self.v_end:
531
-
532
- LOGGER.info(f"Change in v_end: {self.v_end} -> {v_end}")
533
-
534
- self.v_end = v_end
535
- crucial_parameter_change = True
536
-
537
- if h_end != self.h_end:
538
-
539
- LOGGER.info(f"Change in h_end: {self.h_end} -> {h_end}")
540
-
541
- self.h_end = h_end
542
- crucial_parameter_change = True
543
-
544
- if rows_final_dump != self.rows_final_dump:
545
-
546
- LOGGER.info(f"Change in rows_final_dump: {self.rows_final_dump} -> {rows_final_dump}")
547
-
548
- self.rows_final_dump = rows_final_dump
549
- crucial_parameter_change = True
550
-
551
- if ccd_readout_order != self.ccd_readout_order:
552
-
553
- LOGGER.info(f"Change in ccd_readout_order: {self.ccd_readout_order} -> {ccd_readout_order}")
554
-
555
- self.ccd_readout_order = ccd_readout_order
556
- crucial_parameter_change = True
557
-
558
- if ccd_mode_config != self.ccd_mode_config:
559
-
560
- LOGGER.info(f"Change in ccd_mode_config: {self.ccd_mode_config} -> {ccd_mode_config}")
561
-
562
- self.ccd_mode_config = ccd_mode_config
563
- crucial_parameter_change = True
564
-
565
- return crucial_parameter_change
566
-
567
-
568
- def convert_to_cubes(filename, setup: Setup):
569
- """ Conversion of level-1 FITS files to level-2 FITS files.
570
-
571
- After the conversion, the flat-structure FITS file is removed.
572
-
573
- Args:
574
- - filename: Full path of the level-1 FITS file.
575
- """
576
-
577
- fee_side = setup.camera.fee.ccd_sides.enum
578
- cube_filename = construct_cube_filename(filename)
579
- LOGGER.info(f"Converting to {cube_filename}")
580
-
581
- with fits.open(filename) as level1:
582
-
583
- primary_header = level1["PRIMARY"].header
584
-
585
- selected_ccds = np.unique(primary_header["CCD_READOUT_ORDER"][1:-1].split(", ")) # str
586
- side_is_present = {ccd: {fee_side.E: 0, fee_side.F: 0} for ccd in selected_ccds}
587
-
588
- has_serial_overscan = primary_header["H_END"] >= \
589
- CCD_SETTINGS.LENGTH_SERIAL_PRESCAN + CCD_SETTINGS.NUM_COLUMNS // 2
590
- has_parallel_overscan = primary_header["V_END"] >= CCD_SETTINGS.NUM_ROWS
591
-
592
- # We are going to calculate the relative time since the very first exposure in the FITS file. We don't know
593
- # here which CCD side of which CCD came in first, so we determine the start time here.
594
-
595
- start_time = time_since_epoch_1958(format_datetime(precision=6, width=9)) # Now (data will certainly be older)
596
- date_obs = None
597
-
598
- for ccd_number in selected_ccds:
599
-
600
- for ccd_side in fee_side:
601
-
602
- try:
603
-
604
- finetime = level1[f"IMAGE_{ccd_number}_{ccd_side.name[0]}", 0].header["FINETIME"]
605
-
606
- if finetime < start_time:
607
-
608
- start_time = finetime
609
- date_obs = level1[f"IMAGE_{ccd_number}_{ccd_side.name[0]}", 0].header["DATE-OBS"]
610
-
611
- side_is_present[ccd_number][ccd_side] = True
612
-
613
- except KeyError:
614
-
615
- side_is_present[ccd_number][ccd_side] = False
616
-
617
- primary_hdu = fits.PrimaryHDU()
618
- primary_header["DATE-OBS"] = (date_obs, "Timestamp for 1st frame",)
619
- primary_header["FINETIME"] = (start_time, "Finetime representation of DATE-OBS",)
620
- primary_header["LEVEL"] = 2 # Cube structure
621
- primary_hdu.header = primary_header
622
- primary_hdu.writeto(cube_filename)
623
-
624
- for ccd_number in selected_ccds:
625
-
626
- for ccd_side in fee_side:
627
-
628
- if side_is_present[ccd_number][ccd_side]:
629
-
630
- # Image
631
-
632
- images = []
633
- time_axis = np.array([])
634
-
635
- exposure = 0
636
-
637
- while True:
638
-
639
- try:
640
-
641
- slice = level1[f"IMAGE_{ccd_number}_{ccd_side.name[0]}", exposure]
642
-
643
- time = time_since_epoch_1958(slice.header["DATE-OBS"])
644
- time_axis = np.append(time_axis, time)
645
-
646
- images.append(slice.data)
647
-
648
- exposure += 1
649
-
650
- except KeyError:
651
-
652
- break
653
-
654
- image_cube = np.stack(images)
655
- del images
656
-
657
- time_axis -= start_time
658
- time_column = fits.Column("TIME", format="F", array=time_axis)
659
- time_table = fits.BinTableHDU.from_columns([time_column])
660
- time_table.header["EXTNAME"] = f"WCS-TAB_{ccd_number}_{ccd_side.name[0]}"
661
-
662
- fits.append(cube_filename, time_table.data, time_table.header)
663
- fits.append(filename, time_table.data, time_table.header)
664
-
665
- image_cube_header = level1[f"IMAGE_{ccd_number}_{ccd_side.name[0]}", 0].header
666
- image_cube_header["NAXIS"] = (3, f"Dimensionality of the image cube ({ccd_side.name[0]}-side)",)
667
- image_cube_header["NAXIS3"] = exposure
668
- image_cube_header["CRPIX3"] = 1
669
- image_cube_header["CRVAL3"] = start_time
670
- image_cube_header["CTYPE3"] = "TIMETAB"
671
- image_cube_header["CUNIT3"] = "s"
672
- image_cube_header["PS3_0"] = f"WCS-TAB_{ccd_number}_{ccd_side.name[0]}"
673
- image_cube_header["PS3_1"] = "TIME"
674
-
675
- fits.append(cube_filename, image_cube, image_cube_header)
676
-
677
- # Serial pre-scan
678
-
679
- serial_prescans = []
680
-
681
- exposure = 0
682
-
683
- while True:
684
-
685
- try:
686
-
687
- serial_prescans.append(level1[f"SPRE_{ccd_number}_{ccd_side.name[0]}", exposure].data)
688
- exposure += 1
689
-
690
- except KeyError:
691
-
692
- break
693
-
694
- serial_prescan_cube = np.stack(serial_prescans)
695
- del serial_prescans
696
-
697
- serial_prescan_cube_header = level1[f"SPRE_{ccd_number}_{ccd_side.name[0]}", 0].header
698
- serial_prescan_cube_header["NAXIS"] = (3, f"Dimensionality of the serial pre-scan cube ({ccd_side.name[0]}-side)",)
699
- serial_prescan_cube_header["NAXIS3"] = exposure
700
- serial_prescan_cube_header["CRPIX3"] = 1
701
- serial_prescan_cube_header["CRVAL3"] = start_time
702
- serial_prescan_cube_header["CTYPE3"] = "TIMETAB"
703
- serial_prescan_cube_header["CUNIT3"] = "s"
704
- serial_prescan_cube_header["PS3_0"] = f"WCS-TAB_{ccd_number}_{ccd_side.name[0]}"
705
- serial_prescan_cube_header["PS3_1"] = "TIME"
706
-
707
- fits.append(cube_filename, serial_prescan_cube, serial_prescan_cube_header)
708
-
709
- # Serial over-scan
710
-
711
- if has_serial_overscan:
712
-
713
- serial_overscans = []
714
- exposure = 0
715
-
716
- while True:
717
-
718
- try:
719
-
720
- serial_overscans.append(level1[f"SOVER_{ccd_number}_{ccd_side.name[0]}", exposure].data)
721
- exposure += 1
722
-
723
- except KeyError:
724
-
725
- break
726
-
727
- serial_overscan_cube = np.stack(serial_overscans)
728
- del serial_overscans
729
-
730
- serial_overscan_cube_header = level1[f"SOVER_{ccd_number}_{ccd_side.name[0]}", 0].header
731
- serial_overscan_cube_header["NAXIS"] = (3, f"Dimensionality of the serial over-scan cube ({ccd_side.name[0]}-side)",)
732
- serial_overscan_cube_header["NAXIS3"] = exposure
733
- serial_overscan_cube_header["CRPIX3"] = 1
734
- serial_overscan_cube_header["CRVAL3"] = start_time
735
- serial_overscan_cube_header["CTYPE3"] = "TIMETAB"
736
- serial_overscan_cube_header["CUNIT3"] = "s"
737
- serial_overscan_cube_header["PS3_0"] = f"WCS-TAB_{ccd_number}_{ccd_side.name[0]}"
738
- serial_overscan_cube_header["PS3_1"] = "TIME"
739
-
740
- fits.append(cube_filename, serial_overscan_cube, serial_overscan_cube_header)
741
-
742
- # Parallel over-scan
743
-
744
- if has_parallel_overscan:
745
-
746
- parallel_overscans = []
747
- exposure = 0
748
-
749
- while True:
750
-
751
- try:
752
-
753
- parallel_overscans.append(level1[f"POVER_{ccd_number}_{ccd_side.name[0]}", exposure].data)
754
- exposure += 1
755
-
756
- except KeyError:
757
- break
758
-
759
- parallel_overscan_cube = np.stack(parallel_overscans)
760
- del parallel_overscans
761
-
762
- parallel_overscan_cube_header = level1[f"POVER_{ccd_number}_{ccd_side.name[0]}", 0].header
763
- parallel_overscan_cube_header["NAXIS"] = (3, f"Dimensionality of the parallel over-scan cube ({ccd_side.name[0]}-side)",)
764
- parallel_overscan_cube_header["NAXIS3"] = exposure
765
- parallel_overscan_cube_header["CRPIX3"] = 1
766
- parallel_overscan_cube_header["CRVAL3"] = start_time
767
- parallel_overscan_cube_header["CTYPE3"] = "TIMETAB"
768
- parallel_overscan_cube_header["CUNIT3"] = "s"
769
- parallel_overscan_cube_header["PS3_0"] = f"WCS-TAB_{ccd_number}_{ccd_side.name[0]}"
770
- parallel_overscan_cube_header["PS3_1"] = "TIME"
771
-
772
- fits.append(
773
- cube_filename, parallel_overscan_cube, parallel_overscan_cube_header
774
- )
775
-
776
- # Remove the level-1 FITS file
777
-
778
- LOGGER.info(f"Removing flat-structure FITS file {filename}")
779
- os.remove(filename)
780
-
781
-
782
- def is_incomplete(hdf5_file: File):
783
- """ Check whether the given HDF5 file is incomplete.
784
-
785
- The HDF5 files are created at the start of a cycle. The register map and (if applicable) the format version are
786
- stored at this point. If an observation starts "half way" a cycle, the register map will not be present.
787
-
788
- Args:
789
- - hdf5_file: HDF5 file.
790
-
791
- Returns: True if the given HDF5 file is incomplete (i.e. if the register map is not stored); False otherwise.
792
- """
793
-
794
- return "register" not in hdf5_file
795
-
796
-
797
- def is_corrupt(hdf5_file: File):
798
- """ Check whether the given HDF5 file is corrupt.
799
-
800
- Args:
801
- - hdf5_file: HDF5 file.
802
-
803
- Returns: True if an error flag is set in one of the groups; False otherwise.
804
- """
805
-
806
- for count in range(4):
807
-
808
- if f"/{count}/hk" in hdf5_file:
809
-
810
- hk_packet = SpaceWirePacket.create_packet(hdf5_file[f"/{count}/hk"][...])
811
- error_flags = HousekeepingData(hk_packet.data)['error_flags']
812
-
813
- if error_flags:
814
- return True
815
-
816
- return False
817
-
818
-
819
- def any_crucial_parameters_changed(prep: dict, n_fee_state: Mapping):
820
- """ Check whether there is a change in crucial parameters.
821
-
822
- Return True if any of the following parameters changed with respect to the revious check: v_start, v_end, h_end,
823
- rows_final_dump, ccd_mode_config, and ccd_readout_order.
824
-
825
- Args:
826
- - prep (dict): Current values for the crucial parameters.
827
- - n_fee_state: N-FEE state parameters or register map.
828
-
829
- Returns: True if any of the values have changed, False otherwise.
830
- """
831
-
832
- v_start = n_fee_state['v_start']
833
- v_end = n_fee_state['v_end']
834
- h_end = n_fee_state['h_end']
835
- rows_final_dump = n_fee_state['n_final_dump']
836
- ccd_mode_config = n_fee_state['ccd_mode_config']
837
- ccd_readout_order = n_fee_state['ccd_readout_order']
838
- ccd_readout_order = convert_ccd_order_value(ccd_readout_order)
839
-
840
- for x, y in dict(
841
- v_start=v_start, v_end=v_end, h_end=h_end, rows_final_dump=rows_final_dump,
842
- ccd_mode_config=ccd_mode_config, ccd_readout_order=ccd_readout_order,
843
- ).items():
844
- if prep.get(x) != y:
845
- LOGGER.debug(f"{x=}, {prep.get(x)=}, {y=}")
846
- return True
847
-
848
- return False
849
-
850
-
851
- def in_data_acquisition_mode(n_fee_state: Mapping):
852
- """ Check whether the N-FEE is in data acquisition mode.
853
-
854
- Args:
855
- - n_fee_state: N-FEE state parameters or register map.
856
-
857
- Returns: True if the N-FEE is in imaging mode (full-image (pattern) mode, windowing (pattern) mode, or
858
- parallel/serial trap pumping mode (1/2)) and the digitised data is transferred to the N-DPU.
859
- """
860
-
861
- ccd_mode_config = n_fee_state["ccd_mode_config"]
862
- digitise_en = n_fee_state["digitise_en"]
863
-
864
- return ccd_mode_config in [n_fee_mode.FULL_IMAGE_MODE, n_fee_mode.FULL_IMAGE_PATTERN_MODE,
865
- n_fee_mode.PARALLEL_TRAP_PUMPING_1_MODE, n_fee_mode.PARALLEL_TRAP_PUMPING_2_MODE,
866
- n_fee_mode.SERIAL_TRAP_PUMPING_1_MODE, n_fee_mode.SERIAL_TRAP_PUMPING_2_MODE,
867
- n_fee_mode.WINDOWING_PATTERN_MODE, n_fee_mode.WINDOWING_MODE] and digitise_en
868
-
869
-
870
- def construct_cube_filename(fits_filename: str) -> str:
871
- """ Construct the filename for the level-2 FITS file.
872
-
873
- The level-2 FITS file will have the data arranged in cubes, rather than in a flat structure.
874
-
875
- Args:
876
- - fits_filename: Filename for the level-1 FITS file. The level-1 FITS files has the data arranged in a flat
877
- structure.
878
-
879
- Returns: Filename for the level-2 FITS file.
880
- """
881
-
882
- LOGGER.info(f"Construct cube filename from {fits_filename}")
883
-
884
- # LOGGER.info(f"Images: {'images' in fits_filename}")
885
-
886
- if "images" in fits_filename:
887
- return fits_filename.replace("images", "cube")
888
-
889
- else:
890
- prefix, suffix = str(fits_filename).rsplit('_', 1)
891
- return f"{prefix}_cube_{suffix}"
892
-
893
-
894
- def construct_images_filename(hdf5_filename: PosixPath, obsid: ObservationIdentifier = None,
895
- location=None, camera_name: str = None):
896
- """ Construct the filename for the level-1 FITS file.
897
-
898
-
899
- The level-1 FITS files has the data arranged in a flat structure.
900
-
901
- Args:
902
- - identifier (str): Identifier for the source of the data, this string is usually what is sent in the `origin`
903
- of the item dictionary.
904
- - ext (str): File extension: this depends on the persistence class that is used for storing the data.
905
- - obsid (ObservationIdentifier): Unique identifier for the observation (LAB_SETUP_TEST).
906
- - use_counter: Indicates whether or not a counter should be included in the filename.
907
- - location: Folder (with /daily and /obs sub-folders) in which the FITS files should be written (in a
908
- dedicated directory in the /obs folder). If not specified, the `PLATO_DATA_STORAGE_LOCATION`
909
- environment variable will be used to construct the location..
910
-
911
- Returns: Full path to the file as a `PurePath`.
912
- """
913
-
914
- location = location or get_data_storage_location()
915
-
916
- if obsid is None:
917
-
918
- timestamp, site_id, _, _, counter = str.split(str.split(str(hdf5_filename), ".")[0], "_")
919
- fits_filename = f"{timestamp}_{site_id}_{N_FEE_SETTINGS.ORIGIN_CCD_DATA}_{counter}_images.{FITS.extension}"
920
-
921
- location += "/daily/"
922
-
923
- return str(Path(location) / timestamp / fits_filename)
924
-
925
- else:
926
-
927
- # Make sure that the FITS file ends up in the correct sub-folder
928
- # - oldest data: TEST_LAB_SETUP
929
- # - more recent data: TEST_LAB
930
-
931
- obsid = obsid_from_storage(obsid, data_dir=location, camera_name=camera_name)
932
-
933
- location += "/obs/"
934
- dpu_filename = find_file(f"{obsid}_DPU_*.csv", root=f"{hdf5_filename.parents[2]}/obs/{obsid}")
935
- timestamp = str(dpu_filename).split("_")[-2]
936
-
937
- if not os.path.isdir(f"{location}/{obsid}"):
938
- os.makedirs(f"{location}/{obsid}")
939
- location += f"{obsid}/"
940
-
941
- # Determine the filename
942
-
943
- pattern = f"{obsid}_{N_FEE_SETTINGS.ORIGIN_CCD_DATA}_*_{timestamp}_cube.{FITS.extension}"
944
- counter = get_fits_counter(location, pattern)
945
-
946
- fits_filename = f"{obsid}_{N_FEE_SETTINGS.ORIGIN_CCD_DATA}_{counter:05d}_{timestamp}_images.{FITS.extension}"
947
-
948
- return str(Path(location) / fits_filename)
949
-
950
-
951
- def get_fits_counter(location, pattern):
952
- """ Determine counter for a new FITS file at the given location and with the given pattern.
953
-
954
- Args:
955
- - location: Location where the FITS file should be stored.
956
- - pattern: Pattern for the filename.
957
-
958
- Returns: Value of the next counter; 1 if no previous files were found or if an error occurred.
959
- """
960
-
961
- LOGGER.debug(f"Pattern: {pattern=}")
962
- LOGGER.debug(f"Location: {location=}")
963
-
964
- files = sorted(find_files(pattern=pattern, root=location))
965
-
966
- # No filenames found showing the given pattern -> start counting at 1
967
-
968
- LOGGER.debug(f"Number of matches: {len(files)=}")
969
-
970
- if len(files) == 0:
971
- return 1
972
-
973
- last_file = files[-1]
974
-
975
- counter = last_file.name.split("_")
976
-
977
- LOGGER.debug(f"{counter = }")
978
-
979
- try:
980
-
981
- # Observation files have the following pattern:
982
- # <test ID>_<lab ID>_N-FEE_CCD_<counter>_<day YYYYmmdd>_cube.fits
983
-
984
- counter = int(counter[-3]) + 1
985
- LOGGER.debug(f"{counter = }")
986
- return counter
987
-
988
- except ValueError:
989
-
990
- LOGGER.warning("ValueError", exc_info=True)
991
- return 1
992
-
993
-
994
- def create_fits_from_hdf5(files: List, location: str = None, setup: Setup = None):
995
- """ Off-line generation of FITS files from HDF5 files with SpW packets.
996
-
997
- When there is a change in crucial parameters, the current FITS file (if any) will be closed and a new one will be
998
- created as soon and HDF5 file with data content is encountered (when the N-FEE is in full-image or full-image
999
- pattern mode):
1000
-
1001
- - If there is a change in crucial parameters, close the current FITS file (if any).
1002
- - If there is a change in crucial parameter and the N-FEE is in full-image mode or in full-image pattern
1003
- mode, or the N-FEE goes to full-image mode or full-image pattern mode, a new FITS file will be created.
1004
- - The content of the HDF5 files will be extracted and passed to the FITS persistence layer as SpW packets.
1005
-
1006
- In the older HDF5 files, only the register map is stored, which does not always reflect the actual N-FEE state.
1007
- This is solved in the later version of the HDF5 files (format version >= 2.0). In these files, the current N-FEE
1008
- state is stored in each of the data groups.
1009
-
1010
- It's possible that the first file in the list is incomplete, because it was already created by the time the
1011
- current observation started. That file
1012
-
1013
- Args:
1014
- - files: List of filenames of the HDF5 files to use to create the FITS file.
1015
- - location: Folder (with /daily and /obs sub-folders) in which the FITS files should be written (in a
1016
- dedicated directory in the /obs folder). If not specified, the `PLATO_DATA_STORAGE_LOCATION`
1017
- environment variable will be used to construct the location.
1018
- - setup: Setup to retrieve information from.
1019
- """
1020
-
1021
- location = location or get_data_storage_location()
1022
-
1023
- # Loop over the filenames. When you encounter an HDF5 file, check its format version.
1024
-
1025
- for filename in files:
1026
-
1027
- filename = Path(filename)
1028
-
1029
- if filename.suffix == f".{HDF5.extension}":
1030
-
1031
- try:
1032
-
1033
- with h5.get_file(filename, mode="r", locking=False) as hdf5_file:
1034
-
1035
- # It happens that some of the HDF5 files are incomplete. These should not be considered to determine
1036
- # whether the register map (original format version) or the N-FEE state (format version >= 2.0) to
1037
- # determine the state of the crucial parameters.
1038
-
1039
- if is_incomplete(hdf5_file): # or is_corrupt(hdf5_file):
1040
-
1041
- files = files[1:]
1042
-
1043
- else:
1044
-
1045
- # The N-FEE state is stored in the data groups of the HDF5 files (format version >= 2.0)
1046
-
1047
- if "versions" in hdf5_file:
1048
-
1049
- version_attrs = hdf5_file["versions"]["format_version"].attrs
1050
-
1051
- if version_attrs["major_version"] == 2:
1052
- create_fits_from_hdf5_nfee_state(files, location=location, setup=setup)
1053
- break
1054
-
1055
- else:
1056
-
1057
- version = f"{version_attrs['major_version']}.{version_attrs['minor_version']}"
1058
-
1059
- raise AttributeError(f"HDF5 file format version {version} cannot be handled by the FITS generator")
1060
-
1061
- # The register map is stored (globally) in the HDF5 files
1062
-
1063
- else:
1064
- create_fits_from_hdf5_register_map(files, location=location, setup=setup)
1065
- break
1066
-
1067
- except RuntimeError as exc:
1068
- LOGGER.debug(f"Unable to open HDF5 file: {exc}")
1069
-
1070
-
1071
- def create_fits_from_hdf5_register_map(files: List, location: str = None, setup: Setup = None):
1072
- """ Off-line generation of FITS files from HDF5 files with SpW packets.
1073
-
1074
- When there is a change in crucial parameters, the current FITS file (if any) will be closed and a new one will be
1075
- created as soon and HDF5 file with data content is encountered (when the N-FEE is in full-image or full-image
1076
- pattern mode):
1077
-
1078
- - If there is a change in crucial parameters, close the current FITS file (if any).
1079
- - If there is a change in crucial parameter and the N-FEE is in full-image mode or in full-image pattern
1080
- mode, or the N-FEE goes to full-image mode or full-image pattern mode, a new FITS file will be created.
1081
- - The content of the HDF5 files will be extracted and passed to the FITS persistence layer as SpW packets.
1082
-
1083
- In the given HDF5 files, only the register map is stored, which does not always reflect the actual N-FEE state. As
1084
- a result not all data may be present in the generated FITS files (e.g. because the register map says the N-FEE is
1085
- already in dump mode) or the data might be split over more FITS files than expected (e.g. because the v_start and
1086
- v_end parameters are already / not yet changed in the register map but not in the N-FEE state).
1087
-
1088
- Note that this problem is solved in the later version of the HDF5 files (format version >= 2.0).
1089
-
1090
- Args:
1091
- - files: List of filenames of the HDF5 files to use to create the FITS file.
1092
- - location: Folder (with /daily and /obs sub-folders) in which the FITS files should be written (in a
1093
- dedicated directory in the /obs folder). If not specified, the `PLATO_DATA_STORAGE_LOCATION`
1094
- environment variable will be used to construct the location.
1095
- - setup: Setup to retrieve information from.
1096
- """
1097
- setup = setup or load_setup()
1098
-
1099
- location = location or get_data_storage_location()
1100
- hdf5_file_root = Path(files[0]).parent.parent.parent
1101
- sensor_sel_enum = setup.camera.fee.sensor_sel.enum
1102
- camera_name = setup.camera.ID
1103
-
1104
- prep = {}
1105
- fits_filename = None
1106
-
1107
- for filename in files:
1108
-
1109
- filename = Path(filename)
1110
-
1111
- if filename.suffix == '.hdf5':
1112
-
1113
- print(f"Processing {filename=!s}...")
1114
-
1115
- try:
1116
-
1117
- with h5.get_file(filename, mode="r", locking=False) as hdf5_file:
1118
-
1119
- # if is_corrupt(hdf5_file):
1120
- # LOGGER.warning(f"Skipping {filename} (corrupt)")
1121
- #
1122
- # else:
1123
-
1124
- if 'register' not in hdf5_file:
1125
- LOGGER.warning(f"No register map found for {filename=!s}, continue with next file..")
1126
- continue # next HDF5 file
1127
-
1128
- register_map = RegisterMap("N-FEE", memory_map=h5.get_data(hdf5_file["register"]))
1129
-
1130
- has_data = False
1131
-
1132
- for group in h5.groups(hdf5_file):
1133
-
1134
- if "data" in group.keys():
1135
-
1136
- has_data = True
1137
-
1138
- # Should a new FITS file be created?
1139
-
1140
- if any_crucial_parameters_changed(prep, register_map):
1141
-
1142
- if fits_filename:
1143
-
1144
- LOGGER.info(f"Creating a FITS CUBE file ...")
1145
- convert_to_cubes(fits_filename, setup)
1146
- fits_filename = None
1147
-
1148
- if in_data_acquisition_mode(register_map):
1149
-
1150
- if fits_filename is None:
1151
-
1152
- LOGGER.info(f"A new FITS file will be created...")
1153
-
1154
- # Start writing to a new FITS file
1155
- # Collect all information to sent to the FITS layer
1156
-
1157
- if "obsid" in hdf5_file:
1158
- obsid = hdf5_file["obsid"][()].decode()
1159
- obsid = ObservationIdentifier.create_from_string(obsid, order=LAB_SETUP_TEST)
1160
- else:
1161
- obsid = None
1162
-
1163
- fits_filename = construct_images_filename(
1164
- filename, obsid, location=location, camera_name=camera_name
1165
- )
1166
- LOGGER.info(f"{fits_filename = !s}")
1167
-
1168
- ccd_readout_order = register_map['ccd_readout_order']
1169
- ccd_readout_order = convert_ccd_order_value(ccd_readout_order)
1170
-
1171
- prep = {
1172
- "v_start": register_map['v_start'],
1173
- "v_end": register_map['v_end'],
1174
- "h_end": register_map['h_end'],
1175
- "rows_final_dump": register_map['n_final_dump'],
1176
- "ccd_mode_config": register_map['ccd_mode_config'],
1177
- "ccd_readout_order": ccd_readout_order, # CCD numbering [1-4]
1178
- "expected_last_packet_flags": get_expected_last_packet_flags(register_map,
1179
- sensor_sel_enum),
1180
- "obsid": str(obsid) if obsid is not None else None,
1181
- "cycle_time": get_cycle_time(register_map, obsid=obsid, data_dir=hdf5_file_root),
1182
- "cgse_version": get_cgse_version(obsid=obsid, data_dir=hdf5_file_root),
1183
- "setup": setup,
1184
- "register_map": register_map
1185
- }
1186
-
1187
- persistence = FITS(str(fits_filename), prep)
1188
- persistence.open()
1189
-
1190
- # See https://github.com/IvS-KULeuven/plato-common-egse/issues/901
1191
- # timecode = group["timecode"]
1192
- # spw_packet = SpaceWirePacket.create_packet(h5.get_data(timecode))
1193
-
1194
- timestamp = group["timecode"].attrs["timestamp"]
1195
- persistence.create({"Timestamp": timestamp})
1196
-
1197
- data = group["data"]
1198
- sorted_datasets = natsort.natsorted(data.items(), key=lambda x: x[0])
1199
-
1200
- persistence.expected_last_packet_flags = get_expected_last_packet_flags(register_map,
1201
- sensor_sel_enum)
1202
-
1203
- for identifier, dataset in sorted_datasets:
1204
-
1205
- spw_packet = SpaceWirePacket.create_packet(h5.get_data(dataset))
1206
- # LOGGER.debug(f"{spw_packet.type = !s}")
1207
- persistence.create({f"SpW packet {identifier}": spw_packet})
1208
-
1209
- if not has_data:
1210
-
1211
- if fits_filename:
1212
-
1213
- LOGGER.info(f"Creating a FITS CUBE file ...")
1214
- convert_to_cubes(fits_filename, setup)
1215
- fits_filename = None
1216
-
1217
- prep = clear_crucial_parameters(prep)
1218
-
1219
- except RuntimeError as exc:
1220
- LOGGER.debug(f"Unable to open HDF5 file: {exc}")
1221
- else:
1222
- print(f"Skipping {filename=}")
1223
-
1224
- try:
1225
- if fits_filename:
1226
- LOGGER.info(f"Creating a FITS CUBE file ...")
1227
- convert_to_cubes(fits_filename, setup)
1228
- except OSError:
1229
- # The last file in the list still contained data, so we reached the end of the list without creating a cube
1230
- # FITS file yet
1231
- pass
1232
-
1233
-
1234
- def create_fits_from_hdf5_nfee_state(files: List, location: str = None, setup: Setup = None):
1235
- """ Off-line generation of FITS files from HDF5 files with SpW packets.
1236
-
1237
- When there is a change in crucial parameters, the current FITS file (if any) will be closed and a new one will be
1238
- created as soon and HDF5 file with data content is encountered (when the N-FEE is in full-image or full-image
1239
- pattern mode):
1240
-
1241
- - If there is a change in crucial parameters, close the current FITS file (if any).
1242
- - If there is a change in crucial parameter and the N-FEE is in full-image mode or in full-image pattern
1243
- mode, or the N-FEE goes to full-image mode or full-image pattern mode, a new FITS file will be created.
1244
- - The content of the HDF5 files will be extracted and passed to the FITS persistence layer as SpW packets.
1245
-
1246
- In the given HDF5 files, the N-FEE state is saved in all data groups, reflecting the actual N-FEE state (i.e.
1247
- solving the problem of the mismatch between the register map and the N-FEE state).
1248
-
1249
- Args:
1250
- - files: List of filenames of the HDF5 files to use to create the FITS file.
1251
- - location: Folder (with /daily and /obs sub-folders) in which the FITS files should be written (in a
1252
- dedicated directory in the /obs folder). If not specified, the `PLATO_DATA_STORAGE_LOCATION`
1253
- environment variable will be used to construct the location.
1254
- - setup: Setup to retrieve information from, if not provided, the setup is loaded from the
1255
- configuration manager..
1256
- """
1257
- setup = setup or load_setup()
1258
-
1259
- location = location or get_data_storage_location()
1260
- hdf5_file_root = Path(files[0]).parent.parent.parent
1261
- sensor_sel_enum = setup.camera.fee.sensor_sel.enum
1262
- camera_name = setup.camera.ID
1263
-
1264
- config_slicing_num_cycles = 0 # Configured slicing parameter
1265
- processed_num_cycles = 0 # HDF5 files with image data processed for current FITS file
1266
-
1267
- prep = {}
1268
- fits_filename = None
1269
-
1270
- for filename in files:
1271
-
1272
- filename = Path(filename)
1273
-
1274
- if filename.suffix == '.hdf5':
1275
-
1276
- print(f"Processing {filename=!s}...")
1277
-
1278
- try:
1279
-
1280
- with h5.get_file(filename, mode="r", locking=False) as hdf5_file:
1281
-
1282
- # Slicing
1283
-
1284
- try:
1285
- slicing_num_cycles = hdf5_file["dpu"].attrs["slicing_num_cycles"]
1286
- if slicing_num_cycles != config_slicing_num_cycles:
1287
- if fits_filename:
1288
- convert_to_cubes(fits_filename, setup)
1289
- fits_filename = None
1290
- processed_num_cycles = 0
1291
- config_slicing_num_cycles = slicing_num_cycles
1292
- except KeyError:
1293
- config_slicing_num_cycles = 0
1294
-
1295
- register_map = RegisterMap("N-FEE", memory_map=h5.get_data(hdf5_file["register"]))
1296
-
1297
- # if is_corrupt(hdf5_file):
1298
- # LOGGER.warning(f"Skipping {filename} (corrupt)")
1299
- #
1300
- # else:
1301
-
1302
- has_data = False
1303
-
1304
- for group in h5.groups(hdf5_file):
1305
-
1306
- if "data" in group.keys():
1307
-
1308
- has_data = True
1309
-
1310
- n_fee_state = group["data"].attrs
1311
-
1312
- # Should a new FITS file be created?
1313
-
1314
- if any_crucial_parameters_changed(prep, n_fee_state):
1315
-
1316
- if fits_filename:
1317
-
1318
- LOGGER.info(f"Creating a FITS CUBE file ...")
1319
- convert_to_cubes(fits_filename, setup)
1320
- fits_filename = None
1321
- processed_num_cycles = 0
1322
-
1323
- if in_data_acquisition_mode(n_fee_state):
1324
-
1325
- if fits_filename is None:
1326
-
1327
- LOGGER.info(f"A new FITS file will be created...")
1328
-
1329
- # Start writing to a new FITS file
1330
- # Collect all information to sent to the FITS layer
1331
-
1332
- if "obsid" in hdf5_file:
1333
- obsid = hdf5_file["obsid"][()].decode()
1334
- obsid = ObservationIdentifier.create_from_string(obsid, order=LAB_SETUP_TEST)
1335
- else:
1336
- obsid = None
1337
-
1338
- fits_filename = construct_images_filename(
1339
- filename, obsid, location=location, camera_name=camera_name
1340
- )
1341
- LOGGER.info(f"{fits_filename = !s}")
1342
-
1343
- ccd_readout_order = n_fee_state['ccd_readout_order']
1344
- ccd_readout_order = convert_ccd_order_value(ccd_readout_order)
1345
-
1346
- prep = {
1347
- "v_start": n_fee_state['v_start'],
1348
- "v_end": n_fee_state['v_end'],
1349
- "h_end": n_fee_state['h_end'],
1350
- "rows_final_dump": n_fee_state['n_final_dump'],
1351
- "ccd_mode_config": n_fee_state['ccd_mode_config'],
1352
- "ccd_readout_order": ccd_readout_order, # CCD numbering [1-4]
1353
- "expected_last_packet_flags": get_expected_last_packet_flags(n_fee_state,
1354
- sensor_sel_enum),
1355
- "obsid": str(obsid) if obsid is not None else None,
1356
- "cycle_time": get_cycle_time(n_fee_state, obsid=obsid, data_dir=hdf5_file_root),
1357
- "cgse_version": get_cgse_version(obsid=obsid, data_dir=hdf5_file_root),
1358
- "setup": setup,
1359
- "register_map": register_map
1360
- }
1361
-
1362
- persistence = FITS(str(fits_filename), prep)
1363
- persistence.open()
1364
-
1365
- # See https://github.com/IvS-KULeuven/plato-common-egse/issues/901
1366
- # timecode = group["timecode"]
1367
- # spw_packet = SpaceWirePacket.create_packet(h5.get_data(timecode))
1368
-
1369
- timestamp = group["timecode"].attrs["timestamp"]
1370
- persistence.create({"Timestamp": timestamp})
1371
-
1372
- data = group["data"]
1373
- sorted_datasets = natsort.natsorted(data.items(), key=lambda x: x[0])
1374
-
1375
- persistence.expected_last_packet_flags = get_expected_last_packet_flags(n_fee_state,
1376
- sensor_sel_enum)
1377
-
1378
- for identifier, dataset in sorted_datasets:
1379
-
1380
- spw_packet = SpaceWirePacket.create_packet(h5.get_data(dataset))
1381
- # LOGGER.debug(f"{spw_packet.type = !s}")
1382
- persistence.create({f"SpW packet {identifier}": spw_packet})
1383
-
1384
- if has_data:
1385
- processed_num_cycles += 1
1386
-
1387
- if fits_filename and config_slicing_num_cycles != 0 \
1388
- and processed_num_cycles == config_slicing_num_cycles:
1389
- convert_to_cubes(fits_filename, setup)
1390
- fits_filename = None
1391
- processed_num_cycles = 0
1392
- else:
1393
-
1394
- if fits_filename:
1395
- LOGGER.info(f"Creating a FITS CUBE file ...")
1396
- convert_to_cubes(fits_filename, setup)
1397
- fits_filename = None
1398
- processed_num_cycles = 0
1399
-
1400
- prep = clear_crucial_parameters(prep)
1401
-
1402
- except RuntimeError as exc:
1403
- LOGGER.debug(f"Unable to open HDF5 file: {exc}")
1404
- else:
1405
- print(f"skipping {filename=}")
1406
-
1407
- try:
1408
- if fits_filename:
1409
- LOGGER.info(f"Creating a FITS CUBE file ...")
1410
- convert_to_cubes(fits_filename, setup)
1411
- except OSError:
1412
- # The last file in the list still contained data, so we reached the end of the list without creating a cube
1413
- # FITS file yet
1414
- pass
1415
-
1416
-
1417
- def clear_crucial_parameters(prep: dict):
1418
- """ Clear the crucial parameters from the given dictionary.
1419
-
1420
- Args:
1421
- - prep: Dictionary with crucial parameters.
1422
-
1423
- Returns: Dictionary with the cleared crucial parameters.
1424
- """
1425
-
1426
- prep["v_start"] = None
1427
- prep["v_end"] = None
1428
- prep["h_end"] = None
1429
- prep["rows_final_dump"] = None
1430
- prep["ccd_mode_config"] = None
1431
- prep["ccd_readout_order"] = None
1432
-
1433
- return prep
1434
-
1435
-
1436
- class SynopticsFwdFill(tuple, Enum):
1437
- """ Enumeration of the synoptics to forward fill.
1438
-
1439
- This is only applicable for the commanded source position.
1440
- """
1441
-
1442
- # Source position (commanded)
1443
-
1444
- THETA_CMD = ("GSYN_CMD_THETA", "Commanded source position theta [deg]")
1445
- PHI_CMD = ("GSYN_CMD_PHI", "Commanded source position phi [deg]")
1446
-
1447
-
1448
- class SynopticsInterp1d(tuple, Enum):
1449
- """ Enumeration of the synoptics to linearly interpolate.
1450
-
1451
- This is only applicable for:
1452
- - calibrated TCS temperatures;
1453
- - calibrated N-FEE temperatures (TOU + CCDs + and board sensors);
1454
- - selection of TH DAQ(s) temperatures;
1455
- - OGSE attenuation (relative intensity + FWC fraction for the OGSE);
1456
- - actual source position.
1457
- """
1458
-
1459
- # TCS temperatures
1460
-
1461
- T_TRP1 = ("GSYN_TRP1", "Mean T for TOU TRP1 (TCS) [deg C]")
1462
- T_TRP22 = ("GSYN_TRP22", "Mean T for FEE TRP22 (TCS) [deg C]")
1463
-
1464
- # TOU TRP PT1000 sensors (N-FEE)
1465
-
1466
- T_TRP5 = ("GSYN_TRP5", "Mean T for TRP5 (TOU baffle ring) [deg C]")
1467
- T_TRP6 = ("GSYN_TRP6", "Mean T for TRP6 (FPA I/F) [deg C]")
1468
- T_TRP8 = ("GSYN_TRP8", "Mean T for TRP8 (L3) [deg C]")
1469
- T_TRP21 = ("GSYN_TRP21", "Mean T for TRP21 (TOU bipod +X bottom) [deg C]")
1470
- T_TRP31 = ("GSYN_TRP31", "Mean T for TRP31 (TOU bipod -Y bottom) [deg C]")
1471
- T_TRP41 = ("GSYN_TRP41", "Mean T for TRP41 (TOU bipod +Y bottom) [deg C]")
1472
-
1473
- # CCD PT100/PT1000 sensors (N-FEE)
1474
-
1475
- T_CCD1 = ("GSYN_CCD1", "Mean T for CCD1 [deg C]")
1476
- T_CCD2 = ("GSYN_CCD2", "Mean T for CCD2 [deg C]")
1477
- T_CCD3 = ("GSYN_CCD3", "Mean T for CCD3 [deg C]")
1478
- T_CCD4 = ("GSYN_CCD4", "Mean T for CCD4 [deg C]")
1479
-
1480
- T_CCD1_AMB = ("GSYN_CCD1_AMB", "Mean T for CCD1 (ambient calibration) [deg C]")
1481
- T_CCD2_AMB = ("GSYN_CCD2_AMB", "Mean T for CCD2 (ambient calibration) [deg C]")
1482
- T_CCD3_AMB = ("GSYN_CCD3_AMB", "Mean T for CCD3 (ambient calibration) [deg C]")
1483
- T_CCD4_AMB = ("GSYN_CCD4_AMB", "Mean T for CCD4 (ambient calibration) [deg C]")
1484
-
1485
- # Board sensors: type PT1000 (N-FEE)
1486
-
1487
- T_PCB1 = ("GSYN_NFEE_T_PCB1", "Mean T for board sensor PCB1 [deg C]")
1488
- T_PCB2 = ("GSYN_NFEE_T_PCB2", "Mean T for board sensor PCB2 [deg C]")
1489
- T_PCB3 = ("GSYN_NFEE_T_PCB3", "Mean T for board sensor PCB3 [deg C]")
1490
- T_PCB4 = ("GSYN_NFEE_T_PCB4", "Mean T for board sensor PCB4 [deg C]")
1491
-
1492
- # Board sensors: type ISL71590
1493
-
1494
- T_ADC = ("GSYN_NFEE_T_ADC", "Mean ADC board T [deg C]")
1495
- T_CDS = ("GSYN_NFEE_T_CDS", "Mean CDS board T [deg C]")
1496
- T_ANALOG = ("GSYN_NFEE_T_ANALOG", "Mean analog board T [deg C]")
1497
- T_SKYSHROUD = ("GSYN_SKYSHROUD", "Mean front shroud T [deg C]")
1498
- T_TEB_TOU = ("GSYN_TEB_TOU", "Mean TEB TOU T [deg C]")
1499
- T_TEB_FEE = ("GSYN_TEB_FEE", "Mean TEB FEE T [deg C]")
1500
-
1501
- # Temperatures from the TH DAQ
1502
-
1503
- T_TRP2 = ("GSYN_TRP2", "Mean T for TRP2 (MaRi bipod +X I/F) [deg C]")
1504
- T_TRP3 = ("GSYN_TRP3", "Mean T for TRP3 (MaRi bipod -Y I/F) [deg C]")
1505
- T_TRP4 = ("GSYN_TRP4", "Mean T for TRP4 (MaRi bipod +Y I/F) [deg C]")
1506
-
1507
- T_TRP7 = ("GSYN_TRP7", "Mean T for TRP7 (thermal strap) [deg C]")
1508
- T_TRP10 = ("GSYN_TRP10", "Mean T for TRP10 (FPA) [deg C]")
1509
-
1510
- # OGSE attenuation
1511
-
1512
- OGATT = ("GSYN_OGSE_REL_INTENSITY", "Relative OGSE intensity")
1513
- OGFWC = ("GSYN_OGSE_FWC_FRACTION", "OGSE FWC fraction")
1514
-
1515
- # Source position (actual)
1516
-
1517
- THETA = ("GSYN_ACT_THETA", "Actual source position theta [deg]")
1518
- PHI = ("GSYN_ACT_PHI", "Actual source position phi [deg]")
1519
-
1520
-
1521
- class SynopticsLeaveGaps(tuple, Enum):
1522
- """ Enumeration of the synoptics not to fill the gaps for.
1523
-
1524
- This is only applicable for the status of the shutter (open/closed). Note that there is no shutter in CSL, so we
1525
- indicate that the shutter is always open there.
1526
- """
1527
-
1528
- OGSHTTR = ("GSYN_OGSE_SHUTTER_OPEN", "Is the shutter open?")
1529
-
1530
-
1531
- def get_fits_synoptics(obsid: str, data_dir=None) -> dict:
1532
- """ Retrieve the synoptics that need to be included in the FITS files for the given observation.
1533
-
1534
- The synoptics that need to be included in the FITS files are represented by the following enumerations:
1535
-
1536
- - SynopticsFwdFill: Use forward filling for the gaps -> only at the beginning of the observation it is possible
1537
- that there still are gaps (but it is unlikely that the data acquisition has already started then);
1538
- - SynopticsInterp1d: Use linear interpolation to fill the gaps. At the extremes, we use extrapolation;
1539
- - SynopticsLeaveGaps: Don't fill the gaps.
1540
-
1541
- Args:
1542
- - obsid: Observation identifier [TEST_LAB or TEST_LAB_SETUP].
1543
-
1544
- Returns: Dictionary with the synoptics that should go into the FITS files for the given observation.
1545
- """
1546
-
1547
- synoptics_table = get_synoptics_table(obsid, data_dir=data_dir)
1548
-
1549
- # We keep the original timestamps (when filling the gaps)
1550
-
1551
- timestamps = synoptics_table["timestamp"].values
1552
- for index in range(len(timestamps)):
1553
- timestamps[index] = time_since_epoch_1958(timestamps[index])
1554
- timestamps = timestamps.astype(float)
1555
-
1556
- synoptics = {"timestamps": timestamps} # Don't forget to include the timestamps to the returned dictionary
1557
-
1558
- # Linear interpolation
1559
-
1560
- for syn_enum in SynopticsInterp1d:
1561
-
1562
- syn_name = syn_enum.value[0]
1563
-
1564
- if syn_name in synoptics_table:
1565
-
1566
- # We need to filter out the NaNs or the interpolation will not work
1567
-
1568
- values = synoptics_table[syn_name].values
1569
-
1570
- if len(values) > 0:
1571
- selection = ~np.isnan(values)
1572
-
1573
- if np.any(selection):
1574
- selected_timestamps = timestamps[np.where(selection)]
1575
- selected_values = values[np.where(selection)]
1576
-
1577
- if len(selected_timestamps) > 1:
1578
- interpolation = interp1d(selected_timestamps, selected_values, kind='linear',
1579
- fill_value='extrapolate')
1580
- synoptics[syn_enum] = interpolation(timestamps)
1581
-
1582
- # Forward fill
1583
-
1584
- for syn_enum in SynopticsFwdFill:
1585
-
1586
- syn_name = syn_enum.value[0]
1587
-
1588
- if syn_name in synoptics_table:
1589
- synoptics[syn_enum] = synoptics_table[syn_name].ffill()
1590
-
1591
- # Leave the gaps in
1592
-
1593
- for syn_enum in SynopticsLeaveGaps:
1594
-
1595
- syn_name = syn_enum.value[0]
1596
-
1597
- if syn_name in synoptics_table:
1598
- synoptics[syn_enum] = synoptics_table[syn_name]
1599
-
1600
- return synoptics
1601
-
1602
-
1603
- def add_synoptics(obsid: str, fits_dir: str, syn_dir: str, fee_side: EnumMeta):
1604
- """ Add synoptics to the FITS headers for the given observation.
1605
-
1606
- When all FITS files have been produced for the given obsid, synoptics is added to the headers. This is done in the
1607
- following steps:
1608
- - Determine which folder in the /obs directory comprises the HK and FITS files for the given obsid;
1609
- - Read the synoptics for the given obsid (from said folder) into a pandas DataFrame;
1610
- - Compose the list of FITS files for the given observation (from said folder);
1611
- - For all of these FITS files, loop over the cubes it contains and:
1612
- - Determine the time range covered by the cube;
1613
- - Select the synoptics (from the pandas DataFrame) over that time range;
1614
- - For the synoptical temperatures, source position (commanded + actual), and OGSE intensity: calculate
1615
- the average and add this to the header of the cube;
1616
- - For the shutter: calculate the mean and add this to the header of the cube.
1617
-
1618
- Args:
1619
- obsid: Observation identifier [TEST_LAB or TEST_LAB_SETUP]
1620
- fits_dir: Directory (with /daily and /obs sub-folders) with the FITS files
1621
- syn_dir: Directory (with /daily and /obs sub-folders) with the original synoptics files
1622
- fee_side: Enumeration with the definition of the FEE CCD sides
1623
- """
1624
-
1625
- fits_dir = fits_dir or get_data_storage_location()
1626
- syn_dir = syn_dir or get_data_storage_location()
1627
-
1628
- obsid = obsid_from_storage(obsid, data_dir=fits_dir)
1629
- obs_dir = f"{fits_dir}/obs/{obsid}" # Where the HK and FITS files are stored
1630
-
1631
- try:
1632
- synoptics = get_fits_synoptics(obsid, data_dir=fits_dir)
1633
- except FileNotFoundError:
1634
- synoptics = get_fits_synoptics(obsid, data_dir=syn_dir)
1635
- timestamps = synoptics["timestamps"] # Timestamps of the synoptics -> compare with absolute time in FITS file
1636
-
1637
- # Compose the list of FITS files for the given obsid
1638
-
1639
- pattern = f"{obsid}_{N_FEE_SETTINGS.ORIGIN_CCD_DATA}_*_*_cube.fits"
1640
- fits_filenames = sorted(find_files(pattern=pattern, root=obs_dir))
1641
-
1642
- # Loop over all FITS files (cubes) for the given obsid
1643
-
1644
- for fits_filename in fits_filenames:
1645
-
1646
- syn_info = {}
1647
-
1648
- # Loop over all image cubes
1649
-
1650
- with fits.open(fits_filename) as fits_file:
1651
-
1652
- start_time = fits_file["PRIMARY"].header["FINETIME"]
1653
-
1654
- # Loop over both sides of all CCDs (not all of them might be in -> hence the KeyError)
1655
-
1656
- for ccd_number in range(1, 5):
1657
-
1658
- for ccd_side in fee_side:
1659
-
1660
- try:
1661
- # Absolute time = time at the start of the readout
1662
- # = time at the end of the exposure
1663
- # -> Extract relative time from the WCS-TAB and add the DATE-OBS (which is the time of the
1664
- # 1st frame in the FITS file; all times in the WCS-TAB are relative to this)
1665
-
1666
- wcs_table_name = f"WCS-TAB_{ccd_number}_{ccd_side.name[0]}" # Holds relative time
1667
- absolute_time = np.array(fits_file[wcs_table_name].data["TIME"]) + start_time
1668
-
1669
- # We don't care about the 1st frame of any CCD side, as the image is saturated anyway, and it is
1670
- # very difficult to determine the start of that exposure anyway
1671
- # -> Simplest solution: indicate that the synoptics is unknown for those frames
1672
- # For all other frames:
1673
- # - Determine when the readout for the previous frame started -> start_previous_readout;
1674
- # - Determine when the readout for the current frame started -> start_current_readout;
1675
- # - For each synoptics parameter, gather the values acquired in the timespan
1676
- # [start_previous_readout, start_current_readout]
1677
- # - For the numerical values: take the mean (skipping the NaNs)
1678
- # - For the boolean values (i.c. status of the shutter):
1679
- # - Only NaN selected -> "U" (unknown)
1680
- # - Both True & False selected (potentially also NaNs) -> "M" (mixed)
1681
- # - Only True (potentially also NaNs) selected -> "T" (True = shutter open)
1682
- # - Only False (potentially also NaNs) selected -> "F" (False = shutter closed)
1683
- #
1684
- # For each synoptical parameter, first determine all the values that need to be included in the
1685
- # current cube of the current FITS file (it is only when we have composed these arrays, that we
1686
- # can included them in a table in the FITS file)
1687
-
1688
- fits_synoptics = {syn_enum: np.array([np.nan]) for syn_enum in chain(SynopticsFwdFill,
1689
- SynopticsInterp1d)}
1690
- fits_synoptics.update({syn_enum: np.array(["U"]) for syn_enum in SynopticsLeaveGaps})
1691
-
1692
- for index in range(1, len(absolute_time)):
1693
-
1694
- # Selection of the synoptics for the current frame: based on timestamps:
1695
- # - Start (start_previous_readout): start of the readout of the previous exposure
1696
- # - End (start_current_readout): start of the readout of the current exposure
1697
-
1698
- start_previous_readout = absolute_time[index - 1]
1699
- start_current_readout = absolute_time[index]
1700
-
1701
- selection = np.where(timestamps >= start_previous_readout) \
1702
- and np.where(timestamps <= start_current_readout)[0]
1703
-
1704
- # Average (skipping NaNs)
1705
-
1706
- for syn_enum in chain(SynopticsFwdFill, SynopticsInterp1d):
1707
- try:
1708
- selected_values = synoptics[syn_enum][selection]
1709
- average_value = np.nanmean(selected_values)
1710
-
1711
- fits_synoptics[syn_enum] = np.append(fits_synoptics[syn_enum], average_value)
1712
- except (KeyError, AttributeError):
1713
- fits_synoptics[syn_enum] = np.append(fits_synoptics[syn_enum], np.nan)
1714
-
1715
- for syn_enum in SynopticsLeaveGaps:
1716
- try:
1717
- selected_values = synoptics[syn_enum][selection].astype(float)
1718
- selection = ~np.isnan(selected_values)
1719
-
1720
- if not np.any(selection): # No data -> "U" (unknown)
1721
- value = "U"
1722
- else: # Use "T" (True) / "F" (False) only when unique (otherwise: "M" (mixed))
1723
- unique_values = np.unique(selected_values[selection])
1724
- value = str(bool(unique_values[0]))[0] if len(unique_values) == 1 else "M"
1725
- fits_synoptics[syn_enum] = np.append(fits_synoptics[syn_enum], value)
1726
- except (KeyError, AttributeError): # "U" (unknown)
1727
- fits_synoptics[syn_enum] = np.append(fits_synoptics[syn_enum], "U")
1728
-
1729
- # At this point, we have for each synoptical parameter an array of the values that need to
1730
- # be included in the FITS file. We now put all this information in a dedicated table and
1731
- # add it to the FITS file.
1732
-
1733
- syn_columns = []
1734
-
1735
- for syn_enum in chain(SynopticsFwdFill, SynopticsInterp1d, SynopticsLeaveGaps):
1736
- column_format = "A" if syn_enum == SynopticsLeaveGaps.OGSHTTR else "F"
1737
-
1738
- syn_column = fits.Column(syn_enum.value[0], format=column_format,
1739
- array=fits_synoptics[syn_enum])
1740
- syn_columns.append(syn_column)
1741
-
1742
- syn_table = fits.BinTableHDU.from_columns(syn_columns)
1743
- syn_table.header["EXTNAME"] = f"SYN-TAB_{ccd_number}_{ccd_side.name[0]}"
1744
-
1745
- # merged_columns = wcs_table.columns + syn_table.columns
1746
- # merged_table = fits.BinTableHDU.from_columns(merged_columns)
1747
-
1748
- syn_info[syn_table.header["EXTNAME"]] = (syn_table.data, syn_table.header)
1749
- except KeyError:
1750
- pass
1751
-
1752
- for data in syn_info.values():
1753
- fits.append(str(fits_filename), data[0], data[1])
1754
-
1755
- @click.group()
1756
- def cli():
1757
- pass
1758
-
1759
-
1760
- @cli.command()
1761
- def start():
1762
- multiprocessing.current_process().name = "fitsgen"
1763
-
1764
- # FIXME: Why is this line commented out?
1765
- # start_http_server(CTRL_SETTINGS.METRICS_PORT)
1766
-
1767
- # The Storage Manager must be active (otherwise the HK cannot be stored)
1768
-
1769
- if not is_storage_manager_active():
1770
- LOGGER.error("The Storage Manager is not running, start the core services before running the data acquisition.")
1771
- return
1772
-
1773
- if not is_dpu_cs_active():
1774
- LOGGER.critical("DPU Control Server must be running to be able to start the FITS generator.")
1775
- return
1776
-
1777
- FITSGenerator().run()
1778
-
1779
-
1780
- @cli.command()
1781
- def start_bg():
1782
-
1783
- invoke.run("fitsgen start", disown=True)
1784
-
1785
-
1786
- @cli.command()
1787
- def stop():
1788
- """Stop the FOV HK Control Server. """
1789
-
1790
- # In the while True loop in the start command, _should_stop needs to force a break from the loop.When this happens
1791
- # (and also when a keyboard interrupt has been caught), the monitoring socket needs to be closed (this needs to be
1792
- # done in the TH - specific implementation of _start). Unregistering from the Storage Manager is done
1793
- # automatically.
1794
-
1795
- response = send_request("quit")
1796
-
1797
- if response == "ACK":
1798
- rich.print("FITS generation successfully terminated.")
1799
- else:
1800
- rich.print(f"[red] ERROR: {response}")
1801
-
1802
-
1803
- def _check_commander_status(commander, poller) -> bool:
1804
- """ Check the status of the commander.
1805
-
1806
- Checks whether a command has been received by the given commander.
1807
-
1808
- Returns: True if a quit command was received; False otherwise.
1809
-
1810
- Args:
1811
- - commander: Commanding socket for the FOV HK generation.
1812
- - poller: Poller for the FOV HK generation.
1813
- """
1814
-
1815
- socks = dict(poller.poll(timeout=5000)) # Timeout of 5s
1816
-
1817
- if commander in socks:
1818
- pickle_string = commander.recv()
1819
- command = pickle.loads(pickle_string)
1820
-
1821
- if command.lower() == "quit":
1822
-
1823
- commander.send(pickle.dumps("ACK"))
1824
- return True
1825
-
1826
- if command.lower() == "status":
1827
- response = dict(
1828
- status="ACK",
1829
- host=CTRL_SETTINGS.HOSTNAME,
1830
- command_port=CTRL_SETTINGS.COMMANDING_PORT
1831
- )
1832
- commander.send(pickle.dumps(response))
1833
-
1834
- return False
1835
-
1836
- return False
1837
-
1838
-
1839
- @cli.command()
1840
- def status():
1841
- """Print the status of the FITS Generation Control Server."""
1842
-
1843
- rich.print("FITS generation:")
1844
-
1845
- response = send_request("status")
1846
-
1847
- if response.get("status") == "ACK":
1848
- rich.print(" Status: [green]active")
1849
- rich.print(f" Hostname: {response.get('host')}")
1850
- rich.print(f" Commanding port: {response.get('command_port')}")
1851
- else:
1852
- rich.print(" Status: [red]not active")
1853
-
1854
- def send_request(command_request: str):
1855
- """Sends a request to the FOV HK Control Server and waits for a response.
1856
-
1857
- Args:
1858
- - command_request: Request.
1859
-
1860
- Returns: Response to the request.
1861
- """
1862
-
1863
- ctx = zmq.Context().instance()
1864
- endpoint = connect_address(CTRL_SETTINGS.PROTOCOL, CTRL_SETTINGS.HOSTNAME, CTRL_SETTINGS.COMMANDING_PORT)
1865
- socket = ctx.socket(zmq.REQ)
1866
- socket.connect(endpoint)
1867
-
1868
- socket.send(pickle.dumps(command_request))
1869
- rlist, _, _ = zmq.select([socket], [], [], timeout=TIMEOUT_RECV)
1870
-
1871
- if socket in rlist:
1872
- response = socket.recv()
1873
- response = pickle.loads(response)
1874
- else:
1875
- response = {"error": "Receive from ZeroMQ socket timed out for FITS generation Control Server."}
1876
- socket.close(linger=0)
1877
-
1878
- return response
1879
-
1880
-
1881
- @cli.command()
1882
- @click.argument('files', type=str, nargs=-1)
1883
- @click.option("--location", type=str, is_flag=False, default=None, help="Set the root folder for the output "
1884
- "(i.e. folder with /daily and /obs)")
1885
- @click.option("--setup_id", type=int, is_flag=False, default=None, help="Setup ID")
1886
- @click.option("--site_id", type=str, is_flag=False, default=None, help="Site ID")
1887
- def from_hdf5(files, location=None, setup_id=None, site_id=None):
1888
- """ Generate the FITS files for the given list of HDF5 files.
1889
-
1890
- Args:
1891
- - files: List of HDF5 filenames.
1892
- - setup_id: Identifier of the setup that should be used. When not specified, the setup loading in the
1893
- Configuration Manager will be used to retrieve information from.
1894
- - site_id: Identifier for the test site.
1895
- """
1896
-
1897
- setup = get_offline_setup(site_id=site_id, setup_id=setup_id)
1898
- location = location or get_data_storage_location()
1899
-
1900
- create_fits_from_hdf5(files, location=location, setup=setup)
1901
-
1902
-
1903
- @cli.command()
1904
- @click.argument('obsid', type=str)
1905
- @click.option("--input_dir", type=str, is_flag=False, default=None, help="Set the root folder for the input "
1906
- "(i.e. folder with /daily and /obs)")
1907
- @click.option("--output_dir", type=str, is_flag=False, default=None, help="Set the root folder for the output "
1908
- "(i.e. folder with /daily and /obs)")
1909
- @click.option("--setup_id", type=int, is_flag=False, default=None, help="Setup ID")
1910
- @click.option("--site_id", type=str, is_flag=False, default=None, help="Site ID")
1911
- def for_obsid(obsid, input_dir=None, output_dir=None, setup_id=None, site_id=None):
1912
- """ Generate the FITS files for the given obsid.
1913
-
1914
- The setup that was loaded in the Configuration Manager during the given observation, will be used to retrieve
1915
- information from.
1916
-
1917
- Args:
1918
- - obsid: Observation identifier [TEST_LAB or TEST_LAB_SETUP].
1919
- - location: Folder (with /daily and /obs sub-folders) in which the FITS files should be written (in a
1920
- dedicated directory in the /obs folder). If not specified, the `PLATO_DATA_STORAGE_LOCATION`
1921
- environment variable will be used to construct the location.
1922
- """
1923
-
1924
- input_dir = input_dir or get_data_storage_location() # Location of HDF5 files (under /daily)
1925
- output_dir = output_dir or input_dir # Location of the FITS files that will be generated (under /obs)
1926
-
1927
- obsid = obsid_from_storage(obsid, data_dir=input_dir)
1928
- # Folder in the output /obs directory in which the FITS files will be stored (full path)
1929
- output_obs_folder = Path(f"{output_dir}/obs/{obsid}")
1930
- if not output_obs_folder.exists(): # If this directory doesn't exist yet, create it
1931
- os.makedirs(output_obs_folder)
1932
-
1933
- setup = get_offline_setup(site_id=site_id, setup_id=setup_id) # Setup (complete for the camera in question)
1934
-
1935
- hdf5_filenames = get_hdf5_filenames_for_obsid(obsid, data_dir=input_dir) # HDF5 files to process
1936
-
1937
- # Create FITS files (flat structure -> cubes)
1938
- create_fits_from_hdf5(hdf5_filenames, location=output_dir, setup=setup)
1939
-
1940
- fee_side = setup.camera.fee.ccd_sides.enum
1941
-
1942
- # Add synoptics
1943
-
1944
- if find_file(f"{obsid_from_storage(obsid, data_dir=input_dir)}_{SYN_ORIGIN}_*.csv", root=output_obs_folder):
1945
- # Synoptics have already been re-processed (located in the directory to which the FITS files will be stored)
1946
- add_synoptics(obsid, fits_dir=output_dir, syn_dir=output_dir, fee_side=fee_side)
1947
- else:
1948
- # Use the original synoptics files
1949
- add_synoptics(obsid, fits_dir=output_dir, syn_dir=input_dir, fee_side=fee_side)
1950
-
1951
-
1952
- def get_offline_setup(site_id: str = None, setup_id: int = None):
1953
- """ Return setup to use for the off-line FITS generation.
1954
-
1955
- If the setup ID and site ID have been specified, the corresponding setup is used. Otherwise, the setup that is
1956
- currently loaded in the Configuration Manager is used.
1957
-
1958
- Args:
1959
- site_id: Identifier of the testhouse
1960
- setup_id: Identifier of the setup
1961
-
1962
- Returns:
1963
- - Setup to use for the off-line FITS generation.
1964
- """
1965
-
1966
- if setup_id is None:
1967
- return load_setup()
1968
- else:
1969
- site_id = site_id or SITE.ID
1970
- return load_setup(setup_id=setup_id, site_id=site_id, from_disk=True)
1971
-
1972
-
1973
- def get_hdf5_filenames_for_obsid(obsid: str, data_dir: str = None) -> List:
1974
- """ Return list of HDF5 filenames that contribute to the given obsid.
1975
-
1976
- The given obsid can be specified in either of these two formats: TEST_LAB or TEST_LAB_SETUP. The obsid that is
1977
- stored in the HDF5 files is of format LAB_SETUP_TEST. In this method, we gather the list of HDF5 filenames for
1978
- which the combination (TEST, SITE) matches with the (TEST, SITE) combination from the given obsid. To do this, the
1979
- list of relevant ODs is composed, based on the first and last timestamp in the DPU HK file (this file will always
1980
- be present if data has been acquired). Then all HDF5 files for these ODs are looped over and the obsid stored in
1981
- there is compared with the given obsid. In case of a match, the HDF5 filename is added to the list.
1982
-
1983
- Args:
1984
- - obsid: Observation identifier [TEST_LAB or TEST_LAB_SETUP].
1985
- - data_dir: Full path to the directory in which the data resides. This is the folder with a sub-folder /daily,
1986
- in which the HDF5 files are stored.
1987
- """
1988
-
1989
- data_dir = data_dir or get_data_storage_location()
1990
-
1991
- # Determine in which location (i.e. in the folder of which OD in the /daily sub-folder of the data directory)
1992
- # the required HDF5 files are stored. This sub-folder carries the OD [yyyymmdd] as name.
1993
-
1994
- od_list = get_od(obsid, data_dir) # Obsid -> OD
1995
- LOGGER.info(f"OD for obsid {obsid}: {od_list}")
1996
-
1997
- obs_hdf5_files = []
1998
-
1999
- for od in od_list:
2000
-
2001
- day_dir = Path(f"{data_dir}/daily/{od}") # Sub-folder with the data for that OD
2002
-
2003
- daily_hdf5_filenames = glob.glob(str(day_dir / f"*.{HDF5.extension}"))
2004
-
2005
- for hdf5_filename in sorted(daily_hdf5_filenames):
2006
-
2007
- try:
2008
- with h5.get_file(hdf5_filename, mode="r", locking=False) as hdf5_file:
2009
-
2010
- if "/obsid" in hdf5_file:
2011
-
2012
- hdf5_obsid = h5.get_data(hdf5_file["/obsid"]).item().decode()
2013
-
2014
- if hdf5_obsid != "None":
2015
- hdf5_obsid = ObservationIdentifier.create_from_string(
2016
- hdf5_obsid, LAB_SETUP_TEST).create_id(order=TEST_LAB) # TEST_LAB
2017
- if hdf5_obsid in str(obsid):
2018
- obs_hdf5_files.append(hdf5_filename)
2019
-
2020
- except OSError as exc:
2021
- LOGGER.error(f"Couldn't open {hdf5_filename} ({exc=})")
2022
- except RuntimeError as exc:
2023
- LOGGER.debug(f"Unable to open HDF5 file: {exc}")
2024
-
2025
- return obs_hdf5_files
2026
-
2027
-
2028
- def get_od(obsid: str, data_dir: str = None):
2029
- """ Return list of OD(s) for the given obsid.
2030
-
2031
- The given obsid can be specified in either of these two formats: TEST_LAB or TEST_LAB_SETUP. In this method, we
2032
- determine during which OD(s) the given obsid was executed. To do this, the first and last timestamp from the DPU HK
2033
- file (this file will always be present if data has been acquired) are extracted. This file resides in the folder of
2034
- the given obsid in the /obs directory, with the name (i.e. obsid) in the format TEST_SITE_SETUP or TEST_SITE
2035
- (depending on how old the observation is). The obsid that is used in the filename follows the same pattern, so the
2036
- given obsid must be converted to that format.
2037
-
2038
- Args:
2039
- - obsid: Observation identifier [TEST_LAB or TEST_LAB_SETUP].
2040
- - data_dir: Full path to the directory in which the data resides. This is the folder with a sub-folder /daily,
2041
- in which the HDF5 files are stored.
2042
-
2043
- Returns: List of observation day [yyyymmdd].
2044
- """
2045
-
2046
- data_dir = data_dir or get_data_storage_location()
2047
- obsid = obsid_from_storage(obsid, data_dir=data_dir) # Convert the obsid to the correct format
2048
- obs_dir = f"{data_dir}/obs/{obsid}"
2049
-
2050
- try:
2051
- filename = str(find_file(f"{obsid}_DPU_*.csv", root=obs_dir))
2052
-
2053
- od_start = datetime.strptime(filename.split("_")[-2], "%Y%m%d") # First OD (from filename)
2054
- od_end = datetime.strptime(read_last_line(filename)[:10], "%Y-%m-%d") # Last OD (from last line)
2055
-
2056
- od = od_start
2057
- delta = timedelta(days=1)
2058
- od_list = []
2059
-
2060
- while od <= od_end:
2061
-
2062
- od_list.append(od.strftime("%Y%m%d"))
2063
-
2064
- od += delta
2065
-
2066
- return od_list
2067
- except IndexError:
2068
- raise Abort(f"DPU was not running during obsid {obsid}: no data could be acquired")
2069
-
2070
-
2071
- def get_obsid(od: str, index: int, day_dir: str) -> int:
2072
- """ Return the obsid stored in the HDF5 file for the given OD and the given index.
2073
-
2074
- Args:
2075
- - od: Observation day.
2076
- - index: Index of the HDF5 file.
2077
- - day_dir: Full path to the directory with the HDF5 files for the given OD.
2078
-
2079
- Returns: Obsid as stored in the HDF5 file for the given OD and the given index (LAB_SETUP_TEST).
2080
- """
2081
-
2082
- if index == 0: # For the first file, no index is used
2083
- hdf5_filename = f"{day_dir}/{od}_{SITE.ID}_{N_FEE_SETTINGS.ORIGIN_SPW_DATA}.hdf5"
2084
- else:
2085
- hdf5_filename = f"{day_dir}/{od}_{SITE.ID}_{N_FEE_SETTINGS.ORIGIN_SPW_DATA}_{index:05d}.hdf5"
2086
-
2087
- with h5.get_file(hdf5_filename, mode="r", locking=False) as hdf5_file:
2088
- try:
2089
- return int(hdf5_file["obsid"][()].decode().split("_")[-1])
2090
- except:
2091
- return None
2092
-
2093
-
2094
- if __name__ == "__main__":
2095
-
2096
- sys.exit(cli())