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.
- README.md +27 -0
- bump.py +85 -0
- cgse-2025.0.2.dist-info/METADATA +38 -0
- cgse-2025.0.2.dist-info/RECORD +5 -0
- {cgse-2024.7.0.dist-info → cgse-2025.0.2.dist-info}/WHEEL +1 -2
- cgse-2024.7.0.dist-info/COPYING +0 -674
- cgse-2024.7.0.dist-info/COPYING.LESSER +0 -165
- cgse-2024.7.0.dist-info/METADATA +0 -144
- cgse-2024.7.0.dist-info/RECORD +0 -660
- cgse-2024.7.0.dist-info/entry_points.txt +0 -75
- cgse-2024.7.0.dist-info/top_level.txt +0 -2
- egse/__init__.py +0 -12
- egse/__main__.py +0 -32
- egse/aeu/aeu.py +0 -5238
- egse/aeu/aeu_awg.yaml +0 -265
- egse/aeu/aeu_crio.yaml +0 -273
- egse/aeu/aeu_cs.py +0 -627
- egse/aeu/aeu_devif.py +0 -321
- egse/aeu/aeu_main_ui.py +0 -903
- egse/aeu/aeu_metrics.py +0 -131
- egse/aeu/aeu_protocol.py +0 -463
- egse/aeu/aeu_psu.yaml +0 -204
- egse/aeu/aeu_ui.py +0 -873
- egse/aeu/arbdata/FccdRead.arb +0 -2
- egse/aeu/arbdata/FccdRead_min_points.arb +0 -2
- egse/aeu/arbdata/HeaterSync_FccdRead.arb +0 -2
- egse/aeu/arbdata/HeaterSync_ccdRead25.arb +0 -2
- egse/aeu/arbdata/HeaterSync_ccdRead31_25.arb +0 -2
- egse/aeu/arbdata/HeaterSync_ccdRead37_50.arb +0 -2
- egse/aeu/arbdata/HeaterSync_ccdRead43_75.arb +0 -2
- egse/aeu/arbdata/HeaterSync_ccdRead50.arb +0 -2
- egse/aeu/arbdata/Heater_FccdRead_min_points.arb +0 -2
- egse/aeu/arbdata/ccdRead25.arb +0 -2
- egse/aeu/arbdata/ccdRead25_150ms.arb +0 -2
- egse/aeu/arbdata/ccdRead31_25.arb +0 -2
- egse/aeu/arbdata/ccdRead31_25_150ms.arb +0 -2
- egse/aeu/arbdata/ccdRead37_50.arb +0 -2
- egse/aeu/arbdata/ccdRead37_50_150ms.arb +0 -2
- egse/aeu/arbdata/ccdRead43_75.arb +0 -2
- egse/aeu/arbdata/ccdRead43_75_150ms.arb +0 -2
- egse/aeu/arbdata/ccdRead50.arb +0 -2
- egse/aeu/arbdata/ccdRead50_150ms.arb +0 -2
- egse/alert/__init__.py +0 -1049
- egse/alert/alertman.yaml +0 -37
- egse/alert/alertman_cs.py +0 -233
- egse/alert/alertman_ui.py +0 -600
- egse/alert/gsm/beaglebone.py +0 -138
- egse/alert/gsm/beaglebone.yaml +0 -51
- egse/alert/gsm/beaglebone_cs.py +0 -108
- egse/alert/gsm/beaglebone_devif.py +0 -122
- egse/alert/gsm/beaglebone_protocol.py +0 -46
- egse/bits.py +0 -318
- egse/camera.py +0 -44
- egse/collimator/__init__.py +0 -0
- egse/collimator/fcul/__init__.py +0 -0
- egse/collimator/fcul/ogse.py +0 -1077
- egse/collimator/fcul/ogse.yaml +0 -14
- egse/collimator/fcul/ogse_cs.py +0 -154
- egse/collimator/fcul/ogse_devif.py +0 -358
- egse/collimator/fcul/ogse_protocol.py +0 -132
- egse/collimator/fcul/ogse_sim.py +0 -431
- egse/collimator/fcul/ogse_ui.py +0 -1108
- egse/command.py +0 -699
- egse/config.py +0 -410
- egse/confman/__init__.py +0 -1058
- egse/confman/confman.yaml +0 -70
- egse/confman/confman_cs.py +0 -240
- egse/confman/confman_ui.py +0 -381
- egse/confman/setup_ui.py +0 -565
- egse/control.py +0 -632
- egse/coordinates/__init__.py +0 -534
- egse/coordinates/avoidance.py +0 -100
- egse/coordinates/cslmodel.py +0 -127
- egse/coordinates/laser_tracker_to_dict.py +0 -122
- egse/coordinates/point.py +0 -707
- egse/coordinates/pyplot.py +0 -194
- egse/coordinates/referenceFrame.py +0 -1279
- egse/coordinates/refmodel.py +0 -737
- egse/coordinates/rotationMatrix.py +0 -85
- egse/coordinates/transform3d_addon.py +0 -419
- egse/csl/__init__.py +0 -50
- egse/csl/commanding.py +0 -78
- egse/csl/icons/hexapod-connected-selected.svg +0 -30
- egse/csl/icons/hexapod-connected.svg +0 -30
- egse/csl/icons/hexapod-homing-selected.svg +0 -68
- egse/csl/icons/hexapod-homing.svg +0 -68
- egse/csl/icons/hexapod-retract-selected.svg +0 -56
- egse/csl/icons/hexapod-retract.svg +0 -51
- egse/csl/icons/hexapod-zero-selected.svg +0 -56
- egse/csl/icons/hexapod-zero.svg +0 -56
- egse/csl/icons/logo-puna.svg +0 -92
- egse/csl/icons/stop.svg +0 -1
- egse/csl/initialisation.py +0 -102
- egse/csl/mech_pos_settings.yaml +0 -18
- egse/das.py +0 -1240
- egse/das.yaml +0 -7
- egse/data/conf/SETUP_CSL_00000_170620_150000.yaml +0 -5
- egse/data/conf/SETUP_CSL_00001_170620_151010.yaml +0 -69
- egse/data/conf/SETUP_CSL_00002_170620_151020.yaml +0 -69
- egse/data/conf/SETUP_CSL_00003_170620_151030.yaml +0 -69
- egse/data/conf/SETUP_CSL_00004_170620_151040.yaml +0 -69
- egse/data/conf/SETUP_CSL_00005_170620_151050.yaml +0 -69
- egse/data/conf/SETUP_CSL_00006_170620_151060.yaml +0 -69
- egse/data/conf/SETUP_CSL_00007_170620_151070.yaml +0 -69
- egse/data/conf/SETUP_CSL_00008_170620_151080.yaml +0 -75
- egse/data/conf/SETUP_CSL_00010_210308_083016.yaml +0 -138
- egse/data/conf/SETUP_INTA_00000_170620_150000.yaml +0 -4
- egse/data/conf/SETUP_SRON_00000_170620_150000.yaml +0 -4
- egse/decorators.py +0 -514
- egse/device.py +0 -269
- egse/dpu/__init__.py +0 -2698
- egse/dpu/ccd_ui.py +0 -514
- egse/dpu/dpu.py +0 -783
- egse/dpu/dpu.yaml +0 -153
- egse/dpu/dpu_cs.py +0 -272
- egse/dpu/dpu_ui.py +0 -671
- egse/dpu/fitsgen.py +0 -2096
- egse/dpu/fitsgen_ui.py +0 -399
- egse/dpu/hdf5_model.py +0 -332
- egse/dpu/hdf5_ui.py +0 -277
- egse/dpu/hdf5_viewer.py +0 -506
- egse/dpu/hk_ui.py +0 -468
- egse/dpu_commands.py +0 -81
- egse/dsi/__init__.py +0 -33
- egse/dsi/_libesl.py +0 -232
- egse/dsi/constants.py +0 -296
- egse/dsi/esl.py +0 -630
- egse/dsi/rmap.py +0 -444
- egse/dsi/rmapci.py +0 -39
- egse/dsi/spw.py +0 -335
- egse/dsi/spw_state.py +0 -29
- egse/dummy.py +0 -318
- egse/dyndummy.py +0 -179
- egse/env.py +0 -278
- egse/exceptions.py +0 -88
- egse/fdir/__init__.py +0 -26
- egse/fdir/fdir_manager.py +0 -85
- egse/fdir/fdir_manager.yaml +0 -37
- egse/fdir/fdir_manager_controller.py +0 -136
- egse/fdir/fdir_manager_cs.py +0 -164
- egse/fdir/fdir_manager_interface.py +0 -15
- egse/fdir/fdir_remote.py +0 -73
- egse/fdir/fdir_remote.yaml +0 -30
- egse/fdir/fdir_remote_controller.py +0 -30
- egse/fdir/fdir_remote_cs.py +0 -94
- egse/fdir/fdir_remote_interface.py +0 -9
- egse/fdir/fdir_remote_popup.py +0 -26
- egse/fee/__init__.py +0 -106
- egse/fee/f_fee_register.yaml +0 -43
- egse/fee/feesim.py +0 -914
- egse/fee/n_fee_hk.py +0 -768
- egse/fee/nfee.py +0 -188
- egse/filterwheel/__init__.py +0 -4
- egse/filterwheel/eksma/__init__.py +0 -49
- egse/filterwheel/eksma/fw8smc4.py +0 -657
- egse/filterwheel/eksma/fw8smc4.yaml +0 -121
- egse/filterwheel/eksma/fw8smc4_cs.py +0 -144
- egse/filterwheel/eksma/fw8smc4_devif.py +0 -473
- egse/filterwheel/eksma/fw8smc4_protocol.py +0 -82
- egse/filterwheel/eksma/fw8smc4_ui.py +0 -940
- egse/filterwheel/eksma/fw8smc5.py +0 -115
- egse/filterwheel/eksma/fw8smc5.yaml +0 -105
- egse/filterwheel/eksma/fw8smc5_controller.py +0 -307
- egse/filterwheel/eksma/fw8smc5_cs.py +0 -141
- egse/filterwheel/eksma/fw8smc5_interface.py +0 -65
- egse/filterwheel/eksma/fw8smc5_simulator.py +0 -29
- egse/filterwheel/eksma/fw8smc5_ui.py +0 -1065
- egse/filterwheel/eksma/testpythonfw.py +0 -215
- egse/fov/__init__.py +0 -65
- egse/fov/fov_hk.py +0 -710
- egse/fov/fov_ui.py +0 -859
- egse/fov/fov_ui_controller.py +0 -140
- egse/fov/fov_ui_model.py +0 -200
- egse/fov/fov_ui_view.py +0 -345
- egse/gimbal/__init__.py +0 -32
- egse/gimbal/symetrie/__init__.py +0 -26
- egse/gimbal/symetrie/alpha.py +0 -586
- egse/gimbal/symetrie/generic_gimbal_ui.py +0 -1521
- egse/gimbal/symetrie/gimbal.py +0 -877
- egse/gimbal/symetrie/gimbal.yaml +0 -168
- egse/gimbal/symetrie/gimbal_cs.py +0 -183
- egse/gimbal/symetrie/gimbal_protocol.py +0 -138
- egse/gimbal/symetrie/gimbal_ui.py +0 -361
- egse/gimbal/symetrie/pmac.py +0 -1006
- egse/gimbal/symetrie/pmac_regex.py +0 -83
- egse/graph.py +0 -132
- egse/gui/__init__.py +0 -47
- egse/gui/buttons.py +0 -378
- egse/gui/focalplane.py +0 -1285
- egse/gui/formatter.py +0 -10
- egse/gui/led.py +0 -162
- egse/gui/limitswitch.py +0 -143
- egse/gui/mechanisms.py +0 -587
- egse/gui/states.py +0 -148
- egse/gui/stripchart.py +0 -729
- egse/gui/styles.qss +0 -48
- egse/gui/switch.py +0 -112
- egse/h5.py +0 -274
- egse/help/__init__.py +0 -0
- egse/help/help_ui.py +0 -126
- egse/hexapod/__init__.py +0 -32
- egse/hexapod/symetrie/__init__.py +0 -137
- egse/hexapod/symetrie/alpha.py +0 -874
- egse/hexapod/symetrie/dynalpha.py +0 -1387
- egse/hexapod/symetrie/hexapod_ui.py +0 -1516
- egse/hexapod/symetrie/pmac.py +0 -1010
- egse/hexapod/symetrie/pmac_regex.py +0 -83
- egse/hexapod/symetrie/puna.py +0 -1167
- egse/hexapod/symetrie/puna.yaml +0 -193
- egse/hexapod/symetrie/puna_cs.py +0 -195
- egse/hexapod/symetrie/puna_protocol.py +0 -134
- egse/hexapod/symetrie/puna_ui.py +0 -433
- egse/hexapod/symetrie/punaplus.py +0 -107
- egse/hexapod/symetrie/zonda.py +0 -872
- egse/hexapod/symetrie/zonda.yaml +0 -337
- egse/hexapod/symetrie/zonda_cs.py +0 -172
- egse/hexapod/symetrie/zonda_devif.py +0 -414
- egse/hexapod/symetrie/zonda_protocol.py +0 -123
- egse/hexapod/symetrie/zonda_ui.py +0 -449
- egse/hk.py +0 -791
- egse/icons/aeu-cs-start.svg +0 -117
- egse/icons/aeu-cs-stop.svg +0 -118
- egse/icons/aeu-cs.svg +0 -107
- egse/icons/aeu_cs-started.svg +0 -112
- egse/icons/aeu_cs-stopped.svg +0 -112
- egse/icons/aeu_cs.svg +0 -55
- egse/icons/alert.svg +0 -1
- egse/icons/arrow-double-left.png +0 -0
- egse/icons/arrow-double-right.png +0 -0
- egse/icons/arrow-up.svg +0 -11
- egse/icons/backward.svg +0 -1
- egse/icons/busy.svg +0 -1
- egse/icons/cleaning.svg +0 -115
- egse/icons/color-scheme.svg +0 -1
- egse/icons/cs-connected-alert.svg +0 -91
- egse/icons/cs-connected-disabled.svg +0 -43
- egse/icons/cs-connected.svg +0 -89
- egse/icons/cs-not-connected.svg +0 -44
- egse/icons/double-left-arrow.svg +0 -1
- egse/icons/double-right-arrow.svg +0 -1
- egse/icons/erase-disabled.svg +0 -19
- egse/icons/erase.svg +0 -59
- egse/icons/fitsgen-start.svg +0 -47
- egse/icons/fitsgen-stop.svg +0 -48
- egse/icons/fitsgen.svg +0 -1
- egse/icons/forward.svg +0 -1
- egse/icons/fov-hk-start.svg +0 -33
- egse/icons/fov-hk-stop.svg +0 -37
- egse/icons/fov-hk.svg +0 -1
- egse/icons/front-desk.svg +0 -1
- egse/icons/home-actioned.svg +0 -15
- egse/icons/home-disabled.svg +0 -15
- egse/icons/home.svg +0 -13
- egse/icons/info.svg +0 -1
- egse/icons/invalid.png +0 -0
- egse/icons/led-green.svg +0 -20
- egse/icons/led-grey.svg +0 -20
- egse/icons/led-orange.svg +0 -20
- egse/icons/led-red.svg +0 -20
- egse/icons/led-square-green.svg +0 -134
- egse/icons/led-square-grey.svg +0 -134
- egse/icons/led-square-orange.svg +0 -134
- egse/icons/led-square-red.svg +0 -134
- egse/icons/limit-switch-all-green.svg +0 -115
- egse/icons/limit-switch-all-red.svg +0 -117
- egse/icons/limit-switch-el+.svg +0 -116
- egse/icons/limit-switch-el-.svg +0 -117
- egse/icons/location-marker.svg +0 -1
- egse/icons/logo-dpu.svg +0 -48
- egse/icons/logo-gimbal.svg +0 -112
- egse/icons/logo-huber.svg +0 -23
- egse/icons/logo-ogse.svg +0 -31
- egse/icons/logo-puna.svg +0 -92
- egse/icons/logo-tcs.svg +0 -29
- egse/icons/logo-zonda.svg +0 -66
- egse/icons/maximize.svg +0 -1
- egse/icons/meter.svg +0 -1
- egse/icons/more.svg +0 -45
- egse/icons/n-fee-hk-start.svg +0 -24
- egse/icons/n-fee-hk-stop.svg +0 -25
- egse/icons/n-fee-hk.svg +0 -83
- egse/icons/observing-off.svg +0 -46
- egse/icons/observing-on.svg +0 -46
- egse/icons/open-document-hdf5.png +0 -0
- egse/icons/open-document-hdf5.svg +0 -21
- egse/icons/ops-mode.svg +0 -1
- egse/icons/play-green.svg +0 -17
- egse/icons/plugged-disabled.svg +0 -27
- egse/icons/plugged.svg +0 -21
- egse/icons/pm_ui.svg +0 -1
- egse/icons/power-button-green.svg +0 -27
- egse/icons/power-button-red.svg +0 -27
- egse/icons/power-button.svg +0 -27
- egse/icons/radar.svg +0 -1
- egse/icons/radioactive.svg +0 -2
- egse/icons/reload.svg +0 -1
- egse/icons/remote-control-off.svg +0 -28
- egse/icons/remote-control-on.svg +0 -28
- egse/icons/repeat-blue.svg +0 -15
- egse/icons/repeat.svg +0 -1
- egse/icons/settings.svg +0 -1
- egse/icons/shrink.svg +0 -1
- egse/icons/shutter.svg +0 -1
- egse/icons/sign-off.svg +0 -1
- egse/icons/sign-on.svg +0 -1
- egse/icons/sim-mode.svg +0 -1
- egse/icons/small-buttons-go.svg +0 -20
- egse/icons/small-buttons-minus.svg +0 -51
- egse/icons/small-buttons-plus.svg +0 -51
- egse/icons/sponge.svg +0 -220
- egse/icons/start-button-disabled.svg +0 -84
- egse/icons/start-button.svg +0 -50
- egse/icons/stop-button-disabled.svg +0 -84
- egse/icons/stop-button.svg +0 -50
- egse/icons/stop-red.svg +0 -17
- egse/icons/stop.svg +0 -1
- egse/icons/switch-disabled-square.svg +0 -87
- egse/icons/switch-disabled.svg +0 -15
- egse/icons/switch-off-square.svg +0 -87
- egse/icons/switch-off.svg +0 -72
- egse/icons/switch-on-square.svg +0 -87
- egse/icons/switch-on.svg +0 -61
- egse/icons/temperature-control.svg +0 -44
- egse/icons/th_ui_logo.svg +0 -1
- egse/icons/unplugged.svg +0 -23
- egse/icons/unvalid.png +0 -0
- egse/icons/user-interface.svg +0 -1
- egse/icons/vacuum.svg +0 -1
- egse/icons/valid.png +0 -0
- egse/icons/zoom-to-pixel-dark.svg +0 -64
- egse/icons/zoom-to-pixel-white.svg +0 -36
- egse/images/big-rotation-stage.png +0 -0
- egse/images/connected-100.png +0 -0
- egse/images/cross.svg +0 -6
- egse/images/disconnected-100.png +0 -0
- egse/images/gui-icon.png +0 -0
- egse/images/home.svg +0 -6
- egse/images/info-icon.png +0 -0
- egse/images/led-black.svg +0 -89
- egse/images/led-green.svg +0 -85
- egse/images/led-orange.svg +0 -85
- egse/images/led-red.svg +0 -85
- egse/images/load-icon.png +0 -0
- egse/images/load-setup.png +0 -0
- egse/images/load.png +0 -0
- egse/images/pause.png +0 -0
- egse/images/play-button.svg +0 -8
- egse/images/play.png +0 -0
- egse/images/process-status.png +0 -0
- egse/images/restart.png +0 -0
- egse/images/search.png +0 -0
- egse/images/sma.png +0 -0
- egse/images/start.png +0 -0
- egse/images/stop-button.svg +0 -8
- egse/images/stop.png +0 -0
- egse/images/switch-off.svg +0 -48
- egse/images/switch-on.svg +0 -48
- egse/images/undo.png +0 -0
- egse/images/update-button.svg +0 -11
- egse/imageviewer/exposureselection.py +0 -475
- egse/imageviewer/imageviewer.py +0 -198
- egse/imageviewer/matchfocalplane.py +0 -179
- egse/imageviewer/subfieldposition.py +0 -133
- egse/lampcontrol/__init__.py +0 -4
- egse/lampcontrol/beaglebone/beaglebone.py +0 -178
- egse/lampcontrol/beaglebone/beaglebone.yaml +0 -62
- egse/lampcontrol/beaglebone/beaglebone_cs.py +0 -106
- egse/lampcontrol/beaglebone/beaglebone_devif.py +0 -150
- egse/lampcontrol/beaglebone/beaglebone_protocol.py +0 -73
- egse/lampcontrol/energetiq/__init__.py +0 -22
- egse/lampcontrol/energetiq/eq99.yaml +0 -98
- egse/lampcontrol/energetiq/lampEQ99.py +0 -283
- egse/lampcontrol/energetiq/lampEQ99_cs.py +0 -128
- egse/lampcontrol/energetiq/lampEQ99_devif.py +0 -158
- egse/lampcontrol/energetiq/lampEQ99_encode_decode_errors.py +0 -73
- egse/lampcontrol/energetiq/lampEQ99_protocol.py +0 -71
- egse/lampcontrol/energetiq/lampEQ99_ui.py +0 -465
- egse/lib/CentOS-7/EtherSpaceLink_v34_86.dylib +0 -0
- egse/lib/CentOS-8/ESL-RMAP_v34_86.dylib +0 -0
- egse/lib/CentOS-8/EtherSpaceLink_v34_86.dylib +0 -0
- egse/lib/Debian/ESL-RMAP_v34_86.dylib +0 -0
- egse/lib/Debian/EtherSpaceLink_v34_86.dylib +0 -0
- egse/lib/Debian/libetherspacelink_v35_21.dylib +0 -0
- egse/lib/Linux/ESL-RMAP_v34_86.dylib +0 -0
- egse/lib/Linux/EtherSpaceLink_v34_86.dylib +0 -0
- egse/lib/Ubuntu-20/ESL-RMAP_v34_86.dylib +0 -0
- egse/lib/Ubuntu-20/EtherSpaceLink_v34_86.dylib +0 -0
- egse/lib/gssw/python3-gssw_2.2.3+31f63c9f-1_all.deb +0 -0
- egse/lib/ximc/__pycache__/pyximc.cpython-38 2.pyc +0 -0
- egse/lib/ximc/__pycache__/pyximc.cpython-38.pyc +0 -0
- egse/lib/ximc/libximc.framework/Frameworks/libbindy.dylib +0 -0
- egse/lib/ximc/libximc.framework/Frameworks/libxiwrapper.dylib +0 -0
- egse/lib/ximc/libximc.framework/Headers/ximc.h +0 -5510
- egse/lib/ximc/libximc.framework/Resources/Info.plist +0 -42
- egse/lib/ximc/libximc.framework/Resources/keyfile.sqlite +0 -0
- egse/lib/ximc/libximc.framework/libbindy.so +0 -0
- egse/lib/ximc/libximc.framework/libximc +0 -0
- egse/lib/ximc/libximc.framework/libximc.so +0 -0
- egse/lib/ximc/libximc.framework/libximc.so.7.0.0 +0 -0
- egse/lib/ximc/libximc.framework/libxiwrapper.so +0 -0
- egse/lib/ximc/pyximc.py +0 -922
- egse/listener.py +0 -179
- egse/logger/__init__.py +0 -243
- egse/logger/log_cs.py +0 -321
- egse/metrics.py +0 -102
- egse/mixin.py +0 -464
- egse/monitoring.py +0 -95
- egse/ni/alarms/__init__.py +0 -26
- egse/ni/alarms/cdaq9375.py +0 -300
- egse/ni/alarms/cdaq9375.yaml +0 -89
- egse/ni/alarms/cdaq9375_cs.py +0 -130
- egse/ni/alarms/cdaq9375_devif.py +0 -183
- egse/ni/alarms/cdaq9375_protocol.py +0 -48
- egse/obs_inspection.py +0 -165
- egse/observer.py +0 -41
- egse/obsid.py +0 -163
- egse/powermeter/__init__.py +0 -0
- egse/powermeter/ni/__init__.py +0 -38
- egse/powermeter/ni/cdaq9184.py +0 -224
- egse/powermeter/ni/cdaq9184.yaml +0 -73
- egse/powermeter/ni/cdaq9184_cs.py +0 -130
- egse/powermeter/ni/cdaq9184_devif.py +0 -201
- egse/powermeter/ni/cdaq9184_protocol.py +0 -48
- egse/powermeter/ni/cdaq9184_ui.py +0 -544
- egse/powermeter/thorlabs/__init__.py +0 -25
- egse/powermeter/thorlabs/pm100a.py +0 -380
- egse/powermeter/thorlabs/pm100a.yaml +0 -132
- egse/powermeter/thorlabs/pm100a_cs.py +0 -136
- egse/powermeter/thorlabs/pm100a_devif.py +0 -127
- egse/powermeter/thorlabs/pm100a_protocol.py +0 -80
- egse/powermeter/thorlabs/pm100a_ui.py +0 -725
- egse/process.py +0 -451
- egse/procman/__init__.py +0 -834
- egse/procman/cannot_start_process_popup.py +0 -43
- egse/procman/procman.yaml +0 -49
- egse/procman/procman_cs.py +0 -201
- egse/procman/procman_ui.py +0 -2081
- egse/protocol.py +0 -605
- egse/proxy.py +0 -531
- egse/randomwalk.py +0 -140
- egse/reg.py +0 -585
- egse/reload.py +0 -122
- egse/reprocess.py +0 -693
- egse/resource.py +0 -333
- egse/rmap.py +0 -406
- egse/rst.py +0 -135
- egse/search.py +0 -182
- egse/serialdevice.py +0 -190
- egse/services.py +0 -247
- egse/services.yaml +0 -68
- egse/settings.py +0 -379
- egse/settings.yaml +0 -980
- egse/setup.py +0 -1181
- egse/shutter/__init__.py +0 -0
- egse/shutter/thorlabs/__init__.py +0 -19
- egse/shutter/thorlabs/ksc101.py +0 -205
- egse/shutter/thorlabs/ksc101.yaml +0 -105
- egse/shutter/thorlabs/ksc101_cs.py +0 -136
- egse/shutter/thorlabs/ksc101_devif.py +0 -201
- egse/shutter/thorlabs/ksc101_protocol.py +0 -71
- egse/shutter/thorlabs/ksc101_ui.py +0 -548
- egse/shutter/thorlabs/sc10.py +0 -82
- egse/shutter/thorlabs/sc10.yaml +0 -52
- egse/shutter/thorlabs/sc10_controller.py +0 -81
- egse/shutter/thorlabs/sc10_cs.py +0 -108
- egse/shutter/thorlabs/sc10_interface.py +0 -25
- egse/shutter/thorlabs/sc10_simulator.py +0 -30
- egse/simulator.py +0 -41
- egse/slack.py +0 -61
- egse/socketdevice.py +0 -218
- egse/sockets.py +0 -218
- egse/spw.py +0 -1401
- egse/stages/__init__.py +0 -12
- egse/stages/aerotech/ensemble.py +0 -245
- egse/stages/aerotech/ensemble.yaml +0 -205
- egse/stages/aerotech/ensemble_controller.py +0 -275
- egse/stages/aerotech/ensemble_cs.py +0 -110
- egse/stages/aerotech/ensemble_interface.py +0 -132
- egse/stages/aerotech/ensemble_parameters.py +0 -433
- egse/stages/aerotech/ensemble_simulator.py +0 -27
- egse/stages/aerotech/mgse_sim.py +0 -188
- egse/stages/arun/smd3.py +0 -110
- egse/stages/arun/smd3.yaml +0 -68
- egse/stages/arun/smd3_controller.py +0 -470
- egse/stages/arun/smd3_cs.py +0 -112
- egse/stages/arun/smd3_interface.py +0 -53
- egse/stages/arun/smd3_simulator.py +0 -27
- egse/stages/arun/smd3_stop.py +0 -16
- egse/stages/huber/__init__.py +0 -49
- egse/stages/huber/smc9300.py +0 -920
- egse/stages/huber/smc9300.yaml +0 -63
- egse/stages/huber/smc9300_cs.py +0 -178
- egse/stages/huber/smc9300_devif.py +0 -345
- egse/stages/huber/smc9300_protocol.py +0 -113
- egse/stages/huber/smc9300_sim.py +0 -547
- egse/stages/huber/smc9300_ui.py +0 -973
- egse/state.py +0 -173
- egse/statemachine.py +0 -274
- egse/storage/__init__.py +0 -1067
- egse/storage/persistence.py +0 -2295
- egse/storage/storage.yaml +0 -79
- egse/storage/storage_cs.py +0 -231
- egse/styles/dark.qss +0 -343
- egse/styles/default.qss +0 -48
- egse/synoptics/__init__.py +0 -417
- egse/synoptics/syn.yaml +0 -9
- egse/synoptics/syn_cs.py +0 -195
- egse/system.py +0 -1611
- egse/tcs/__init__.py +0 -14
- egse/tcs/tcs.py +0 -879
- egse/tcs/tcs.yaml +0 -14
- egse/tcs/tcs_cs.py +0 -202
- egse/tcs/tcs_devif.py +0 -292
- egse/tcs/tcs_protocol.py +0 -180
- egse/tcs/tcs_sim.py +0 -177
- egse/tcs/tcs_ui.py +0 -543
- egse/tdms.py +0 -171
- egse/tempcontrol/__init__.py +0 -23
- egse/tempcontrol/agilent/agilent34970.py +0 -109
- egse/tempcontrol/agilent/agilent34970.yaml +0 -44
- egse/tempcontrol/agilent/agilent34970_cs.py +0 -114
- egse/tempcontrol/agilent/agilent34970_devif.py +0 -182
- egse/tempcontrol/agilent/agilent34970_protocol.py +0 -96
- egse/tempcontrol/agilent/agilent34972.py +0 -111
- egse/tempcontrol/agilent/agilent34972.yaml +0 -44
- egse/tempcontrol/agilent/agilent34972_cs.py +0 -115
- egse/tempcontrol/agilent/agilent34972_devif.py +0 -189
- egse/tempcontrol/agilent/agilent34972_protocol.py +0 -98
- egse/tempcontrol/beaglebone/beaglebone.py +0 -341
- egse/tempcontrol/beaglebone/beaglebone.yaml +0 -110
- egse/tempcontrol/beaglebone/beaglebone_cs.py +0 -117
- egse/tempcontrol/beaglebone/beaglebone_protocol.py +0 -134
- egse/tempcontrol/beaglebone/beaglebone_ui.py +0 -674
- egse/tempcontrol/digalox/digalox.py +0 -115
- egse/tempcontrol/digalox/digalox.yaml +0 -36
- egse/tempcontrol/digalox/digalox_cs.py +0 -108
- egse/tempcontrol/digalox/digalox_protocol.py +0 -56
- egse/tempcontrol/keithley/__init__.py +0 -33
- egse/tempcontrol/keithley/daq6510.py +0 -662
- egse/tempcontrol/keithley/daq6510.yaml +0 -105
- egse/tempcontrol/keithley/daq6510_cs.py +0 -163
- egse/tempcontrol/keithley/daq6510_devif.py +0 -343
- egse/tempcontrol/keithley/daq6510_protocol.py +0 -79
- egse/tempcontrol/keithley/daq6510_sim.py +0 -186
- egse/tempcontrol/lakeshore/__init__.py +0 -33
- egse/tempcontrol/lakeshore/lsci.py +0 -361
- egse/tempcontrol/lakeshore/lsci.yaml +0 -162
- egse/tempcontrol/lakeshore/lsci_cs.py +0 -174
- egse/tempcontrol/lakeshore/lsci_devif.py +0 -292
- egse/tempcontrol/lakeshore/lsci_protocol.py +0 -76
- egse/tempcontrol/lakeshore/lsci_ui.py +0 -387
- egse/tempcontrol/ni/__init__.py +0 -0
- egse/tempcontrol/spid/spid.py +0 -109
- egse/tempcontrol/spid/spid.yaml +0 -81
- egse/tempcontrol/spid/spid_controller.py +0 -279
- egse/tempcontrol/spid/spid_cs.py +0 -136
- egse/tempcontrol/spid/spid_protocol.py +0 -107
- egse/tempcontrol/spid/spid_ui.py +0 -723
- egse/tempcontrol/srs/__init__.py +0 -22
- egse/tempcontrol/srs/ptc10.py +0 -867
- egse/tempcontrol/srs/ptc10.yaml +0 -227
- egse/tempcontrol/srs/ptc10_cs.py +0 -128
- egse/tempcontrol/srs/ptc10_devif.py +0 -116
- egse/tempcontrol/srs/ptc10_protocol.py +0 -39
- egse/tempcontrol/srs/ptc10_ui.py +0 -906
- egse/ups/apc/apc.py +0 -236
- egse/ups/apc/apc.yaml +0 -45
- egse/ups/apc/apc_cs.py +0 -101
- egse/ups/apc/apc_protocol.py +0 -125
- egse/user.yaml +0 -7
- egse/vacuum/beaglebone/beaglebone.py +0 -149
- egse/vacuum/beaglebone/beaglebone.yaml +0 -44
- egse/vacuum/beaglebone/beaglebone_cs.py +0 -108
- egse/vacuum/beaglebone/beaglebone_devif.py +0 -159
- egse/vacuum/beaglebone/beaglebone_protocol.py +0 -192
- egse/vacuum/beaglebone/beaglebone_ui.py +0 -638
- egse/vacuum/instrutech/igm402.py +0 -91
- egse/vacuum/instrutech/igm402.yaml +0 -90
- egse/vacuum/instrutech/igm402_controller.py +0 -124
- egse/vacuum/instrutech/igm402_cs.py +0 -108
- egse/vacuum/instrutech/igm402_interface.py +0 -49
- egse/vacuum/instrutech/igm402_simulator.py +0 -36
- egse/vacuum/keller/kellerBus.py +0 -256
- egse/vacuum/keller/leo3.py +0 -100
- egse/vacuum/keller/leo3.yaml +0 -38
- egse/vacuum/keller/leo3_controller.py +0 -81
- egse/vacuum/keller/leo3_cs.py +0 -101
- egse/vacuum/keller/leo3_interface.py +0 -33
- egse/vacuum/mks/evision.py +0 -86
- egse/vacuum/mks/evision.yaml +0 -75
- egse/vacuum/mks/evision_cs.py +0 -101
- egse/vacuum/mks/evision_devif.py +0 -313
- egse/vacuum/mks/evision_interface.py +0 -60
- egse/vacuum/mks/evision_simulator.py +0 -24
- egse/vacuum/mks/evision_ui.py +0 -701
- egse/vacuum/pfeiffer/acp40.py +0 -87
- egse/vacuum/pfeiffer/acp40.yaml +0 -60
- egse/vacuum/pfeiffer/acp40_controller.py +0 -117
- egse/vacuum/pfeiffer/acp40_cs.py +0 -109
- egse/vacuum/pfeiffer/acp40_interface.py +0 -40
- egse/vacuum/pfeiffer/acp40_simulator.py +0 -37
- egse/vacuum/pfeiffer/tc400.py +0 -87
- egse/vacuum/pfeiffer/tc400.yaml +0 -83
- egse/vacuum/pfeiffer/tc400_controller.py +0 -136
- egse/vacuum/pfeiffer/tc400_cs.py +0 -109
- egse/vacuum/pfeiffer/tc400_interface.py +0 -70
- egse/vacuum/pfeiffer/tc400_simulator.py +0 -35
- egse/vacuum/pfeiffer/tpg261.py +0 -80
- egse/vacuum/pfeiffer/tpg261.yaml +0 -66
- egse/vacuum/pfeiffer/tpg261_controller.py +0 -150
- egse/vacuum/pfeiffer/tpg261_cs.py +0 -109
- egse/vacuum/pfeiffer/tpg261_interface.py +0 -59
- egse/vacuum/pfeiffer/tpg261_simulator.py +0 -23
- egse/version.py +0 -174
- egse/visitedpositions.py +0 -398
- egse/windowing.py +0 -213
- egse/zmq/__init__.py +0 -28
- egse/zmq/spw.py +0 -160
- egse/zmq_ser.py +0 -41
- scripts/alerts/cold.yaml +0 -278
- scripts/alerts/example_alerts.yaml +0 -54
- scripts/alerts/transition.yaml +0 -14
- scripts/alerts/warm.yaml +0 -49
- scripts/analyse_n_fee_hk_data.py +0 -52
- scripts/check_hdf5_files.py +0 -192
- scripts/check_register_sync.py +0 -47
- scripts/check_tcs_calib_coef.py +0 -90
- scripts/correct_ccd_cold_temperature_cal.py +0 -157
- scripts/create_hdf5_report.py +0 -293
- scripts/csl_model.py +0 -420
- scripts/csl_restore_setup.py +0 -229
- scripts/export-grafana-dashboards.py +0 -49
- scripts/fdir/cs_recovery/fdir_cs_recovery.py +0 -54
- scripts/fdir/fdir_table.yaml +0 -70
- scripts/fdir/fdir_test_recovery.py +0 -10
- scripts/fdir/hw_recovery/fdir_agilent_hw_recovery.py +0 -73
- scripts/fdir/limit_recovery/fdir_agilent_limit.py +0 -61
- scripts/fdir/limit_recovery/fdir_bb_heater_limit.py +0 -59
- scripts/fdir/limit_recovery/fdir_ensemble_limit.py +0 -33
- scripts/fdir/limit_recovery/fdir_pressure_limit_recovery.py +0 -71
- scripts/fix_csv.py +0 -80
- scripts/ias/correct_ccd_temp_cal_elfique.py +0 -43
- scripts/ias/correct_ccd_temp_cal_floreffe.py +0 -43
- scripts/ias/correct_trp_swap_achel.py +0 -199
- scripts/inta/correct_ccd_temp_cal_duvel.py +0 -43
- scripts/inta/correct_ccd_temp_cal_gueuze.py +0 -43
- scripts/n_fee_supply_voltage_calculation.py +0 -92
- scripts/playground.py +0 -30
- scripts/print_hdf5_hk_data.py +0 -68
- scripts/print_register_map.py +0 -43
- scripts/remove_lines_between_matches.py +0 -188
- scripts/sron/commanding/control_heaters.py +0 -44
- scripts/sron/commanding/pumpdown.py +0 -46
- scripts/sron/commanding/set_pid_setpoint.py +0 -19
- scripts/sron/commanding/shutdown_bbb_heaters.py +0 -10
- scripts/sron/commanding/shutdown_pumps.py +0 -33
- scripts/sron/correct_mgse_coordinates_brigand_chimay.py +0 -272
- scripts/sron/correct_trp_swap_brigand.py +0 -204
- scripts/sron/gimbal_conversions.py +0 -75
- scripts/sron/tm_gen/tm_gen_agilent.py +0 -37
- scripts/sron/tm_gen/tm_gen_heaters.py +0 -4
- scripts/sron/tm_gen/tm_gen_spid.py +0 -13
- scripts/update_operational_cgse.py +0 -268
- 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())
|