dls-dodal 1.31.1__py3-none-any.whl → 1.32.0__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.
- {dls_dodal-1.31.1.dist-info → dls_dodal-1.32.0.dist-info}/METADATA +3 -2
- {dls_dodal-1.31.1.dist-info → dls_dodal-1.32.0.dist-info}/RECORD +23 -23
- {dls_dodal-1.31.1.dist-info → dls_dodal-1.32.0.dist-info}/WHEEL +1 -1
- dodal/_version.py +2 -2
- dodal/beamlines/i03.py +1 -1
- dodal/beamlines/i04.py +1 -0
- dodal/beamlines/i22.py +1 -0
- dodal/devices/aperturescatterguard.py +10 -10
- dodal/devices/areadetector/plugins/MJPG.py +32 -8
- dodal/devices/fast_grid_scan.py +2 -1
- dodal/devices/focusing_mirror.py +4 -5
- dodal/devices/i24/pmac.py +77 -15
- dodal/devices/robot.py +18 -6
- dodal/devices/tetramm.py +11 -16
- dodal/devices/undulator.py +91 -2
- dodal/devices/undulator_dcm.py +3 -57
- dodal/devices/webcam.py +33 -7
- dodal/devices/zocalo/zocalo_interaction.py +6 -1
- dodal/devices/zocalo/zocalo_results.py +114 -10
- dodal/utils.py +4 -3
- {dls_dodal-1.31.1.dist-info → dls_dodal-1.32.0.dist-info}/LICENSE +0 -0
- {dls_dodal-1.31.1.dist-info → dls_dodal-1.32.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.31.1.dist-info → dls_dodal-1.32.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dls-dodal
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.32.0
|
|
4
4
|
Summary: Ophyd devices and other utils that could be used across DLS beamlines
|
|
5
5
|
Author-email: Dominic Oram <dominic.oram@diamond.ac.uk>
|
|
6
6
|
License: Apache License
|
|
@@ -216,7 +216,7 @@ Description-Content-Type: text/markdown
|
|
|
216
216
|
License-File: LICENSE
|
|
217
217
|
Requires-Dist: click
|
|
218
218
|
Requires-Dist: ophyd
|
|
219
|
-
Requires-Dist: ophyd-async
|
|
219
|
+
Requires-Dist: ophyd-async <0.7,>=0.6
|
|
220
220
|
Requires-Dist: bluesky
|
|
221
221
|
Requires-Dist: pyepics
|
|
222
222
|
Requires-Dist: dataclasses-json
|
|
@@ -232,6 +232,7 @@ Requires-Dist: numpy <2.0
|
|
|
232
232
|
Requires-Dist: aiofiles
|
|
233
233
|
Requires-Dist: aiohttp
|
|
234
234
|
Requires-Dist: redis
|
|
235
|
+
Requires-Dist: deepdiff
|
|
235
236
|
Provides-Extra: dev
|
|
236
237
|
Requires-Dist: black ; extra == 'dev'
|
|
237
238
|
Requires-Dist: diff-cover ; extra == 'dev'
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
dodal/__init__.py,sha256=v-rRiDOgZ3sQSMQKq0vgUQZvpeOkoHFXissAx6Ktg84,61
|
|
2
2
|
dodal/__main__.py,sha256=kP2S2RPitnOWpNGokjZ1Yq-1umOtp5sNOZk2B3tBPLM,111
|
|
3
|
-
dodal/_version.py,sha256=
|
|
3
|
+
dodal/_version.py,sha256=DJRUo3ZQOOrgoMbrpGNhypFaRgQ4TvlEoMxoldzMF6Y,413
|
|
4
4
|
dodal/adsim.py,sha256=OW2dcS7ciD4Yq9WFw4PN_c5Bwccrmu7R-zr-u6ZCbQM,497
|
|
5
5
|
dodal/cli.py,sha256=_crmaHchxphSW8eEJB58_XZIeK82aiUv9bV7tpz-LpA,2122
|
|
6
6
|
dodal/log.py,sha256=0to7CRsbzbgVfAAfKRAMhsaUuKqF2-7CGdQc-z8Uhno,9499
|
|
7
|
-
dodal/utils.py,sha256=
|
|
7
|
+
dodal/utils.py,sha256=zlHPQjJOYeEvdC-UHPRvuTZaLt4hG6o9x2Vm4eHFBDU,11851
|
|
8
8
|
dodal/beamline_specific_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
dodal/beamline_specific_utils/i03.py,sha256=eM6ZWZzpL0JYNBff8LhOnwFoZTJ5PDCY2XWI7ZKdtFY,276
|
|
10
10
|
dodal/beamlines/README.md,sha256=K9MkL_GomxlsoTB7Mz-_dJA5NNSbmCfMiutchGg3C8o,404
|
|
11
11
|
dodal/beamlines/__init__.py,sha256=CD0Dz2H1adLsqY4H3-_QxTdODPZD6mquMfsep5W5s0Q,3076
|
|
12
|
-
dodal/beamlines/i03.py,sha256=
|
|
13
|
-
dodal/beamlines/i04.py,sha256=
|
|
12
|
+
dodal/beamlines/i03.py,sha256=8Lra4u1kLjWUoboxuSx6Po92OymRaKaJjM9ERr9zeUI,17004
|
|
13
|
+
dodal/beamlines/i04.py,sha256=pRVNIBaUn3oIgbxInCgBUwLqHtPirxf0fKfTfx4RcJ8,13877
|
|
14
14
|
dodal/beamlines/i04_1.py,sha256=KDxSUQNhIs_NFiRaLY-Jiory0DeN7Y0ErvGuoTrwCDU,4731
|
|
15
15
|
dodal/beamlines/i13_1.py,sha256=csXHrdwUh4sXTmb4X6ZiiSS_XxRkNShsVoBMxYI6rG0,1833
|
|
16
16
|
dodal/beamlines/i20_1.py,sha256=MaPgONHqpoZuBtkiKEzYtViJnKBM2_ekeP4OdbmuXHE,1158
|
|
17
|
-
dodal/beamlines/i22.py,sha256=
|
|
17
|
+
dodal/beamlines/i22.py,sha256=YWTz2PjOMTEO7n3QRfrCerIEUMHd6JTHsd2dYe_4F7c,9915
|
|
18
18
|
dodal/beamlines/i23.py,sha256=2j5qLoqE_hg9ETHqNkOVu7LLkVB8qalgXeORnVYKN_I,1075
|
|
19
19
|
dodal/beamlines/i24.py,sha256=1XVCNWbzowQB6CWpJGSLp9Ih0zypktIzHxb64h-Xj6Y,6583
|
|
20
20
|
dodal/beamlines/p38.py,sha256=mCDjRVpY6AIFIAAYCWe-jGABHadFwfJB7poa-dGMU6s,7940
|
|
@@ -35,24 +35,24 @@ dodal/devices/CTAB.py,sha256=MoExneblYUHg9Va8vAVx_p_Vw_HnqbhkzxxrX7Ic_wo,2000
|
|
|
35
35
|
dodal/devices/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
36
|
dodal/devices/adsim.py,sha256=dMU0TKIuiODHYFHQOH4_5UvB8iJtaJEtjqaEDGjcU-w,311
|
|
37
37
|
dodal/devices/aperture.py,sha256=BLaroQ3n8yd7uZyacJ3KvDWZH8yhA_sJc8b49QMKg9o,585
|
|
38
|
-
dodal/devices/aperturescatterguard.py,sha256=
|
|
38
|
+
dodal/devices/aperturescatterguard.py,sha256=fOdwQl_qZLaUo1cI1-wz1t2OqncMoUm_uqeZNWyHn3M,8459
|
|
39
39
|
dodal/devices/attenuator.py,sha256=viK1iccNekX6ZvR_ZmSwj5JdM1j2B8pcTg8qWDdmzhQ,2584
|
|
40
40
|
dodal/devices/backlight.py,sha256=mOnptopsVOsT8JUIX_siDRgJ73CQPz_bm0Eb7oA81wc,1607
|
|
41
41
|
dodal/devices/cryostream.py,sha256=CpNA2HGhN_PXkL9eqH_yAPsDxyOLIiehlUxEoNmXJVg,668
|
|
42
42
|
dodal/devices/dcm.py,sha256=eZNMGjLM56Ll0siU14XomB77W_grLIdxIrMOQNmYFG8,1609
|
|
43
43
|
dodal/devices/eiger.py,sha256=sR-Fr97Y0lzzq57fFOUTwUZw5E7asoj36A1JR1QUkLI,13985
|
|
44
44
|
dodal/devices/eiger_odin.py,sha256=tDpEhOUY02YManYAMRI3TwSDDa3uITBxI0JHevaK7Rk,7010
|
|
45
|
-
dodal/devices/fast_grid_scan.py,sha256=
|
|
45
|
+
dodal/devices/fast_grid_scan.py,sha256=WQGeKR-82fbnY4zUD_MQBsQyJgyIiuRpJK5nn_mfR1E,11969
|
|
46
46
|
dodal/devices/fluorescence_detector_motion.py,sha256=5IcyaVHXa9TXLFlLB0tfpQ1_ThgIRJNaFNw_uj6ahCA,501
|
|
47
47
|
dodal/devices/flux.py,sha256=RtPStHw7Mad0igVKntKWVZfuZn2clokVJqH14HLix6M,198
|
|
48
|
-
dodal/devices/focusing_mirror.py,sha256
|
|
48
|
+
dodal/devices/focusing_mirror.py,sha256=-jq2uqBfDjSgRVrV3-sqswPAND72cagBUQVvzd04diw,5901
|
|
49
49
|
dodal/devices/hutch_shutter.py,sha256=_-hR3SJHM05YHV_fEtc0VYOLamYnpVGDE56AwJGJS48,3320
|
|
50
50
|
dodal/devices/ipin.py,sha256=qsf8E3xrJYNDwzsacNLCCp3gaqsadqmN1b-Fvou8y8k,420
|
|
51
51
|
dodal/devices/linkam3.py,sha256=3oYwPtaKSPvLKEat8m7tuhE4Wizz8mg8HMrEWPCYxn0,3820
|
|
52
52
|
dodal/devices/logging_ophyd_device.py,sha256=dUVE-XhWA56WUXez0mrc4sf322CXY3MVLreTycO5j_A,668
|
|
53
53
|
dodal/devices/motors.py,sha256=dYa9T6FDMTbr8GvTb-lXtk3v4QEqAWRuGmHIO20fazQ,1039
|
|
54
54
|
dodal/devices/p45.py,sha256=jzBW2fGRhIbGzSRs5Fgupxro6aqE611n1RTcrTTG-yY,1047
|
|
55
|
-
dodal/devices/robot.py,sha256=
|
|
55
|
+
dodal/devices/robot.py,sha256=yzRq-77fVrlhxaIqeORJLGkDHzGgLrHsFYTxmkG-b0w,5343
|
|
56
56
|
dodal/devices/s4_slit_gaps.py,sha256=j3kgF9WfGFaU9xdUuiAh-QqI5u_vhiAftaDVINt91SM,243
|
|
57
57
|
dodal/devices/scatterguard.py,sha256=jx03in9QgaThWxD4t1S8_Llent2kWrn_hThJ9KkUWTk,330
|
|
58
58
|
dodal/devices/scintillator.py,sha256=PlD6cnJ39PTB_e7QrRspPliLYE4kL_K7ziJURzuxgdA,365
|
|
@@ -60,12 +60,12 @@ dodal/devices/slits.py,sha256=uOyVmbgeygiP6e5Z9t5zMPXLuVEWFfYg9GB3ZU76Tug,600
|
|
|
60
60
|
dodal/devices/smargon.py,sha256=hX-tCftKumxk67eS5-_gQRmYOrjSyQ4s3mMJsTRuvCk,4706
|
|
61
61
|
dodal/devices/status.py,sha256=hVrJS1yooQo6PRumRACoIEh-SKBUKxvBlQl-MtLFUMQ,327
|
|
62
62
|
dodal/devices/synchrotron.py,sha256=QtTufJA_fCaBawHougSc7nxwu240oX46_y0P-4qIW8o,1960
|
|
63
|
-
dodal/devices/tetramm.py,sha256=
|
|
63
|
+
dodal/devices/tetramm.py,sha256=EgtaExJBZWZC6lWgUEg0RcWQYmSKwRm20KjTZyhCjBk,8439
|
|
64
64
|
dodal/devices/thawer.py,sha256=Gq-3f__KJUM6_Ds9OVxpZ5jC447HywJxQGXen6L33Lk,1616
|
|
65
65
|
dodal/devices/turbo_slit.py,sha256=B6SPXqviMnG-U4PnUF1BdTO0LBKmTuwAUKRbxMiNJXo,1125
|
|
66
|
-
dodal/devices/undulator.py,sha256=
|
|
67
|
-
dodal/devices/undulator_dcm.py,sha256=
|
|
68
|
-
dodal/devices/webcam.py,sha256=
|
|
66
|
+
dodal/devices/undulator.py,sha256=udwAodxYM9XgtsQGH2PDBA6ehtel5dAFkjsK13nKp6Q,5160
|
|
67
|
+
dodal/devices/undulator_dcm.py,sha256=5hn3UZeu4CYXmfUVSdIxjrcIpStgeA1S744p0iIFp4I,2725
|
|
68
|
+
dodal/devices/webcam.py,sha256=EqdzUBov5wMCULzzkfnCfD-5TQMZFQLp-2nlDHezmPs,2332
|
|
69
69
|
dodal/devices/xbpm_feedback.py,sha256=-1wbnahJ_oSljQR0Sjiwn3mytVP-VwsAy0a_YPjPM0Y,1168
|
|
70
70
|
dodal/devices/zebra.py,sha256=iTHkKv8EP-gkr0Cl2gR9yxt2qTHT2Q4etS67Rshf83k,9327
|
|
71
71
|
dodal/devices/zebra_controlled_shutter.py,sha256=w2ISASJ_sb3dbQGi63Yuj3ymTkjX73aSl_ZTYs8TyaI,1860
|
|
@@ -73,7 +73,7 @@ dodal/devices/areadetector/__init__.py,sha256=8IwLxuZMW0MOJpJp_ZDdlaE20hrtsH_PXW
|
|
|
73
73
|
dodal/devices/areadetector/adaravis.py,sha256=Cqw_Mzrp_zODFxQ2LZBJzHp_DsZ6_dAITkZz8gYz_0w,3797
|
|
74
74
|
dodal/devices/areadetector/adsim.py,sha256=cIc9PRbKnftBk7Ut8d8CU_TVrin8EwcKHObP2n9VxWM,1876
|
|
75
75
|
dodal/devices/areadetector/adutils.py,sha256=4axFR3wtn-K-sjMVJyfTcu-8g35odf2cY8mTKv1gS-o,3093
|
|
76
|
-
dodal/devices/areadetector/plugins/MJPG.py,sha256=
|
|
76
|
+
dodal/devices/areadetector/plugins/MJPG.py,sha256=XztHFB1e7qHeZORYxvPgHfDMfkpXGKvfQYmej_hGVVc,4934
|
|
77
77
|
dodal/devices/detector/__init__.py,sha256=-RdACL3tzc3lLArWOoGNje48UUlv2fElOmGOz9yOuO0,317
|
|
78
78
|
dodal/devices/detector/det_dim_constants.py,sha256=LNrVMd0DbFEcnyNFmXosCP-VYaZ71Ajuv6inwo4Mg3U,2299
|
|
79
79
|
dodal/devices/detector/det_dist_to_beam_converter.py,sha256=7keoqZYfvgayePVx97lHYpcFRTJnQOfAk_PYP4EZTZQ,1951
|
|
@@ -93,7 +93,7 @@ dodal/devices/i24/dcm.py,sha256=nP2qymTy5TrOu078XOY7h1TEwVfcgli5lTyxmwsG4O8,1990
|
|
|
93
93
|
dodal/devices/i24/dual_backlight.py,sha256=Th-RKr28aFxE8LCT_mdN9KkRIVw0BHLGKkI0ienfRZU,2049
|
|
94
94
|
dodal/devices/i24/i24_detector_motion.py,sha256=_HgdsZqFYY0tKqUgMzViHaPEUFXL3WlXXioGvDehRUw,364
|
|
95
95
|
dodal/devices/i24/i24_vgonio.py,sha256=Igqs7687z6lyhGVeJEDtDmPachYxU48MUH2BF0RpK9Q,461
|
|
96
|
-
dodal/devices/i24/pmac.py,sha256=
|
|
96
|
+
dodal/devices/i24/pmac.py,sha256=qug40tz00vjvmQox3W6GrEcorriEeGkNyP5m4u5CVHo,7201
|
|
97
97
|
dodal/devices/oav/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
98
98
|
dodal/devices/oav/grid_overlay.py,sha256=kYs4sCvmo7yG75HQtptuI8jPzM7dR4fSqnOGL0D5j6g,5338
|
|
99
99
|
dodal/devices/oav/microns_for_zoom_levels.json,sha256=kJTkbu2v6_Ccc_cDy7FRTX-gRhXxfYskjVqwBCZIqCQ,1209
|
|
@@ -120,15 +120,15 @@ dodal/devices/util/test_utils.py,sha256=VrSFFGLNKOcCAsWFMZOxwhng3wGR5kV8NqqnKfj8
|
|
|
120
120
|
dodal/devices/xspress3/xspress3.py,sha256=JTx3ppAc8GwV9K-Gfqo81iGYH_L-ONyFWiPRs9XUs-w,4661
|
|
121
121
|
dodal/devices/xspress3/xspress3_channel.py,sha256=yJRwseLmtkW2Vv6GB8sLdOFuBn3e4c9Q8fgPacMgl5w,1638
|
|
122
122
|
dodal/devices/zocalo/__init__.py,sha256=oPhjFB39yf2NWkGD-MMcPFnnOVZ_RtdyBt2OLYn-Xa4,505
|
|
123
|
-
dodal/devices/zocalo/zocalo_interaction.py,sha256=
|
|
124
|
-
dodal/devices/zocalo/zocalo_results.py,sha256=
|
|
123
|
+
dodal/devices/zocalo/zocalo_interaction.py,sha256=y8YKMaVwfsRPBofHGGLYmYsd4QwMvm7JIPEo6wrN_Xo,3493
|
|
124
|
+
dodal/devices/zocalo/zocalo_results.py,sha256=MStx8iK--ITff-rT3AQu_RHnqKqNGLJDVyV3ewBwaKE,14316
|
|
125
125
|
dodal/parameters/experiment_parameter_base.py,sha256=O7JamfuJ5cYHkPf9tsHJPqn-OMHTAGouigvM1cDFehE,313
|
|
126
126
|
dodal/plans/check_topup.py,sha256=3gyLHfHNQBCgEWuAg4QE-ONx7y2Do1vVv5HP8ss0Z1I,5371
|
|
127
127
|
dodal/plans/data_session_metadata.py,sha256=urexZ3mA0K6VWxVW3MlrcsB1Tyi09tFvpKBlaVil7TQ,1567
|
|
128
128
|
dodal/plans/motor_util_plans.py,sha256=JT1K4DBB66MrzNqimxFgiL6mRsj11fF7xZXOz0udEeo,4522
|
|
129
|
-
dls_dodal-1.
|
|
130
|
-
dls_dodal-1.
|
|
131
|
-
dls_dodal-1.
|
|
132
|
-
dls_dodal-1.
|
|
133
|
-
dls_dodal-1.
|
|
134
|
-
dls_dodal-1.
|
|
129
|
+
dls_dodal-1.32.0.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
|
|
130
|
+
dls_dodal-1.32.0.dist-info/METADATA,sha256=Y7XXnR4vFkIsqoZ6vXh9STdoUyc9cbudVRqzP30rHUM,16574
|
|
131
|
+
dls_dodal-1.32.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
132
|
+
dls_dodal-1.32.0.dist-info/entry_points.txt,sha256=bycw_EKUzup_rxfCetOwcauXV4kLln_OPpPT8jEnr-I,94
|
|
133
|
+
dls_dodal-1.32.0.dist-info/top_level.txt,sha256=xIozdmZk_wmMV4wugpq9-6eZs0vgADNUKz3j2UAwlhc,6
|
|
134
|
+
dls_dodal-1.32.0.dist-info/RECORD,,
|
dodal/_version.py
CHANGED
dodal/beamlines/i03.py
CHANGED
|
@@ -316,6 +316,7 @@ def undulator(
|
|
|
316
316
|
wait_for_connection,
|
|
317
317
|
fake_with_ophyd_sim,
|
|
318
318
|
bl_prefix=False,
|
|
319
|
+
id_gap_lookup_table_path="/dls_sw/i03/software/daq_configuration/lookup/BeamLine_Undulator_toGap.txt",
|
|
319
320
|
)
|
|
320
321
|
|
|
321
322
|
|
|
@@ -334,7 +335,6 @@ def undulator_dcm(
|
|
|
334
335
|
undulator=undulator(wait_for_connection, fake_with_ophyd_sim),
|
|
335
336
|
dcm=dcm(wait_for_connection, fake_with_ophyd_sim),
|
|
336
337
|
daq_configuration_path=DAQ_CONFIGURATION_PATH,
|
|
337
|
-
id_gap_lookup_table_path="/dls_sw/i03/software/daq_configuration/lookup/BeamLine_Undulator_toGap.txt",
|
|
338
338
|
)
|
|
339
339
|
|
|
340
340
|
|
dodal/beamlines/i04.py
CHANGED
dodal/beamlines/i22.py
CHANGED
|
@@ -220,15 +220,15 @@ class ApertureScatterguard(StandardReadable, Movable):
|
|
|
220
220
|
self.aperture.y.set(aperture_y),
|
|
221
221
|
self.aperture.z.set(aperture_z),
|
|
222
222
|
)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
223
|
+
else:
|
|
224
|
+
await asyncio.gather(
|
|
225
|
+
self.aperture.x.set(aperture_x),
|
|
226
|
+
self.aperture.y.set(aperture_y),
|
|
227
|
+
self.aperture.z.set(aperture_z),
|
|
228
|
+
)
|
|
229
229
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
230
|
+
await asyncio.gather(
|
|
231
|
+
self.scatterguard.x.set(scatterguard_x),
|
|
232
|
+
self.scatterguard.y.set(scatterguard_y),
|
|
233
|
+
)
|
|
234
234
|
await self.selected_aperture.set(value)
|
|
@@ -87,11 +87,12 @@ class MJPG(Device, ABC):
|
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
class SnapshotWithBeamCentre(MJPG):
|
|
90
|
-
"""A child of MJPG which, when triggered, draws
|
|
91
|
-
image and saves the image to disk."""
|
|
90
|
+
"""A child of MJPG which, when triggered, draws an outlined crosshair at the beam
|
|
91
|
+
centre in the image and saves the image to disk."""
|
|
92
92
|
|
|
93
93
|
CROSSHAIR_LENGTH_PX = 20
|
|
94
|
-
|
|
94
|
+
CROSSHAIR_OUTLINE_COLOUR = "Black"
|
|
95
|
+
CROSSHAIR_FILL_COLOUR = "White"
|
|
95
96
|
|
|
96
97
|
def post_processing(self, image: Image.Image):
|
|
97
98
|
assert (
|
|
@@ -100,15 +101,38 @@ class SnapshotWithBeamCentre(MJPG):
|
|
|
100
101
|
beam_x = self.oav_params.beam_centre_i
|
|
101
102
|
beam_y = self.oav_params.beam_centre_j
|
|
102
103
|
|
|
104
|
+
SnapshotWithBeamCentre.draw_crosshair(image, beam_x, beam_y)
|
|
105
|
+
|
|
106
|
+
self._save_image(image)
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def draw_crosshair(cls, image: Image.Image, beam_x: int, beam_y: int):
|
|
103
110
|
draw = ImageDraw.Draw(image)
|
|
104
|
-
|
|
111
|
+
OUTLINE_WIDTH = 1
|
|
112
|
+
HALF_LEN = cls.CROSSHAIR_LENGTH_PX / 2
|
|
113
|
+
draw.rectangle(
|
|
114
|
+
[
|
|
115
|
+
beam_x - OUTLINE_WIDTH,
|
|
116
|
+
beam_y - HALF_LEN - OUTLINE_WIDTH,
|
|
117
|
+
beam_x + OUTLINE_WIDTH,
|
|
118
|
+
beam_y + HALF_LEN + OUTLINE_WIDTH,
|
|
119
|
+
],
|
|
120
|
+
fill=cls.CROSSHAIR_OUTLINE_COLOUR,
|
|
121
|
+
)
|
|
122
|
+
draw.rectangle(
|
|
123
|
+
[
|
|
124
|
+
beam_x - HALF_LEN - OUTLINE_WIDTH,
|
|
125
|
+
beam_y - OUTLINE_WIDTH,
|
|
126
|
+
beam_x + HALF_LEN + OUTLINE_WIDTH,
|
|
127
|
+
beam_y + OUTLINE_WIDTH,
|
|
128
|
+
],
|
|
129
|
+
fill=cls.CROSSHAIR_OUTLINE_COLOUR,
|
|
130
|
+
)
|
|
105
131
|
draw.line(
|
|
106
132
|
((beam_x, beam_y - HALF_LEN), (beam_x, beam_y + HALF_LEN)),
|
|
107
|
-
fill=
|
|
133
|
+
fill=cls.CROSSHAIR_FILL_COLOUR,
|
|
108
134
|
)
|
|
109
135
|
draw.line(
|
|
110
136
|
((beam_x - HALF_LEN, beam_y), (beam_x + HALF_LEN, beam_y)),
|
|
111
|
-
fill=
|
|
137
|
+
fill=cls.CROSSHAIR_FILL_COLOUR,
|
|
112
138
|
)
|
|
113
|
-
|
|
114
|
-
self._save_image(image)
|
dodal/devices/fast_grid_scan.py
CHANGED
|
@@ -3,6 +3,7 @@ from typing import Generic, TypeVar
|
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
from bluesky.plan_stubs import mv
|
|
6
|
+
from bluesky.protocols import Flyable
|
|
6
7
|
from numpy import ndarray
|
|
7
8
|
from ophyd_async.core import (
|
|
8
9
|
AsyncStatus,
|
|
@@ -193,7 +194,7 @@ class ExpectedImages(SignalR[int]):
|
|
|
193
194
|
return first_grid + second_grid
|
|
194
195
|
|
|
195
196
|
|
|
196
|
-
class FastGridScanCommon(StandardReadable, ABC, Generic[ParamType]):
|
|
197
|
+
class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
|
|
197
198
|
"""Device for a general fast grid scan
|
|
198
199
|
|
|
199
200
|
When the motion program is started, the goniometer will move in a snake-like grid trajectory,
|
dodal/devices/focusing_mirror.py
CHANGED
|
@@ -76,7 +76,7 @@ class MirrorVoltageDevice(Device):
|
|
|
76
76
|
LOGGER.debug(f"{setpoint_v.name} already at {value} - skipping set")
|
|
77
77
|
return
|
|
78
78
|
|
|
79
|
-
LOGGER.debug(f"
|
|
79
|
+
LOGGER.debug(f"Setting {setpoint_v.name} to {value}")
|
|
80
80
|
|
|
81
81
|
# Register an observer up front to ensure we don't miss events after we
|
|
82
82
|
# perform the set
|
|
@@ -85,16 +85,14 @@ class MirrorVoltageDevice(Device):
|
|
|
85
85
|
)
|
|
86
86
|
# discard the current value (OK) so we can await a subsequent change
|
|
87
87
|
await anext(demand_accepted_iterator)
|
|
88
|
-
|
|
88
|
+
set_status = setpoint_v.set(value, wait=False)
|
|
89
89
|
|
|
90
90
|
# The set should always change to SLEW regardless of whether we are
|
|
91
91
|
# already at the set point, then change back to OK/FAIL depending on
|
|
92
92
|
# success
|
|
93
93
|
accepted_value = await anext(demand_accepted_iterator)
|
|
94
94
|
assert accepted_value == MirrorVoltageDemand.SLEW
|
|
95
|
-
LOGGER.debug(
|
|
96
|
-
f"Demand not accepted for {setpoint_v.name}, waiting for acceptance..."
|
|
97
|
-
)
|
|
95
|
+
LOGGER.debug(f"Waiting for {setpoint_v.name} to set")
|
|
98
96
|
while MirrorVoltageDemand.SLEW == (
|
|
99
97
|
accepted_value := await anext(demand_accepted_iterator)
|
|
100
98
|
):
|
|
@@ -104,6 +102,7 @@ class MirrorVoltageDevice(Device):
|
|
|
104
102
|
raise AssertionError(
|
|
105
103
|
f"Voltage slew failed for {setpoint_v.name}, new state={accepted_value}"
|
|
106
104
|
)
|
|
105
|
+
await set_status
|
|
107
106
|
|
|
108
107
|
|
|
109
108
|
class VFMMirrorVoltages(StandardReadable):
|
dodal/devices/i24/pmac.py
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
from asyncio import sleep
|
|
1
2
|
from enum import Enum, IntEnum
|
|
2
|
-
from typing import SupportsFloat
|
|
3
3
|
|
|
4
|
-
from bluesky.protocols import Triggerable
|
|
4
|
+
from bluesky.protocols import Flyable, Triggerable
|
|
5
5
|
from ophyd_async.core import (
|
|
6
|
+
CALCULATE_TIMEOUT,
|
|
6
7
|
DEFAULT_TIMEOUT,
|
|
7
8
|
AsyncStatus,
|
|
8
|
-
CalculateTimeout,
|
|
9
9
|
SignalBackend,
|
|
10
10
|
SignalR,
|
|
11
11
|
SignalRW,
|
|
12
12
|
SoftSignalBackend,
|
|
13
13
|
StandardReadable,
|
|
14
|
+
soft_signal_rw,
|
|
14
15
|
wait_for_value,
|
|
15
16
|
)
|
|
16
17
|
from ophyd_async.epics.motor import Motor
|
|
@@ -89,7 +90,7 @@ class PMACStringLaser(SignalRW):
|
|
|
89
90
|
self,
|
|
90
91
|
value: LaserSettings,
|
|
91
92
|
wait=True,
|
|
92
|
-
timeout=
|
|
93
|
+
timeout=CALCULATE_TIMEOUT,
|
|
93
94
|
):
|
|
94
95
|
await self.signal.set(value.value, wait, timeout)
|
|
95
96
|
|
|
@@ -112,13 +113,13 @@ class PMACStringEncReset(SignalRW):
|
|
|
112
113
|
self,
|
|
113
114
|
value: EncReset,
|
|
114
115
|
wait=True,
|
|
115
|
-
timeout=
|
|
116
|
+
timeout=CALCULATE_TIMEOUT,
|
|
116
117
|
):
|
|
117
118
|
await self.signal.set(value.value, wait, timeout)
|
|
118
119
|
|
|
119
120
|
|
|
120
|
-
class ProgramRunner(SignalRW):
|
|
121
|
-
"""
|
|
121
|
+
class ProgramRunner(SignalRW, Flyable):
|
|
122
|
+
"""Run the collection by setting the program number on the PMAC string.
|
|
122
123
|
|
|
123
124
|
Once the program number has been set, wait for the collection to be complete.
|
|
124
125
|
This will only be true when the status becomes 0.
|
|
@@ -128,22 +129,73 @@ class ProgramRunner(SignalRW):
|
|
|
128
129
|
self,
|
|
129
130
|
pmac_str_sig: SignalRW,
|
|
130
131
|
status_sig: SignalR,
|
|
132
|
+
prog_num_sig: SignalRW,
|
|
133
|
+
collection_time_sig: SignalRW,
|
|
131
134
|
backend: SignalBackend,
|
|
132
135
|
timeout: float | None = DEFAULT_TIMEOUT,
|
|
133
136
|
name: str = "",
|
|
134
137
|
) -> None:
|
|
135
138
|
self.signal = pmac_str_sig
|
|
136
139
|
self.status = status_sig
|
|
140
|
+
self.prog_num = prog_num_sig
|
|
141
|
+
|
|
142
|
+
self.collection_time = collection_time_sig
|
|
143
|
+
self.KICKOFF_TIMEOUT = timeout
|
|
144
|
+
|
|
137
145
|
super().__init__(backend, timeout, name)
|
|
138
146
|
|
|
147
|
+
async def _get_prog_number_string(self) -> str:
|
|
148
|
+
prog_num = await self.prog_num.get_value()
|
|
149
|
+
return f"&2b{prog_num}r"
|
|
150
|
+
|
|
151
|
+
@AsyncStatus.wrap
|
|
152
|
+
async def kickoff(self):
|
|
153
|
+
"""Kick off the collection by sending a program number to the pmac_string and \
|
|
154
|
+
wait for the scan status PV to go to 1.
|
|
155
|
+
"""
|
|
156
|
+
prog_num_str = await self._get_prog_number_string()
|
|
157
|
+
await self.signal.set(prog_num_str, wait=True)
|
|
158
|
+
await wait_for_value(
|
|
159
|
+
self.status,
|
|
160
|
+
ScanState.RUNNING,
|
|
161
|
+
timeout=self.KICKOFF_TIMEOUT,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
@AsyncStatus.wrap
|
|
165
|
+
async def complete(self):
|
|
166
|
+
"""Stop collecting when the scan status PV goes to 0.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
complete_time (float): total time required by the collection to \
|
|
170
|
+
finish correctly.
|
|
171
|
+
"""
|
|
172
|
+
scan_complete_time = await self.collection_time.get_value()
|
|
173
|
+
await wait_for_value(self.status, ScanState.DONE, timeout=scan_complete_time)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class ProgramAbort(Triggerable):
|
|
177
|
+
"""Abort a data collection by setting the PMAC string and then wait for the \
|
|
178
|
+
status value to go back to 0.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def __init__(
|
|
182
|
+
self,
|
|
183
|
+
pmac_str_sig: SignalRW,
|
|
184
|
+
status_sig: SignalR,
|
|
185
|
+
) -> None:
|
|
186
|
+
self.signal = pmac_str_sig
|
|
187
|
+
self.status = status_sig
|
|
188
|
+
|
|
139
189
|
@AsyncStatus.wrap
|
|
140
|
-
async def
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
190
|
+
async def trigger(self):
|
|
191
|
+
await self.signal.set("A", wait=True)
|
|
192
|
+
await sleep(1.0) # TODO Check with scientist what this sleep is really for.
|
|
193
|
+
await self.signal.set("P2401=0", wait=True)
|
|
194
|
+
await wait_for_value(
|
|
195
|
+
self.status,
|
|
196
|
+
ScanState.DONE,
|
|
197
|
+
timeout=DEFAULT_TIMEOUT,
|
|
198
|
+
)
|
|
147
199
|
|
|
148
200
|
|
|
149
201
|
class PMAC(StandardReadable):
|
|
@@ -172,8 +224,18 @@ class PMAC(StandardReadable):
|
|
|
172
224
|
self.scanstatus = epics_signal_r(float, "BL24I-MO-STEP-14:signal:P2401")
|
|
173
225
|
self.counter = epics_signal_r(float, "BL24I-MO-STEP-14:signal:P2402")
|
|
174
226
|
|
|
227
|
+
# A couple of soft signals for running a collection: program number to send to
|
|
228
|
+
# the PMAC_STRING and expected collection time.
|
|
229
|
+
self.program_number = soft_signal_rw(int)
|
|
230
|
+
self.collection_time = soft_signal_rw(float, initial_value=600.0, units="s")
|
|
231
|
+
|
|
175
232
|
self.run_program = ProgramRunner(
|
|
176
|
-
self.pmac_string,
|
|
233
|
+
self.pmac_string,
|
|
234
|
+
self.scanstatus,
|
|
235
|
+
self.program_number,
|
|
236
|
+
self.collection_time,
|
|
237
|
+
backend=SoftSignalBackend(str),
|
|
177
238
|
)
|
|
239
|
+
self.abort_program = ProgramAbort(self.pmac_string, self.scanstatus)
|
|
178
240
|
|
|
179
241
|
super().__init__(name)
|
dodal/devices/robot.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from asyncio import FIRST_COMPLETED, CancelledError, Task
|
|
2
|
+
from asyncio import FIRST_COMPLETED, CancelledError, Task, wait_for
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from enum import Enum
|
|
5
5
|
|
|
@@ -41,6 +41,9 @@ class PinMounted(str, Enum):
|
|
|
41
41
|
class BartRobot(StandardReadable, Movable):
|
|
42
42
|
"""The sample changing robot."""
|
|
43
43
|
|
|
44
|
+
# How long to wait for the robot if it is busy soaking/drying
|
|
45
|
+
NOT_BUSY_TIMEOUT = 60
|
|
46
|
+
# How long to wait for the actual load to happen
|
|
44
47
|
LOAD_TIMEOUT = 60
|
|
45
48
|
NO_PIN_ERROR_CODE = 25
|
|
46
49
|
|
|
@@ -54,10 +57,15 @@ class BartRobot(StandardReadable, Movable):
|
|
|
54
57
|
) -> None:
|
|
55
58
|
self.barcode = epics_signal_r(str, prefix + "BARCODE")
|
|
56
59
|
self.gonio_pin_sensor = epics_signal_r(PinMounted, prefix + "PIN_MOUNTED")
|
|
60
|
+
|
|
57
61
|
self.next_pin = epics_signal_rw_rbv(float, prefix + "NEXT_PIN")
|
|
58
62
|
self.next_puck = epics_signal_rw_rbv(float, prefix + "NEXT_PUCK")
|
|
63
|
+
self.current_puck = epics_signal_r(float, prefix + "CURRENT_PUCK_RBV")
|
|
64
|
+
self.current_pin = epics_signal_r(float, prefix + "CURRENT_PIN_RBV")
|
|
65
|
+
|
|
59
66
|
self.next_sample_id = epics_signal_rw_rbv(float, prefix + "NEXT_ID")
|
|
60
67
|
self.sample_id = epics_signal_r(float, prefix + "CURRENT_ID_RBV")
|
|
68
|
+
|
|
61
69
|
self.load = epics_signal_x(prefix + "LOAD.PROC")
|
|
62
70
|
self.program_running = epics_signal_r(bool, prefix + "PROGRAM_RUNNING")
|
|
63
71
|
self.program_name = epics_signal_r(str, prefix + "PROGRAM_NAME")
|
|
@@ -93,7 +101,7 @@ class BartRobot(StandardReadable, Movable):
|
|
|
93
101
|
for task in finished:
|
|
94
102
|
await task
|
|
95
103
|
except CancelledError:
|
|
96
|
-
# If the outer enclosing task cancels after
|
|
104
|
+
# If the outer enclosing task cancels after a timeout, this causes CancelledError to be raised
|
|
97
105
|
# in the current task, when it propagates to here we should cancel all pending tasks before bubbling up
|
|
98
106
|
for task in tasks:
|
|
99
107
|
task.cancel()
|
|
@@ -105,7 +113,9 @@ class BartRobot(StandardReadable, Movable):
|
|
|
105
113
|
LOGGER.info(
|
|
106
114
|
f"Waiting on robot to finish {await self.program_name.get_value()}"
|
|
107
115
|
)
|
|
108
|
-
await wait_for_value(
|
|
116
|
+
await wait_for_value(
|
|
117
|
+
self.program_running, False, timeout=self.NOT_BUSY_TIMEOUT
|
|
118
|
+
)
|
|
109
119
|
await asyncio.gather(
|
|
110
120
|
set_and_wait_for_value(self.next_puck, sample_location.puck),
|
|
111
121
|
set_and_wait_for_value(self.next_pin, sample_location.pin),
|
|
@@ -121,10 +131,12 @@ class BartRobot(StandardReadable, Movable):
|
|
|
121
131
|
@AsyncStatus.wrap
|
|
122
132
|
async def set(self, value: SampleLocation):
|
|
123
133
|
try:
|
|
124
|
-
await
|
|
125
|
-
self._load_pin_and_puck(value),
|
|
134
|
+
await wait_for(
|
|
135
|
+
self._load_pin_and_puck(value),
|
|
136
|
+
timeout=self.LOAD_TIMEOUT + self.NOT_BUSY_TIMEOUT,
|
|
126
137
|
)
|
|
127
|
-
except asyncio.TimeoutError as e:
|
|
138
|
+
except (asyncio.TimeoutError, TimeoutError) as e:
|
|
139
|
+
# Will only need to catch asyncio.TimeoutError after https://github.com/bluesky/ophyd-async/issues/572
|
|
128
140
|
error_code = await self.error_code.get_value()
|
|
129
141
|
error_string = await self.error_str.get_value()
|
|
130
142
|
raise RobotLoadFailed(int(error_code), error_string) from e
|
dodal/devices/tetramm.py
CHANGED
|
@@ -3,13 +3,13 @@ from enum import Enum
|
|
|
3
3
|
|
|
4
4
|
from bluesky.protocols import Hints
|
|
5
5
|
from ophyd_async.core import (
|
|
6
|
-
AsyncStatus,
|
|
7
6
|
DatasetDescriber,
|
|
8
7
|
DetectorControl,
|
|
9
8
|
DetectorTrigger,
|
|
10
9
|
Device,
|
|
11
10
|
PathProvider,
|
|
12
11
|
StandardDetector,
|
|
12
|
+
TriggerInfo,
|
|
13
13
|
set_and_wait_for_value,
|
|
14
14
|
soft_signal_r_and_setter,
|
|
15
15
|
)
|
|
@@ -113,29 +113,24 @@ class TetrammController(DetectorControl):
|
|
|
113
113
|
# 2 internal clock cycles. Best effort approximation
|
|
114
114
|
return 2 / self.base_sample_rate
|
|
115
115
|
|
|
116
|
-
async def
|
|
117
|
-
self
|
|
118
|
-
|
|
119
|
-
trigger: DetectorTrigger = DetectorTrigger.edge_trigger,
|
|
120
|
-
exposure: float | None = None,
|
|
121
|
-
) -> AsyncStatus:
|
|
122
|
-
if exposure is None:
|
|
123
|
-
raise ValueError(
|
|
124
|
-
"Tetramm does not support arm without exposure time. "
|
|
125
|
-
"Is this a software scan? Tetramm only supports hardware scans."
|
|
126
|
-
)
|
|
127
|
-
self._validate_trigger(trigger)
|
|
116
|
+
async def prepare(self, trigger_info: TriggerInfo):
|
|
117
|
+
self._validate_trigger(trigger_info.trigger)
|
|
118
|
+
assert trigger_info.livetime is not None
|
|
128
119
|
|
|
129
120
|
# trigger mode must be set first and on its own!
|
|
130
121
|
await self._drv.trigger_mode.set(TetrammTrigger.ExtTrigger)
|
|
131
122
|
|
|
132
123
|
await asyncio.gather(
|
|
133
|
-
self._drv.averaging_time.set(
|
|
124
|
+
self._drv.averaging_time.set(trigger_info.livetime),
|
|
125
|
+
self.set_exposure(trigger_info.livetime),
|
|
134
126
|
)
|
|
135
127
|
|
|
136
|
-
|
|
128
|
+
async def arm(self):
|
|
129
|
+
self._arm_status = await set_and_wait_for_value(self._drv.acquire, True)
|
|
137
130
|
|
|
138
|
-
|
|
131
|
+
async def wait_for_idle(self):
|
|
132
|
+
if self._arm_status:
|
|
133
|
+
await self._arm_status
|
|
139
134
|
|
|
140
135
|
def _validate_trigger(self, trigger: DetectorTrigger) -> None:
|
|
141
136
|
supported_trigger_types = {
|
dodal/devices/undulator.py
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import numpy as np
|
|
4
|
+
from bluesky.protocols import Movable
|
|
5
|
+
from numpy import argmin, ndarray
|
|
6
|
+
from ophyd_async.core import (
|
|
7
|
+
AsyncStatus,
|
|
8
|
+
ConfigSignal,
|
|
9
|
+
StandardReadable,
|
|
10
|
+
soft_signal_r_and_setter,
|
|
11
|
+
)
|
|
4
12
|
from ophyd_async.epics.motor import Motor
|
|
5
13
|
from ophyd_async.epics.signal import epics_signal_r
|
|
6
14
|
|
|
15
|
+
from dodal.log import LOGGER
|
|
16
|
+
|
|
17
|
+
from .util.lookup_tables import energy_distance_table
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AccessError(Exception):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Enable to allow testing when the beamline is down, do not change in production!
|
|
25
|
+
TEST_MODE = False
|
|
26
|
+
# will be made more generic in https://github.com/DiamondLightSource/dodal/issues/754
|
|
27
|
+
|
|
28
|
+
|
|
7
29
|
# The acceptable difference, in mm, between the undulator gap and the DCM
|
|
8
30
|
# energy, when the latter is converted to mm using lookup tables
|
|
9
31
|
UNDULATOR_DISCREPANCY_THRESHOLD_MM = 2e-3
|
|
32
|
+
STATUS_TIMEOUT_S: float = 10.0
|
|
10
33
|
|
|
11
34
|
|
|
12
35
|
class UndulatorGapAccess(str, Enum):
|
|
@@ -14,7 +37,15 @@ class UndulatorGapAccess(str, Enum):
|
|
|
14
37
|
DISABLED = "DISABLED"
|
|
15
38
|
|
|
16
39
|
|
|
17
|
-
|
|
40
|
+
def _get_closest_gap_for_energy(
|
|
41
|
+
dcm_energy_ev: float, energy_to_distance_table: ndarray
|
|
42
|
+
) -> float:
|
|
43
|
+
table = energy_to_distance_table.transpose()
|
|
44
|
+
idx = argmin(np.abs(table[0] - dcm_energy_ev))
|
|
45
|
+
return table[1][idx]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Undulator(StandardReadable, Movable):
|
|
18
49
|
"""
|
|
19
50
|
An Undulator-type insertion device, used to control photon emission at a given
|
|
20
51
|
beam energy.
|
|
@@ -23,6 +54,7 @@ class Undulator(StandardReadable):
|
|
|
23
54
|
def __init__(
|
|
24
55
|
self,
|
|
25
56
|
prefix: str,
|
|
57
|
+
id_gap_lookup_table_path: str,
|
|
26
58
|
name: str = "",
|
|
27
59
|
poles: int | None = None,
|
|
28
60
|
length: float | None = None,
|
|
@@ -36,6 +68,7 @@ class Undulator(StandardReadable):
|
|
|
36
68
|
name (str, optional): Name for device. Defaults to "".
|
|
37
69
|
"""
|
|
38
70
|
|
|
71
|
+
self.id_gap_lookup_table_path = id_gap_lookup_table_path
|
|
39
72
|
with self.add_children_as_readables():
|
|
40
73
|
self.gap_motor = Motor(prefix + "BLGAPMTR")
|
|
41
74
|
self.current_gap = epics_signal_r(float, prefix + "CURRGAPD")
|
|
@@ -63,3 +96,59 @@ class Undulator(StandardReadable):
|
|
|
63
96
|
self.length = None
|
|
64
97
|
|
|
65
98
|
super().__init__(name)
|
|
99
|
+
|
|
100
|
+
@AsyncStatus.wrap
|
|
101
|
+
async def set(self, value: float):
|
|
102
|
+
"""
|
|
103
|
+
Set the undulator gap to a given energy in keV
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
value: energy in keV
|
|
107
|
+
"""
|
|
108
|
+
await self._set_undulator_gap(value)
|
|
109
|
+
|
|
110
|
+
async def _set_undulator_gap(self, energy_kev: float) -> None:
|
|
111
|
+
access_level = await self.gap_access.get_value()
|
|
112
|
+
if access_level is UndulatorGapAccess.DISABLED and not TEST_MODE:
|
|
113
|
+
raise AccessError("Undulator gap access is disabled. Contact Control Room")
|
|
114
|
+
LOGGER.info(f"Setting undulator gap to {energy_kev:.2f} kev")
|
|
115
|
+
target_gap = await self._get_gap_to_match_energy(energy_kev)
|
|
116
|
+
|
|
117
|
+
# Check if undulator gap is close enough to the value from the DCM
|
|
118
|
+
current_gap = await self.current_gap.get_value()
|
|
119
|
+
tolerance = await self.gap_discrepancy_tolerance_mm.get_value()
|
|
120
|
+
difference = abs(target_gap - current_gap)
|
|
121
|
+
if difference > tolerance:
|
|
122
|
+
LOGGER.info(
|
|
123
|
+
f"Undulator gap mismatch. {difference:.3f}mm is outside tolerance.\
|
|
124
|
+
Moving gap to nominal value, {target_gap:.3f}mm"
|
|
125
|
+
)
|
|
126
|
+
if not TEST_MODE:
|
|
127
|
+
# Only move if the gap is sufficiently different to the value from the
|
|
128
|
+
# DCM lookup table AND we're not in TEST_MODE
|
|
129
|
+
await self.gap_motor.set(
|
|
130
|
+
target_gap,
|
|
131
|
+
timeout=STATUS_TIMEOUT_S,
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
LOGGER.debug("In test mode, not moving ID gap")
|
|
135
|
+
else:
|
|
136
|
+
LOGGER.debug(
|
|
137
|
+
"Gap is already in the correct place for the new energy value "
|
|
138
|
+
f"{energy_kev}, no need to ask it to move"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
async def _get_gap_to_match_energy(self, energy_kev: float) -> float:
|
|
142
|
+
"""
|
|
143
|
+
get a 2d np.array from lookup table that
|
|
144
|
+
converts energies to undulator gap distance
|
|
145
|
+
"""
|
|
146
|
+
energy_to_distance_table: np.ndarray = await energy_distance_table(
|
|
147
|
+
self.id_gap_lookup_table_path
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Use the lookup table to get the undulator gap associated with this dcm energy
|
|
151
|
+
return _get_closest_gap_for_energy(
|
|
152
|
+
energy_kev * 1000,
|
|
153
|
+
energy_to_distance_table,
|
|
154
|
+
)
|
dodal/devices/undulator_dcm.py
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
|
|
3
|
-
import numpy as np
|
|
4
3
|
from bluesky.protocols import Movable
|
|
5
|
-
from numpy import argmin, ndarray
|
|
6
4
|
from ophyd_async.core import AsyncStatus, StandardReadable
|
|
7
5
|
|
|
8
6
|
from dodal.common.beamlines.beamline_parameters import get_beamline_parameters
|
|
9
|
-
from dodal.log import LOGGER
|
|
10
7
|
|
|
11
8
|
from .dcm import DCM
|
|
12
9
|
from .undulator import Undulator, UndulatorGapAccess
|
|
13
|
-
from .util.lookup_tables import energy_distance_table
|
|
14
10
|
|
|
15
11
|
ENERGY_TIMEOUT_S: float = 30.0
|
|
16
|
-
STATUS_TIMEOUT_S: float = 10.0
|
|
17
12
|
|
|
18
13
|
# Enable to allow testing when the beamline is down, do not change in production!
|
|
19
14
|
TEST_MODE = False
|
|
@@ -23,14 +18,6 @@ class AccessError(Exception):
|
|
|
23
18
|
pass
|
|
24
19
|
|
|
25
20
|
|
|
26
|
-
def _get_closest_gap_for_energy(
|
|
27
|
-
dcm_energy_ev: float, energy_to_distance_table: ndarray
|
|
28
|
-
) -> float:
|
|
29
|
-
table = energy_to_distance_table.transpose()
|
|
30
|
-
idx = argmin(np.abs(table[0] - dcm_energy_ev))
|
|
31
|
-
return table[1][idx]
|
|
32
|
-
|
|
33
|
-
|
|
34
21
|
class UndulatorDCM(StandardReadable, Movable):
|
|
35
22
|
"""
|
|
36
23
|
Composite device to handle changing beamline energies, wraps the Undulator and the
|
|
@@ -48,7 +35,6 @@ class UndulatorDCM(StandardReadable, Movable):
|
|
|
48
35
|
self,
|
|
49
36
|
undulator: Undulator,
|
|
50
37
|
dcm: DCM,
|
|
51
|
-
id_gap_lookup_table_path: str,
|
|
52
38
|
daq_configuration_path: str,
|
|
53
39
|
prefix: str = "",
|
|
54
40
|
name: str = "",
|
|
@@ -61,11 +47,10 @@ class UndulatorDCM(StandardReadable, Movable):
|
|
|
61
47
|
self.dcm = dcm
|
|
62
48
|
|
|
63
49
|
# These attributes are just used by hyperion for lookup purposes
|
|
64
|
-
self.
|
|
65
|
-
self.dcm_pitch_converter_lookup_table_path = (
|
|
50
|
+
self.pitch_energy_table_path = (
|
|
66
51
|
daq_configuration_path + "/lookup/BeamLineEnergy_DCM_Pitch_converter.txt"
|
|
67
52
|
)
|
|
68
|
-
self.
|
|
53
|
+
self.roll_energy_table_path = (
|
|
69
54
|
daq_configuration_path + "/lookup/BeamLineEnergy_DCM_Roll_converter.txt"
|
|
70
55
|
)
|
|
71
56
|
# I03 configures the DCM Perp as a side effect of applying this fixed value to the DCM Offset after an energy change
|
|
@@ -78,7 +63,7 @@ class UndulatorDCM(StandardReadable, Movable):
|
|
|
78
63
|
async def set(self, value: float):
|
|
79
64
|
await asyncio.gather(
|
|
80
65
|
self._set_dcm_energy(value),
|
|
81
|
-
self.
|
|
66
|
+
self.undulator.set(value),
|
|
82
67
|
)
|
|
83
68
|
|
|
84
69
|
async def _set_dcm_energy(self, energy_kev: float) -> None:
|
|
@@ -90,42 +75,3 @@ class UndulatorDCM(StandardReadable, Movable):
|
|
|
90
75
|
energy_kev,
|
|
91
76
|
timeout=ENERGY_TIMEOUT_S,
|
|
92
77
|
)
|
|
93
|
-
|
|
94
|
-
async def _set_undulator_gap_if_required(self, energy_kev: float) -> None:
|
|
95
|
-
LOGGER.info(f"Setting DCM energy to {energy_kev:.2f} kev")
|
|
96
|
-
gap_to_match_dcm_energy = await self._gap_to_match_dcm_energy(energy_kev)
|
|
97
|
-
|
|
98
|
-
# Check if undulator gap is close enough to the value from the DCM
|
|
99
|
-
current_gap = await self.undulator.current_gap.get_value()
|
|
100
|
-
tolerance = await self.undulator.gap_discrepancy_tolerance_mm.get_value()
|
|
101
|
-
if abs(gap_to_match_dcm_energy - current_gap) > tolerance:
|
|
102
|
-
LOGGER.info(
|
|
103
|
-
f"Undulator gap mismatch. {abs(gap_to_match_dcm_energy-current_gap):.3f}mm is outside tolerance.\
|
|
104
|
-
Moving gap to nominal value, {gap_to_match_dcm_energy:.3f}mm"
|
|
105
|
-
)
|
|
106
|
-
if not TEST_MODE:
|
|
107
|
-
# Only move if the gap is sufficiently different to the value from the
|
|
108
|
-
# DCM lookup table AND we're not in TEST_MODE
|
|
109
|
-
await self.undulator.gap_motor.set(
|
|
110
|
-
gap_to_match_dcm_energy,
|
|
111
|
-
timeout=STATUS_TIMEOUT_S,
|
|
112
|
-
)
|
|
113
|
-
else:
|
|
114
|
-
LOGGER.debug("In test mode, not moving ID gap")
|
|
115
|
-
else:
|
|
116
|
-
LOGGER.debug(
|
|
117
|
-
"Gap is already in the correct place for the new energy value "
|
|
118
|
-
f"{energy_kev}, no need to ask it to move"
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
async def _gap_to_match_dcm_energy(self, energy_kev: float) -> float:
|
|
122
|
-
# Get 2d np.array converting energies to undulator gap distance, from lookup table
|
|
123
|
-
energy_to_distance_table = await energy_distance_table(
|
|
124
|
-
self.id_gap_lookup_table_path
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
# Use the lookup table to get the undulator gap associated with this dcm energy
|
|
128
|
-
return _get_closest_gap_for_energy(
|
|
129
|
-
energy_kev * 1000,
|
|
130
|
-
energy_to_distance_table,
|
|
131
|
-
)
|
dodal/devices/webcam.py
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
|
+
from collections.abc import ByteString
|
|
2
|
+
from io import BytesIO
|
|
1
3
|
from pathlib import Path
|
|
2
4
|
|
|
3
5
|
import aiofiles
|
|
4
6
|
from aiohttp import ClientSession
|
|
5
7
|
from bluesky.protocols import Triggerable
|
|
6
8
|
from ophyd_async.core import AsyncStatus, HintedSignal, StandardReadable, soft_signal_rw
|
|
9
|
+
from PIL import Image
|
|
7
10
|
|
|
8
11
|
from dodal.log import LOGGER
|
|
9
12
|
|
|
13
|
+
PLACEHOLDER_IMAGE_SIZE = (1024, 768)
|
|
14
|
+
IMAGE_FORMAT = "png"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_placeholder_image() -> ByteString:
|
|
18
|
+
image = Image.new("RGB", PLACEHOLDER_IMAGE_SIZE)
|
|
19
|
+
image.save(buffer := BytesIO(), format=IMAGE_FORMAT)
|
|
20
|
+
return buffer.getbuffer()
|
|
21
|
+
|
|
10
22
|
|
|
11
23
|
class Webcam(StandardReadable, Triggerable):
|
|
12
24
|
def __init__(self, name, prefix, url):
|
|
@@ -18,19 +30,33 @@ class Webcam(StandardReadable, Triggerable):
|
|
|
18
30
|
self.add_readables([self.last_saved_path], wrapper=HintedSignal)
|
|
19
31
|
super().__init__(name=name)
|
|
20
32
|
|
|
21
|
-
async def _write_image(self, file_path: str):
|
|
33
|
+
async def _write_image(self, file_path: str, image: ByteString):
|
|
34
|
+
async with aiofiles.open(file_path, "wb") as file:
|
|
35
|
+
await file.write(image)
|
|
36
|
+
|
|
37
|
+
async def _get_and_write_image(self, file_path: str):
|
|
22
38
|
async with ClientSession() as session:
|
|
23
39
|
async with session.get(self.url) as response:
|
|
24
|
-
response.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
if not response.ok:
|
|
41
|
+
LOGGER.warning(
|
|
42
|
+
f"Webcam responded with {response.status}: {response.reason}. Attempting to read anyway."
|
|
43
|
+
)
|
|
44
|
+
try:
|
|
45
|
+
data = await response.read()
|
|
46
|
+
LOGGER.info(f"Saving webcam image from {self.url} to {file_path}")
|
|
47
|
+
except Exception as e:
|
|
48
|
+
LOGGER.warning(
|
|
49
|
+
f"Failed to read data from {self.url} ({e}). Using placeholder image."
|
|
50
|
+
)
|
|
51
|
+
data = create_placeholder_image()
|
|
52
|
+
|
|
53
|
+
await self._write_image(file_path, data)
|
|
28
54
|
|
|
29
55
|
@AsyncStatus.wrap
|
|
30
56
|
async def trigger(self) -> None:
|
|
31
57
|
filename = await self.filename.get_value()
|
|
32
58
|
directory = await self.directory.get_value()
|
|
33
59
|
|
|
34
|
-
file_path = Path(f"{directory}/{filename}.
|
|
35
|
-
await self.
|
|
60
|
+
file_path = Path(f"{directory}/{filename}.{IMAGE_FORMAT}").as_posix()
|
|
61
|
+
await self._get_and_write_image(file_path)
|
|
36
62
|
await self.last_saved_path.set(file_path)
|
|
@@ -39,7 +39,12 @@ class ZocaloStartInfo:
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def _get_zocalo_headers() -> tuple[str, str]:
|
|
42
|
-
user = os.environ.get("ZOCALO_GO_USER"
|
|
42
|
+
user = os.environ.get("ZOCALO_GO_USER")
|
|
43
|
+
|
|
44
|
+
# cannot default as getuser() will throw when called from inside a container
|
|
45
|
+
if not user:
|
|
46
|
+
user = getpass.getuser()
|
|
47
|
+
|
|
43
48
|
hostname = os.environ.get("ZOCALO_GO_HOSTNAME", socket.gethostname())
|
|
44
49
|
return user, hostname
|
|
45
50
|
|
|
@@ -10,6 +10,7 @@ import numpy as np
|
|
|
10
10
|
import workflows.recipe
|
|
11
11
|
import workflows.transport
|
|
12
12
|
from bluesky.protocols import Descriptor, Triggerable
|
|
13
|
+
from deepdiff import DeepDiff
|
|
13
14
|
from numpy.typing import NDArray
|
|
14
15
|
from ophyd_async.core import (
|
|
15
16
|
AsyncStatus,
|
|
@@ -37,6 +38,11 @@ class SortKeys(str, Enum):
|
|
|
37
38
|
n_voxels = "n_voxels"
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
class ZocaloSource(str, Enum):
|
|
42
|
+
CPU = "CPU"
|
|
43
|
+
GPU = "GPU"
|
|
44
|
+
|
|
45
|
+
|
|
40
46
|
DEFAULT_TIMEOUT = 180
|
|
41
47
|
DEFAULT_SORT_KEY = SortKeys.max_count
|
|
42
48
|
ZOCALO_READING_PLAN_NAME = "zocalo reading"
|
|
@@ -60,12 +66,50 @@ def bbox_size(result: XrcResult):
|
|
|
60
66
|
]
|
|
61
67
|
|
|
62
68
|
|
|
69
|
+
def get_dict_differences(
|
|
70
|
+
dict1: dict, dict1_source: str, dict2: dict, dict2_source: str
|
|
71
|
+
) -> str | None:
|
|
72
|
+
"""Returns a string containing dict1 and dict2 if there are differences between them, greater than a
|
|
73
|
+
1e-5 tolerance. If dictionaries are identical, return None"""
|
|
74
|
+
|
|
75
|
+
diff = DeepDiff(dict1, dict2, math_epsilon=1e-5, ignore_numeric_type_changes=True)
|
|
76
|
+
|
|
77
|
+
if diff:
|
|
78
|
+
return f"Zocalo results from {dict1_source} and {dict2_source} are not identical.\n Results from {dict1_source}: {dict1}\n Results from {dict2_source}: {dict2}"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def source_from_results(results):
|
|
82
|
+
return (
|
|
83
|
+
ZocaloSource.GPU.value
|
|
84
|
+
if results["recipe_parameters"].get("gpu")
|
|
85
|
+
else ZocaloSource.CPU.value
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
63
89
|
class ZocaloResults(StandardReadable, Triggerable):
|
|
64
90
|
"""An ophyd device which can wait for results from a Zocalo job. These jobs should
|
|
65
91
|
be triggered from a plan-subscribed callback using the run_start() and run_end()
|
|
66
92
|
methods on dodal.devices.zocalo.ZocaloTrigger.
|
|
67
93
|
|
|
68
|
-
See https://github.
|
|
94
|
+
See https://diamondlightsource.github.io/dodal/main/how-to/zocalo.html
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
name (str): Name of the device
|
|
98
|
+
|
|
99
|
+
zocalo_environment (str): How zocalo is configured. Defaults to i03's development configuration
|
|
100
|
+
|
|
101
|
+
channel (str): Name for the results Queue
|
|
102
|
+
|
|
103
|
+
sort_key (str): How results are ranked. Defaults to sorting by highest counts
|
|
104
|
+
|
|
105
|
+
timeout_s (float): Maximum time to wait for the Queue to be filled by an object, starting
|
|
106
|
+
from when the ZocaloResults device is triggered
|
|
107
|
+
|
|
108
|
+
prefix (str): EPICS PV prefix for the device
|
|
109
|
+
|
|
110
|
+
use_cpu_and_gpu (bool): When True, ZocaloResults will wait for results from the CPU and the GPU, compare them, and provide a warning if the results differ. When False, ZocaloResults will only use results from the CPU
|
|
111
|
+
|
|
112
|
+
"""
|
|
69
113
|
|
|
70
114
|
def __init__(
|
|
71
115
|
self,
|
|
@@ -75,6 +119,7 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
75
119
|
sort_key: str = DEFAULT_SORT_KEY.value,
|
|
76
120
|
timeout_s: float = DEFAULT_TIMEOUT,
|
|
77
121
|
prefix: str = "",
|
|
122
|
+
use_cpu_and_gpu: bool = False,
|
|
78
123
|
) -> None:
|
|
79
124
|
self.zocalo_environment = zocalo_environment
|
|
80
125
|
self.sort_key = SortKeys[sort_key]
|
|
@@ -83,6 +128,7 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
83
128
|
self._prefix = prefix
|
|
84
129
|
self._raw_results_received: Queue = Queue()
|
|
85
130
|
self.transport: CommonTransport | None = None
|
|
131
|
+
self.use_cpu_and_gpu = use_cpu_and_gpu
|
|
86
132
|
|
|
87
133
|
self.results, self._results_setter = soft_signal_r_and_setter(
|
|
88
134
|
list[XrcResult], name="results"
|
|
@@ -111,14 +157,14 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
111
157
|
)
|
|
112
158
|
super().__init__(name)
|
|
113
159
|
|
|
114
|
-
async def _put_results(self, results: Sequence[XrcResult],
|
|
160
|
+
async def _put_results(self, results: Sequence[XrcResult], recipe_parameters):
|
|
115
161
|
self._results_setter(list(results))
|
|
116
162
|
centres_of_mass = np.array([r["centre_of_mass"] for r in results])
|
|
117
163
|
bbox_sizes = np.array([bbox_size(r) for r in results])
|
|
118
164
|
self._com_setter(centres_of_mass)
|
|
119
165
|
self._bbox_setter(bbox_sizes)
|
|
120
|
-
self._ispyb_dcid_setter(
|
|
121
|
-
self._ispyb_dcgid_setter(
|
|
166
|
+
self._ispyb_dcid_setter(recipe_parameters["dcid"])
|
|
167
|
+
self._ispyb_dcgid_setter(recipe_parameters["dcgid"])
|
|
122
168
|
|
|
123
169
|
def _clear_old_results(self):
|
|
124
170
|
LOGGER.info("Clearing queue")
|
|
@@ -127,7 +173,7 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
127
173
|
@AsyncStatus.wrap
|
|
128
174
|
async def stage(self):
|
|
129
175
|
"""Stages the Zocalo device by: subscribing to the queue, doing a background
|
|
130
|
-
sleep for a few seconds to wait for any stale messages to be
|
|
176
|
+
sleep for a few seconds to wait for any stale messages to be received, then
|
|
131
177
|
clearing the queue. Plans using this device should wait on ZOCALO_STAGE_GROUP
|
|
132
178
|
before triggering processing for the experiment"""
|
|
133
179
|
|
|
@@ -169,7 +215,57 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
169
215
|
)
|
|
170
216
|
|
|
171
217
|
raw_results = self._raw_results_received.get(timeout=self.timeout_s)
|
|
172
|
-
|
|
218
|
+
source_of_first_results = source_from_results(raw_results)
|
|
219
|
+
|
|
220
|
+
# Wait for results from CPU and GPU, warn and continue if only GPU times out. Error if CPU times out
|
|
221
|
+
if self.use_cpu_and_gpu:
|
|
222
|
+
if source_of_first_results == ZocaloSource.CPU:
|
|
223
|
+
LOGGER.warning("Received zocalo results from CPU before GPU")
|
|
224
|
+
raw_results_two_sources = [raw_results]
|
|
225
|
+
try:
|
|
226
|
+
raw_results_two_sources.append(
|
|
227
|
+
self._raw_results_received.get(timeout=self.timeout_s / 2)
|
|
228
|
+
)
|
|
229
|
+
source_of_second_results = source_from_results(
|
|
230
|
+
raw_results_two_sources[1]
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Compare results from both sources and warn if they aren't the same
|
|
234
|
+
differences_str = get_dict_differences(
|
|
235
|
+
raw_results_two_sources[0]["results"][0],
|
|
236
|
+
source_of_first_results,
|
|
237
|
+
raw_results_two_sources[1]["results"][0],
|
|
238
|
+
source_of_second_results,
|
|
239
|
+
)
|
|
240
|
+
if differences_str:
|
|
241
|
+
LOGGER.warning(differences_str)
|
|
242
|
+
|
|
243
|
+
# Always use CPU results
|
|
244
|
+
raw_results = (
|
|
245
|
+
raw_results_two_sources[0]
|
|
246
|
+
if source_of_first_results == ZocaloSource.CPU
|
|
247
|
+
else raw_results_two_sources[1]
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
except Empty as err:
|
|
251
|
+
source_of_missing_results = (
|
|
252
|
+
ZocaloSource.CPU.value
|
|
253
|
+
if source_of_first_results == ZocaloSource.GPU.value
|
|
254
|
+
else ZocaloSource.GPU.value
|
|
255
|
+
)
|
|
256
|
+
if source_of_missing_results == ZocaloSource.GPU.value:
|
|
257
|
+
LOGGER.warning(
|
|
258
|
+
f"Zocalo results from {source_of_missing_results} timed out. Using results from {source_of_first_results}"
|
|
259
|
+
)
|
|
260
|
+
else:
|
|
261
|
+
LOGGER.error(
|
|
262
|
+
f"Zocalo results from {source_of_missing_results} timed out and GPU results not yet reliable"
|
|
263
|
+
)
|
|
264
|
+
raise err
|
|
265
|
+
|
|
266
|
+
LOGGER.info(
|
|
267
|
+
f"Zocalo results from {ZocaloSource.CPU.value} processing: found {len(raw_results['results'])} crystals."
|
|
268
|
+
)
|
|
173
269
|
# Sort from strongest to weakest in case of multiple crystals
|
|
174
270
|
await self._put_results(
|
|
175
271
|
sorted(
|
|
@@ -177,7 +273,7 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
177
273
|
key=lambda d: d[self.sort_key.value],
|
|
178
274
|
reverse=True,
|
|
179
275
|
),
|
|
180
|
-
raw_results["
|
|
276
|
+
raw_results["recipe_parameters"],
|
|
181
277
|
)
|
|
182
278
|
except Empty as timeout_exception:
|
|
183
279
|
LOGGER.warning("Timed out waiting for zocalo results!")
|
|
@@ -241,9 +337,17 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
241
337
|
self.transport.ack(header) # type: ignore # we create transport here
|
|
242
338
|
|
|
243
339
|
results = message.get("results", [])
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
340
|
+
|
|
341
|
+
if self.use_cpu_and_gpu:
|
|
342
|
+
self._raw_results_received.put(
|
|
343
|
+
{"results": results, "recipe_parameters": recipe_parameters}
|
|
344
|
+
)
|
|
345
|
+
else:
|
|
346
|
+
# Only add to queue if results are from CPU
|
|
347
|
+
if not recipe_parameters.get("gpu"):
|
|
348
|
+
self._raw_results_received.put(
|
|
349
|
+
{"results": results, "recipe_parameters": recipe_parameters}
|
|
350
|
+
)
|
|
247
351
|
|
|
248
352
|
subscription = workflows.recipe.wrap_subscribe(
|
|
249
353
|
self.transport,
|
dodal/utils.py
CHANGED
|
@@ -13,6 +13,7 @@ from os import environ
|
|
|
13
13
|
from types import ModuleType
|
|
14
14
|
from typing import (
|
|
15
15
|
Any,
|
|
16
|
+
TypeGuard,
|
|
16
17
|
TypeVar,
|
|
17
18
|
)
|
|
18
19
|
|
|
@@ -259,7 +260,7 @@ def _is_device_skipped(func: AnyDeviceFactory) -> bool:
|
|
|
259
260
|
return getattr(func, "__skip__", False)
|
|
260
261
|
|
|
261
262
|
|
|
262
|
-
def is_v1_device_factory(func: Callable) ->
|
|
263
|
+
def is_v1_device_factory(func: Callable) -> TypeGuard[V1DeviceFactory]:
|
|
263
264
|
try:
|
|
264
265
|
return_type = signature(func).return_annotation
|
|
265
266
|
return is_v1_device_type(return_type)
|
|
@@ -267,7 +268,7 @@ def is_v1_device_factory(func: Callable) -> bool:
|
|
|
267
268
|
return False
|
|
268
269
|
|
|
269
270
|
|
|
270
|
-
def is_v2_device_factory(func: Callable) ->
|
|
271
|
+
def is_v2_device_factory(func: Callable) -> TypeGuard[V2DeviceFactory]:
|
|
271
272
|
try:
|
|
272
273
|
return_type = signature(func).return_annotation
|
|
273
274
|
return is_v2_device_type(return_type)
|
|
@@ -275,7 +276,7 @@ def is_v2_device_factory(func: Callable) -> bool:
|
|
|
275
276
|
return False
|
|
276
277
|
|
|
277
278
|
|
|
278
|
-
def is_any_device_factory(func: Callable) ->
|
|
279
|
+
def is_any_device_factory(func: Callable) -> TypeGuard[AnyDeviceFactory]:
|
|
279
280
|
return is_v1_device_factory(func) or is_v2_device_factory(func)
|
|
280
281
|
|
|
281
282
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|