mx-bluesky 1.4.6__py3-none-any.whl → 1.4.8__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.
- mx_bluesky/_version.py +2 -2
- mx_bluesky/beamlines/aithre_lasershaping/__init__.py +13 -0
- mx_bluesky/beamlines/aithre_lasershaping/check_goniometer_performance.py +29 -0
- mx_bluesky/beamlines/aithre_lasershaping/goniometer_controls.py +18 -0
- mx_bluesky/beamlines/i04/redis_to_murko_forwarder.py +35 -29
- mx_bluesky/beamlines/i04/thawing_plan.py +18 -3
- mx_bluesky/beamlines/i23/__init__.py +3 -0
- mx_bluesky/beamlines/i23/serial.py +71 -0
- mx_bluesky/beamlines/i24/serial/__init__.py +2 -0
- mx_bluesky/beamlines/i24/serial/extruder/EX-gui-edm/DiamondExtruder-I24-py3v1.edl +12 -12
- mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +36 -30
- mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/pumpprobe-py3v1.edl +3 -3
- mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +15 -66
- mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +8 -10
- mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +10 -3
- mx_bluesky/beamlines/i24/serial/log.py +9 -9
- mx_bluesky/beamlines/i24/serial/parameters/utils.py +36 -7
- mx_bluesky/beamlines/i24/serial/set_visit_directory.sh +1 -1
- mx_bluesky/beamlines/i24/serial/setup_beamline/pv.py +16 -17
- mx_bluesky/beamlines/i24/serial/setup_beamline/pv_abstract.py +4 -4
- mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +51 -52
- mx_bluesky/beamlines/i24/serial/setup_beamline/setup_detector.py +3 -2
- mx_bluesky/beamlines/i24/serial/setup_beamline/setup_zebra_plans.py +9 -7
- mx_bluesky/beamlines/i24/serial/web_gui_plans/general_plans.py +71 -11
- mx_bluesky/beamlines/i24/serial/write_nexus.py +6 -5
- mx_bluesky/{hyperion → common}/device_setup_plans/check_beamstop.py +1 -1
- mx_bluesky/{hyperion → common}/device_setup_plans/manipulate_sample.py +1 -1
- mx_bluesky/{hyperion → common}/device_setup_plans/setup_oav.py +12 -6
- mx_bluesky/common/device_setup_plans/xbpm_feedback.py +45 -0
- mx_bluesky/{hyperion → common}/experiment_plans/change_aperture_then_move_plan.py +13 -29
- mx_bluesky/{hyperion → common}/experiment_plans/oav_grid_detection_plan.py +6 -6
- mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py +8 -9
- mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py +1 -1
- mx_bluesky/common/external_interaction/callbacks/common/zocalo_callback.py +18 -15
- mx_bluesky/{hyperion → common}/external_interaction/callbacks/sample_handling/sample_handling_callback.py +16 -4
- mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py +50 -45
- mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py +2 -1
- mx_bluesky/common/external_interaction/ispyb/data_model.py +1 -0
- mx_bluesky/common/external_interaction/ispyb/ispyb_store.py +18 -2
- mx_bluesky/common/external_interaction/ispyb/ispyb_utils.py +4 -4
- mx_bluesky/common/external_interaction/nexus/nexus_utils.py +1 -1
- mx_bluesky/common/parameters/components.py +22 -2
- mx_bluesky/common/parameters/constants.py +6 -16
- mx_bluesky/common/parameters/gridscan.py +36 -32
- mx_bluesky/common/plans/common_flyscan_xray_centre_plan.py +316 -0
- mx_bluesky/common/plans/inner_plans/__init__ .py +0 -0
- mx_bluesky/common/plans/read_hardware.py +3 -3
- mx_bluesky/common/plans/write_sample_status.py +46 -0
- mx_bluesky/common/preprocessors/__init__.py +0 -0
- mx_bluesky/common/preprocessors/preprocessors.py +105 -0
- mx_bluesky/common/protocols/__init__.py +0 -0
- mx_bluesky/common/protocols/protocols.py +10 -0
- mx_bluesky/common/utils/log.py +15 -12
- mx_bluesky/hyperion/__main__.py +5 -24
- mx_bluesky/hyperion/baton_handler.py +84 -0
- mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +4 -4
- mx_bluesky/hyperion/device_setup_plans/setup_panda.py +5 -1
- mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +0 -33
- mx_bluesky/hyperion/device_setup_plans/utils.py +4 -4
- mx_bluesky/hyperion/experiment_plans/__init__.py +0 -10
- mx_bluesky/hyperion/experiment_plans/experiment_registry.py +0 -16
- mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +71 -88
- mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +183 -0
- mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +17 -8
- mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +29 -8
- mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +4 -4
- mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +6 -4
- mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +11 -3
- mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +9 -34
- mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +35 -68
- mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +27 -8
- mx_bluesky/hyperion/external_interaction/agamemnon.py +140 -10
- mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +17 -9
- mx_bluesky/hyperion/external_interaction/callbacks/snapshot_callback.py +259 -0
- mx_bluesky/hyperion/parameters/cli.py +2 -10
- mx_bluesky/hyperion/parameters/constants.py +0 -5
- mx_bluesky/hyperion/parameters/device_composites.py +40 -5
- mx_bluesky/hyperion/parameters/gridscan.py +9 -58
- mx_bluesky/hyperion/parameters/rotation.py +1 -5
- mx_bluesky/hyperion/utils/context.py +2 -5
- mx_bluesky/hyperion/utils/validation.py +13 -10
- {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.8.dist-info}/METADATA +10 -9
- {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.8.dist-info}/RECORD +92 -79
- {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.8.dist-info}/WHEEL +1 -1
- mx_bluesky/common/external_interaction/callbacks/common/aperture_change_callback.py +0 -22
- mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py +0 -103
- mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +0 -466
- /mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/{short1-laser.png → s1l.png} +0 -0
- /mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/{short2-laser.png → s2l.png} +0 -0
- /mx_bluesky/{hyperion → common}/device_setup_plans/position_detector.py +0 -0
- /mx_bluesky/{hyperion → common}/external_interaction/callbacks/sample_handling/__init__.py +0 -0
- /mx_bluesky/common/plans/{do_fgs.py → inner_plans/do_fgs.py} +0 -0
- {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.8.dist-info}/entry_points.txt +0 -0
- {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.8.dist-info/licenses}/LICENSE +0 -0
- {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from bluesky import plan_stubs as bps
|
|
2
|
+
from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator
|
|
3
|
+
from dodal.devices.xbpm_feedback import Pause, XBPMFeedback
|
|
4
|
+
|
|
5
|
+
from mx_bluesky.common.utils.log import LOGGER
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def unpause_xbpm_feedback_and_set_transmission_to_1(
|
|
9
|
+
xbpm_feedback: XBPMFeedback, attenuator: BinaryFilterAttenuator
|
|
10
|
+
):
|
|
11
|
+
"""Turns the XBPM feedback back on and sets transmission to 1 so that it keeps the
|
|
12
|
+
beam aligned whilst not collecting.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
xbpm_feedback (XBPMFeedback): The XBPM device that is responsible for keeping
|
|
16
|
+
the beam in position
|
|
17
|
+
attenuator (BinaryFilterAttenuator): The attenuator used to set transmission
|
|
18
|
+
"""
|
|
19
|
+
yield from bps.mv(xbpm_feedback.pause_feedback, Pause.RUN, attenuator, 1.0) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check_and_pause_feedback(
|
|
23
|
+
xbpm_feedback: XBPMFeedback,
|
|
24
|
+
attenuator: BinaryFilterAttenuator,
|
|
25
|
+
desired_transmission_fraction: float,
|
|
26
|
+
):
|
|
27
|
+
"""Checks that the xbpm is in position before then turning it off and setting a new
|
|
28
|
+
transmission.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
xbpm_feedback (XBPMFeedback): The XBPM device that is responsible for keeping
|
|
32
|
+
the beam in position
|
|
33
|
+
attenuator (BinaryFilterAttenuator): The attenuator used to set transmission
|
|
34
|
+
desired_transmission_fraction (float): The desired transmission to set after
|
|
35
|
+
turning XBPM feedback off.
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
yield from bps.mv(attenuator, 1.0) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
|
|
39
|
+
LOGGER.info("Waiting for XBPM feedback to be stable")
|
|
40
|
+
yield from bps.trigger(xbpm_feedback, wait=True)
|
|
41
|
+
LOGGER.info(
|
|
42
|
+
f"XPBM feedback in position, pausing and setting transmission to {desired_transmission_fraction}"
|
|
43
|
+
)
|
|
44
|
+
yield from bps.mv(xbpm_feedback.pause_feedback, Pause.PAUSE) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
|
|
45
|
+
yield from bps.mv(attenuator, desired_transmission_fraction) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
|
|
@@ -1,37 +1,31 @@
|
|
|
1
1
|
import bluesky.plan_stubs as bps
|
|
2
|
-
import bluesky.preprocessors as bpp
|
|
3
2
|
import numpy
|
|
4
3
|
from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue
|
|
5
4
|
from dodal.devices.smargon import Smargon, StubPosition
|
|
6
5
|
|
|
6
|
+
from mx_bluesky.common.device_setup_plans.manipulate_sample import move_x_y_z
|
|
7
7
|
from mx_bluesky.common.utils.log import LOGGER
|
|
8
8
|
from mx_bluesky.common.utils.tracing import TRACER
|
|
9
9
|
from mx_bluesky.common.xrc_result import XRayCentreResult
|
|
10
|
-
from mx_bluesky.hyperion.device_setup_plans.manipulate_sample import move_x_y_z
|
|
11
|
-
from mx_bluesky.hyperion.parameters.gridscan import HyperionSpecifiedThreeDGridScan
|
|
12
10
|
|
|
13
11
|
|
|
14
12
|
def change_aperture_then_move_to_xtal(
|
|
15
13
|
best_hit: XRayCentreResult,
|
|
16
14
|
smargon: Smargon,
|
|
17
15
|
aperture_scatterguard: ApertureScatterguard,
|
|
18
|
-
|
|
16
|
+
set_stub_offsets: bool | None = None,
|
|
19
17
|
):
|
|
20
18
|
"""For the given x-ray centring result,
|
|
21
19
|
* Change the aperture so that the beam size is comparable to the crystal size
|
|
22
20
|
* Centre on the centre-of-mass
|
|
23
21
|
* Reset the stub offsets if specified by params"""
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
bounding_box_size,
|
|
32
|
-
)
|
|
33
|
-
else:
|
|
34
|
-
LOGGER.warning("No bounding box size received")
|
|
22
|
+
bounding_box_size = numpy.abs(
|
|
23
|
+
best_hit.bounding_box_mm[1] - best_hit.bounding_box_mm[0]
|
|
24
|
+
)
|
|
25
|
+
yield from set_aperture_for_bbox_mm(
|
|
26
|
+
aperture_scatterguard,
|
|
27
|
+
bounding_box_size,
|
|
28
|
+
)
|
|
35
29
|
|
|
36
30
|
# once we have the results, go to the appropriate position
|
|
37
31
|
LOGGER.info("Moving to centre of mass.")
|
|
@@ -41,7 +35,7 @@ def change_aperture_then_move_to_xtal(
|
|
|
41
35
|
|
|
42
36
|
# TODO support for setting stub offsets in multipin
|
|
43
37
|
# https://github.com/DiamondLightSource/mx-bluesky/issues/552
|
|
44
|
-
if
|
|
38
|
+
if set_stub_offsets:
|
|
45
39
|
LOGGER.info("Recentring smargon co-ordinate system to this point.")
|
|
46
40
|
yield from bps.mv(smargon.stub_offsets, StubPosition.CURRENT_AS_CENTER)
|
|
47
41
|
|
|
@@ -52,12 +46,12 @@ def set_aperture_for_bbox_mm(
|
|
|
52
46
|
):
|
|
53
47
|
"""Sets aperture size based on bbox_size.
|
|
54
48
|
|
|
55
|
-
This function determines the aperture size needed to
|
|
49
|
+
This function determines the aperture size needed to accommodate the bounding box
|
|
56
50
|
of a crystal. The x-axis length of the bounding box is used, setting the aperture
|
|
57
51
|
to Medium if this is less than 50um, and Large otherwise.
|
|
58
52
|
|
|
59
53
|
Args:
|
|
60
|
-
aperture_device: The aperture scatter
|
|
54
|
+
aperture_device: The aperture scatter guard device we are controlling.
|
|
61
55
|
bbox_size_mm: The [x,y,z] lengths, in mm, of a bounding box
|
|
62
56
|
containing a crystal. This describes (in no particular order):
|
|
63
57
|
* The maximum width a crystal occupies
|
|
@@ -77,14 +71,4 @@ def set_aperture_for_bbox_mm(
|
|
|
77
71
|
f"Setting aperture to {new_selected_aperture} based on bounding box size {bbox_size_mm}."
|
|
78
72
|
)
|
|
79
73
|
|
|
80
|
-
|
|
81
|
-
@bpp.run_decorator(
|
|
82
|
-
md={
|
|
83
|
-
"subplan_name": "change_aperture",
|
|
84
|
-
"aperture_size": new_selected_aperture.value,
|
|
85
|
-
}
|
|
86
|
-
)
|
|
87
|
-
def set_aperture():
|
|
88
|
-
yield from bps.abs_set(aperture_device, new_selected_aperture)
|
|
89
|
-
|
|
90
|
-
yield from set_aperture()
|
|
74
|
+
yield from bps.abs_set(aperture_device.selected_aperture, new_selected_aperture)
|
|
@@ -14,13 +14,13 @@ from dodal.devices.oav.pin_image_recognition.utils import NONE_VALUE
|
|
|
14
14
|
from dodal.devices.oav.utils import PinNotFoundException, wait_for_tip_to_be_found
|
|
15
15
|
from dodal.devices.smargon import Smargon
|
|
16
16
|
|
|
17
|
+
from mx_bluesky.common.device_setup_plans.setup_oav import (
|
|
18
|
+
pre_centring_setup_oav,
|
|
19
|
+
)
|
|
20
|
+
from mx_bluesky.common.parameters.constants import DocDescriptorNames, HardwareConstants
|
|
17
21
|
from mx_bluesky.common.utils.context import device_composite_from_context
|
|
18
22
|
from mx_bluesky.common.utils.exceptions import catch_exception_and_warn
|
|
19
23
|
from mx_bluesky.common.utils.log import LOGGER
|
|
20
|
-
from mx_bluesky.hyperion.device_setup_plans.setup_oav import (
|
|
21
|
-
pre_centring_setup_oav,
|
|
22
|
-
)
|
|
23
|
-
from mx_bluesky.hyperion.parameters.constants import CONST
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
26
|
from dodal.devices.oav.oav_parameters import OAVParameters
|
|
@@ -103,7 +103,7 @@ def grid_detection_plan(
|
|
|
103
103
|
yield from bps.mv(smargon.omega, angle)
|
|
104
104
|
# need to wait for the OAV image to update
|
|
105
105
|
# See #673 for improvements
|
|
106
|
-
yield from bps.sleep(
|
|
106
|
+
yield from bps.sleep(HardwareConstants.OAV_REFRESH_DELAY)
|
|
107
107
|
|
|
108
108
|
tip_x_px, tip_y_px = yield from catch_exception_and_warn(
|
|
109
109
|
PinNotFoundException, wait_for_tip_to_be_found, pin_tip_detection
|
|
@@ -163,7 +163,7 @@ def grid_detection_plan(
|
|
|
163
163
|
yield from bps.abs_set(oav.grid_snapshot.filename, snapshot_filename)
|
|
164
164
|
yield from bps.abs_set(oav.grid_snapshot.directory, snapshot_dir)
|
|
165
165
|
yield from bps.trigger(oav.grid_snapshot, wait=True)
|
|
166
|
-
yield from bps.create(
|
|
166
|
+
yield from bps.create(DocDescriptorNames.OAV_GRID_SNAPSHOT_TRIGGERED)
|
|
167
167
|
|
|
168
168
|
yield from bps.read(oav)
|
|
169
169
|
yield from bps.read(smargon)
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from abc import abstractmethod
|
|
4
4
|
from collections.abc import Callable, Sequence
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
|
6
7
|
|
|
7
8
|
from dodal.beamline_specific_utils.i03 import beam_size_from_aperture
|
|
@@ -26,7 +27,7 @@ from mx_bluesky.common.external_interaction.ispyb.ispyb_store import (
|
|
|
26
27
|
)
|
|
27
28
|
from mx_bluesky.common.external_interaction.ispyb.ispyb_utils import get_ispyb_config
|
|
28
29
|
from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample
|
|
29
|
-
from mx_bluesky.common.parameters.constants import DocDescriptorNames
|
|
30
|
+
from mx_bluesky.common.parameters.constants import DocDescriptorNames
|
|
30
31
|
from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER, set_dcgid_tag
|
|
31
32
|
from mx_bluesky.common.utils.utils import convert_eV_to_angstrom
|
|
32
33
|
|
|
@@ -70,10 +71,10 @@ class BaseISPyBCallback(PlanReactiveCallback):
|
|
|
70
71
|
self.ispyb: StoreInIspyb
|
|
71
72
|
self.descriptors: dict[str, EventDescriptor] = {}
|
|
72
73
|
self.ispyb_config = get_ispyb_config()
|
|
73
|
-
|
|
74
|
-
self.ispyb_config
|
|
75
|
-
|
|
76
|
-
):
|
|
74
|
+
ISPYB_ZOCALO_CALLBACK_LOGGER.info(
|
|
75
|
+
f"Using ISPyB configuration from {self.ispyb_config}"
|
|
76
|
+
)
|
|
77
|
+
if not self.ispyb_config or not Path(self.ispyb_config).is_absolute():
|
|
77
78
|
ISPYB_ZOCALO_CALLBACK_LOGGER.warning(
|
|
78
79
|
f"{self.__class__} using dev ISPyB config: {self.ispyb_config}. If you"
|
|
79
80
|
"want to use the real database, please set the ISPYB_CONFIG_PATH "
|
|
@@ -121,10 +122,8 @@ class BaseISPyBCallback(PlanReactiveCallback):
|
|
|
121
122
|
ISPYB_ZOCALO_CALLBACK_LOGGER.info(
|
|
122
123
|
"ISPyB handler received event from read hardware"
|
|
123
124
|
)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
SynchrotronMode,
|
|
127
|
-
)
|
|
125
|
+
synchrotron_mode = doc["data"]["synchrotron-synchrotron_mode"]
|
|
126
|
+
assert isinstance(synchrotron_mode, SynchrotronMode)
|
|
128
127
|
|
|
129
128
|
hwscan_data_collection_info = DataCollectionInfo(
|
|
130
129
|
undulator_gap1=doc["data"]["undulator-current_gap"],
|
|
@@ -35,7 +35,7 @@ def populate_remaining_data_collection_info(
|
|
|
35
35
|
data_collection_info.detector_id = I03_EIGER_DETECTOR
|
|
36
36
|
data_collection_info.comments = comment
|
|
37
37
|
data_collection_info.detector_distance = params.detector_params.detector_distance
|
|
38
|
-
data_collection_info.exp_time = params.detector_params.
|
|
38
|
+
data_collection_info.exp_time = params.detector_params.exposure_time_s
|
|
39
39
|
data_collection_info.imgdir = params.detector_params.directory
|
|
40
40
|
data_collection_info.imgprefix = params.detector_params.prefix
|
|
41
41
|
data_collection_info.imgsuffix = EIGER_FILE_SUFFIX
|
|
@@ -18,10 +18,11 @@ if TYPE_CHECKING:
|
|
|
18
18
|
|
|
19
19
|
class ZocaloCallback(CallbackBase):
|
|
20
20
|
"""Callback class to handle the triggering of Zocalo processing.
|
|
21
|
-
|
|
22
|
-
sub-plan, and sends a run_end signal on receiving a stop document for the same plan.
|
|
21
|
+
Will start listening for collections when {triggering_plan} has been started.
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
For every ispyb deposition that occurs inside this run the callback will send zocalo
|
|
24
|
+
a run_start signal. Once the {triggering_plan} has ended the callback will send a
|
|
25
|
+
run_end signal for all collections.
|
|
25
26
|
|
|
26
27
|
Shouldn't be subscribed directly to the RunEngine, instead should be passed to the
|
|
27
28
|
`emit` argument of an ISPyB callback which appends DCIDs to the relevant start doc.
|
|
@@ -30,7 +31,9 @@ class ZocaloCallback(CallbackBase):
|
|
|
30
31
|
def _reset_state(self):
|
|
31
32
|
self.run_uid: str | None = None
|
|
32
33
|
self.zocalo_info: list[ZocaloStartInfo] = []
|
|
34
|
+
self._started_zocalo_collections: list[ZocaloStartInfo] = []
|
|
33
35
|
self.descriptors: dict[str, EventDescriptor] = {}
|
|
36
|
+
self.start_frame = 0
|
|
34
37
|
|
|
35
38
|
def __init__(self, triggering_plan: str, zocalo_environment: str):
|
|
36
39
|
super().__init__()
|
|
@@ -42,26 +45,21 @@ class ZocaloCallback(CallbackBase):
|
|
|
42
45
|
ISPYB_ZOCALO_CALLBACK_LOGGER.info("Zocalo handler received start document.")
|
|
43
46
|
if self.triggering_plan and doc.get("subplan_name") == self.triggering_plan:
|
|
44
47
|
self.run_uid = doc.get("uid")
|
|
45
|
-
|
|
48
|
+
if self.run_uid:
|
|
46
49
|
if (
|
|
47
|
-
isinstance(
|
|
50
|
+
isinstance(scan_points := doc.get("scan_points"), list)
|
|
51
|
+
and isinstance(ispyb_ids := doc.get("ispyb_dcids"), tuple)
|
|
48
52
|
and len(ispyb_ids) > 0
|
|
49
53
|
):
|
|
50
54
|
ISPYB_ZOCALO_CALLBACK_LOGGER.info(f"Zocalo triggering for {ispyb_ids}")
|
|
51
55
|
ids_and_shape = list(zip(ispyb_ids, scan_points, strict=False))
|
|
52
|
-
start_frame = 0
|
|
53
|
-
self.zocalo_info = []
|
|
54
56
|
for idx, id_and_shape in enumerate(ids_and_shape):
|
|
55
57
|
id, shape = id_and_shape
|
|
56
58
|
num_frames = number_of_frames_from_scan_spec(shape)
|
|
57
59
|
self.zocalo_info.append(
|
|
58
|
-
ZocaloStartInfo(id, None, start_frame, num_frames, idx)
|
|
60
|
+
ZocaloStartInfo(id, None, self.start_frame, num_frames, idx)
|
|
59
61
|
)
|
|
60
|
-
start_frame += num_frames
|
|
61
|
-
else:
|
|
62
|
-
raise ISPyBDepositionNotMade(
|
|
63
|
-
f"No ISPyB IDs received by the start of {self.triggering_plan=}"
|
|
64
|
-
)
|
|
62
|
+
self.start_frame += num_frames
|
|
65
63
|
|
|
66
64
|
def descriptor(self, doc: EventDescriptor):
|
|
67
65
|
self.descriptors[doc["uid"]] = doc
|
|
@@ -73,6 +71,8 @@ class ZocaloCallback(CallbackBase):
|
|
|
73
71
|
for start_info in self.zocalo_info:
|
|
74
72
|
start_info.filename = filename
|
|
75
73
|
self.zocalo_interactor.run_start(start_info)
|
|
74
|
+
self._started_zocalo_collections.append(start_info)
|
|
75
|
+
self.zocalo_info = []
|
|
76
76
|
return doc
|
|
77
77
|
|
|
78
78
|
def stop(self, doc: RunStop):
|
|
@@ -80,7 +80,10 @@ class ZocaloCallback(CallbackBase):
|
|
|
80
80
|
ISPYB_ZOCALO_CALLBACK_LOGGER.info(
|
|
81
81
|
f"Zocalo handler received stop document, for run {doc.get('run_start')}."
|
|
82
82
|
)
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
if not self._started_zocalo_collections:
|
|
84
|
+
raise ISPyBDepositionNotMade(
|
|
85
|
+
f"No ISPyB IDs received by the end of {self.triggering_plan=}"
|
|
86
|
+
)
|
|
87
|
+
for info in self._started_zocalo_collections:
|
|
85
88
|
self.zocalo_interactor.run_end(info.ispyb_dcid)
|
|
86
89
|
self._reset_state()
|
|
@@ -15,12 +15,15 @@ class SampleHandlingCallback(PlanReactiveCallback):
|
|
|
15
15
|
"""Intercepts exceptions from experiment plans and updates the ISPyB BLSampleStatus
|
|
16
16
|
field according to the type of exception raised."""
|
|
17
17
|
|
|
18
|
-
def __init__(self):
|
|
18
|
+
def __init__(self, record_loaded_on_success=False):
|
|
19
19
|
super().__init__(log=ISPYB_ZOCALO_CALLBACK_LOGGER)
|
|
20
20
|
self._sample_id: int | None = None
|
|
21
21
|
self._descriptor: str | None = None
|
|
22
22
|
self._run_id: str | None = None
|
|
23
23
|
|
|
24
|
+
# Record 'sample loaded' if document successfully stops
|
|
25
|
+
self.record_loaded_on_success = record_loaded_on_success
|
|
26
|
+
|
|
24
27
|
def activity_gated_start(self, doc: RunStart):
|
|
25
28
|
if not self._sample_id and self.active:
|
|
26
29
|
sample_id = doc.get("metadata", {}).get("sample_id")
|
|
@@ -30,6 +33,7 @@ class SampleHandlingCallback(PlanReactiveCallback):
|
|
|
30
33
|
|
|
31
34
|
def activity_gated_stop(self, doc: RunStop) -> RunStop:
|
|
32
35
|
if self._run_id == doc.get("run_start"):
|
|
36
|
+
expeye = ExpeyeInteraction()
|
|
33
37
|
if doc["exit_status"] != "success":
|
|
34
38
|
exception_type, message = SampleException.type_and_message_from_reason(
|
|
35
39
|
doc.get("reason", "")
|
|
@@ -37,13 +41,17 @@ class SampleHandlingCallback(PlanReactiveCallback):
|
|
|
37
41
|
self.log.info(
|
|
38
42
|
f"Sample handling callback intercepted exception of type {exception_type}: {message}"
|
|
39
43
|
)
|
|
40
|
-
self._record_exception(exception_type)
|
|
44
|
+
self._record_exception(exception_type, expeye)
|
|
45
|
+
|
|
46
|
+
elif self.record_loaded_on_success:
|
|
47
|
+
self._record_loaded(expeye)
|
|
48
|
+
|
|
41
49
|
self._sample_id = None
|
|
42
50
|
self._run_id = None
|
|
51
|
+
|
|
43
52
|
return doc
|
|
44
53
|
|
|
45
|
-
def _record_exception(self, exception_type: str):
|
|
46
|
-
expeye = ExpeyeInteraction()
|
|
54
|
+
def _record_exception(self, exception_type: str, expeye: ExpeyeInteraction):
|
|
47
55
|
assert self._sample_id, "Unable to record exception due to no sample ID"
|
|
48
56
|
sample_status = self._decode_sample_status(exception_type)
|
|
49
57
|
expeye.update_sample_status(self._sample_id, sample_status)
|
|
@@ -53,3 +61,7 @@ class SampleHandlingCallback(PlanReactiveCallback):
|
|
|
53
61
|
case SampleException.__name__ | CrystalNotFoundException.__name__:
|
|
54
62
|
return BLSampleStatus.ERROR_SAMPLE
|
|
55
63
|
return BLSampleStatus.ERROR_BEAMLINE
|
|
64
|
+
|
|
65
|
+
def _record_loaded(self, expeye: ExpeyeInteraction):
|
|
66
|
+
assert self._sample_id, "Unable to record loaded state due to no sample ID"
|
|
67
|
+
expeye.update_sample_status(self._sample_id, BLSampleStatus.LOADED)
|
|
@@ -4,13 +4,8 @@ from collections.abc import Callable, Sequence
|
|
|
4
4
|
from time import time
|
|
5
5
|
from typing import TYPE_CHECKING, Any, TypeVar
|
|
6
6
|
|
|
7
|
-
import numpy as np
|
|
8
7
|
from bluesky import preprocessors as bpp
|
|
9
|
-
from bluesky.utils import MsgGenerator
|
|
10
|
-
from dodal.devices.zocalo.zocalo_results import (
|
|
11
|
-
ZOCALO_READING_PLAN_NAME,
|
|
12
|
-
get_processing_results_from_event,
|
|
13
|
-
)
|
|
8
|
+
from bluesky.utils import MsgGenerator, make_decorator
|
|
14
9
|
|
|
15
10
|
from mx_bluesky.common.external_interaction.callbacks.common.ispyb_callback_base import (
|
|
16
11
|
BaseISPyBCallback,
|
|
@@ -19,9 +14,6 @@ from mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping impor
|
|
|
19
14
|
populate_data_collection_group,
|
|
20
15
|
populate_remaining_data_collection_info,
|
|
21
16
|
)
|
|
22
|
-
from mx_bluesky.common.external_interaction.callbacks.common.logging_callback import (
|
|
23
|
-
format_doc_for_log,
|
|
24
|
-
)
|
|
25
17
|
from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_mapping import (
|
|
26
18
|
construct_comment_for_gridscan,
|
|
27
19
|
populate_xy_data_collection_info,
|
|
@@ -29,6 +21,7 @@ from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_mapping
|
|
|
29
21
|
)
|
|
30
22
|
from mx_bluesky.common.external_interaction.ispyb.data_model import (
|
|
31
23
|
DataCollectionGridInfo,
|
|
24
|
+
DataCollectionGroupInfo,
|
|
32
25
|
DataCollectionInfo,
|
|
33
26
|
DataCollectionPositionInfo,
|
|
34
27
|
Orientation,
|
|
@@ -52,6 +45,9 @@ from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER, set_dcgid_
|
|
|
52
45
|
if TYPE_CHECKING:
|
|
53
46
|
from event_model import Event, RunStart, RunStop
|
|
54
47
|
|
|
48
|
+
T = TypeVar("T", bound="GridCommon")
|
|
49
|
+
ASSERT_START_BEFORE_EVENT_DOC_MESSAGE = f"No data collection group info - event document has been emitted before a {PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN} start document"
|
|
50
|
+
|
|
55
51
|
|
|
56
52
|
def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters):
|
|
57
53
|
return bpp.set_run_key_wrapper(
|
|
@@ -67,7 +63,7 @@ def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters):
|
|
|
67
63
|
)
|
|
68
64
|
|
|
69
65
|
|
|
70
|
-
|
|
66
|
+
ispyb_activation_decorator = make_decorator(ispyb_activation_wrapper)
|
|
71
67
|
|
|
72
68
|
|
|
73
69
|
class GridscanISPyBCallback(BaseISPyBCallback):
|
|
@@ -97,10 +93,12 @@ class GridscanISPyBCallback(BaseISPyBCallback):
|
|
|
97
93
|
self.param_type = param_type
|
|
98
94
|
self._start_of_fgs_uid: str | None = None
|
|
99
95
|
self._processing_start_time: float | None = None
|
|
96
|
+
self.data_collection_group_info: DataCollectionGroupInfo | None
|
|
100
97
|
|
|
101
98
|
def activity_gated_start(self, doc: RunStart):
|
|
102
99
|
if doc.get("subplan_name") == PlanNameConstants.DO_FGS:
|
|
103
100
|
self._start_of_fgs_uid = doc.get("uid")
|
|
101
|
+
|
|
104
102
|
if doc.get("subplan_name") == PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN:
|
|
105
103
|
self.uid_to_finalize_on = doc.get("uid")
|
|
106
104
|
ISPYB_ZOCALO_CALLBACK_LOGGER.info(
|
|
@@ -111,7 +109,9 @@ class GridscanISPyBCallback(BaseISPyBCallback):
|
|
|
111
109
|
assert isinstance(mx_bluesky_parameters, str)
|
|
112
110
|
self.params = self.param_type.model_validate_json(mx_bluesky_parameters)
|
|
113
111
|
self.ispyb = StoreInIspyb(self.ispyb_config)
|
|
114
|
-
data_collection_group_info = populate_data_collection_group(
|
|
112
|
+
self.data_collection_group_info = populate_data_collection_group(
|
|
113
|
+
self.params
|
|
114
|
+
)
|
|
115
115
|
|
|
116
116
|
scan_data_infos = [
|
|
117
117
|
ScanDataInfo(
|
|
@@ -135,56 +135,37 @@ class GridscanISPyBCallback(BaseISPyBCallback):
|
|
|
135
135
|
]
|
|
136
136
|
|
|
137
137
|
self.ispyb_ids = self.ispyb.begin_deposition(
|
|
138
|
-
data_collection_group_info, scan_data_infos
|
|
138
|
+
self.data_collection_group_info, scan_data_infos
|
|
139
139
|
)
|
|
140
140
|
set_dcgid_tag(self.ispyb_ids.data_collection_group_id)
|
|
141
141
|
return super().activity_gated_start(doc)
|
|
142
142
|
|
|
143
143
|
def activity_gated_event(self, doc: Event):
|
|
144
|
+
assert self.data_collection_group_info, ASSERT_START_BEFORE_EVENT_DOC_MESSAGE
|
|
145
|
+
|
|
144
146
|
doc = super().activity_gated_event(doc)
|
|
145
147
|
|
|
146
148
|
descriptor_name = self.descriptors[doc["descriptor"]].get("name")
|
|
147
|
-
if descriptor_name ==
|
|
148
|
-
self._handle_zocalo_read_event(doc)
|
|
149
|
-
elif descriptor_name == DocDescriptorNames.OAV_GRID_SNAPSHOT_TRIGGERED:
|
|
149
|
+
if descriptor_name == DocDescriptorNames.OAV_GRID_SNAPSHOT_TRIGGERED:
|
|
150
150
|
scan_data_infos = self._handle_oav_grid_snapshot_triggered(doc)
|
|
151
151
|
self.ispyb_ids = self.ispyb.update_deposition(
|
|
152
152
|
self.ispyb_ids, scan_data_infos
|
|
153
153
|
)
|
|
154
|
+
self.ispyb.update_data_collection_group_table(
|
|
155
|
+
self.data_collection_group_info, self.ispyb_ids.data_collection_group_id
|
|
156
|
+
)
|
|
154
157
|
|
|
155
158
|
return doc
|
|
156
159
|
|
|
157
|
-
def
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
)
|
|
160
|
+
def _add_processing_time_to_comment(self, processing_start_time: float):
|
|
161
|
+
assert self.data_collection_group_info, ASSERT_START_BEFORE_EVENT_DOC_MESSAGE
|
|
162
|
+
proc_time = time() - processing_start_time
|
|
163
|
+
crystal_summary = f"Zocalo processing took {proc_time:.2f} s."
|
|
164
|
+
|
|
165
|
+
self.data_collection_group_info.comments = (
|
|
166
|
+
self.data_collection_group_info.comments or ""
|
|
167
|
+
) + crystal_summary
|
|
166
168
|
|
|
167
|
-
raw_results = get_processing_results_from_event("zocalo", doc)
|
|
168
|
-
if len(raw_results) > 0:
|
|
169
|
-
for n, res in enumerate(raw_results):
|
|
170
|
-
bb = res["bounding_box"]
|
|
171
|
-
diff = np.array(bb[1]) - np.array(bb[0])
|
|
172
|
-
bboxes.append(diff)
|
|
173
|
-
|
|
174
|
-
nicely_formatted_com = [
|
|
175
|
-
f"{np.round(com, 2)}" for com in res["centre_of_mass"]
|
|
176
|
-
]
|
|
177
|
-
crystal_summary += (
|
|
178
|
-
f"Crystal {n + 1}: "
|
|
179
|
-
f"Strength {res['total_count']}; "
|
|
180
|
-
f"Position (grid boxes) {nicely_formatted_com}; "
|
|
181
|
-
f"Size (grid boxes) {bboxes[n]}; "
|
|
182
|
-
)
|
|
183
|
-
else:
|
|
184
|
-
crystal_summary += "Zocalo found no crystals in this gridscan."
|
|
185
|
-
assert self.ispyb_ids.data_collection_ids, (
|
|
186
|
-
"No data collection to add results to"
|
|
187
|
-
)
|
|
188
169
|
self.ispyb.append_to_comment(
|
|
189
170
|
self.ispyb_ids.data_collection_ids[0], crystal_summary
|
|
190
171
|
)
|
|
@@ -192,6 +173,7 @@ class GridscanISPyBCallback(BaseISPyBCallback):
|
|
|
192
173
|
def _handle_oav_grid_snapshot_triggered(self, doc) -> Sequence[ScanDataInfo]:
|
|
193
174
|
assert self.ispyb_ids.data_collection_ids, "No current data collection"
|
|
194
175
|
assert self.params, "ISPyB handler didn't receive parameters!"
|
|
176
|
+
assert self.data_collection_group_info, "No data collection group"
|
|
195
177
|
data = doc["data"]
|
|
196
178
|
data_collection_id = None
|
|
197
179
|
data_collection_info = DataCollectionInfo(
|
|
@@ -220,6 +202,18 @@ class GridscanISPyBCallback(BaseISPyBCallback):
|
|
|
220
202
|
data_collection_info.comments = construct_comment_for_gridscan(
|
|
221
203
|
data_collection_grid_info
|
|
222
204
|
)
|
|
205
|
+
|
|
206
|
+
if self.data_collection_group_info.comments:
|
|
207
|
+
self.data_collection_group_info.comments += (
|
|
208
|
+
f"by {data_collection_grid_info.steps_y}."
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
self.data_collection_group_info.comments = (
|
|
212
|
+
f"Diffraction grid scan of "
|
|
213
|
+
f"{data_collection_grid_info.steps_x} "
|
|
214
|
+
f"by {data_collection_grid_info.steps_y} "
|
|
215
|
+
)
|
|
216
|
+
|
|
223
217
|
if len(self.ispyb_ids.data_collection_ids) > self._oav_snapshot_event_idx:
|
|
224
218
|
data_collection_id = self.ispyb_ids.data_collection_ids[
|
|
225
219
|
self._oav_snapshot_event_idx
|
|
@@ -275,6 +269,9 @@ class GridscanISPyBCallback(BaseISPyBCallback):
|
|
|
275
269
|
return scan_data_infos
|
|
276
270
|
|
|
277
271
|
def activity_gated_stop(self, doc: RunStop) -> RunStop:
|
|
272
|
+
assert self.data_collection_group_info, (
|
|
273
|
+
f"No data collection group info - stop document has been emitted before a {PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN} start document"
|
|
274
|
+
)
|
|
278
275
|
if doc.get("run_start") == self._start_of_fgs_uid:
|
|
279
276
|
self._processing_start_time = time()
|
|
280
277
|
if doc.get("run_start") == self.uid_to_finalize_on:
|
|
@@ -289,5 +286,13 @@ class GridscanISPyBCallback(BaseISPyBCallback):
|
|
|
289
286
|
)
|
|
290
287
|
if exception_type:
|
|
291
288
|
doc["reason"] = message
|
|
289
|
+
self.data_collection_group_info.comments = message
|
|
290
|
+
elif self._processing_start_time:
|
|
291
|
+
self._add_processing_time_to_comment(self._processing_start_time)
|
|
292
|
+
self.ispyb.update_data_collection_group_table(
|
|
293
|
+
self.data_collection_group_info,
|
|
294
|
+
self.ispyb_ids.data_collection_group_id,
|
|
295
|
+
)
|
|
296
|
+
self.data_collection_group_info = None
|
|
292
297
|
return super().activity_gated_stop(doc)
|
|
293
298
|
return self._tag_doc(doc)
|
|
@@ -79,7 +79,8 @@ class GridscanNexusFileCallback(PlanReactiveCallback):
|
|
|
79
79
|
self.descriptors[doc["uid"]] = doc
|
|
80
80
|
|
|
81
81
|
def activity_gated_event(self, doc: Event) -> Event | None:
|
|
82
|
-
|
|
82
|
+
event_descriptor = self.descriptors.get(doc["descriptor"])
|
|
83
|
+
assert event_descriptor is not None
|
|
83
84
|
if event_descriptor.get("name") == DocDescriptorNames.HARDWARE_READ_DURING:
|
|
84
85
|
data = doc["data"]
|
|
85
86
|
for nexus_writer in [self.nexus_writer_1, self.nexus_writer_2]:
|
|
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
|
|
|
6
6
|
|
|
7
7
|
import ispyb
|
|
8
8
|
import ispyb.sqlalchemy
|
|
9
|
+
import numpy as np
|
|
9
10
|
from ispyb.connector.mysqlsp.main import ISPyBMySQLSPConnector as Connector
|
|
10
11
|
from ispyb.sp.mxacquisition import MXAcquisition
|
|
11
12
|
from ispyb.strictordereddict import StrictOrderedDict
|
|
@@ -77,7 +78,7 @@ class StoreInIspyb:
|
|
|
77
78
|
scan_data_infos,
|
|
78
79
|
) -> IspybIds:
|
|
79
80
|
with ispyb.open(self.ISPYB_CONFIG_PATH) as conn:
|
|
80
|
-
assert conn
|
|
81
|
+
assert conn, "Failed to connect to ISPyB"
|
|
81
82
|
if data_collection_group_info:
|
|
82
83
|
ispyb_ids.data_collection_group_id = (
|
|
83
84
|
self._store_data_collection_group_table(
|
|
@@ -152,6 +153,19 @@ class StoreInIspyb:
|
|
|
152
153
|
data_collection_id, comment, delimiter
|
|
153
154
|
)
|
|
154
155
|
|
|
156
|
+
def update_data_collection_group_table(
|
|
157
|
+
self,
|
|
158
|
+
dcg_info: DataCollectionGroupInfo,
|
|
159
|
+
data_collection_group_id: int | None = None,
|
|
160
|
+
) -> None:
|
|
161
|
+
with ispyb.open(self.ISPYB_CONFIG_PATH) as conn:
|
|
162
|
+
assert conn is not None, "Failed to connect to ISPyB!"
|
|
163
|
+
self._store_data_collection_group_table(
|
|
164
|
+
conn,
|
|
165
|
+
dcg_info,
|
|
166
|
+
data_collection_group_id,
|
|
167
|
+
)
|
|
168
|
+
|
|
155
169
|
def _update_scan_with_end_time_and_status(
|
|
156
170
|
self,
|
|
157
171
|
end_time: str,
|
|
@@ -267,7 +281,9 @@ class StoreInIspyb:
|
|
|
267
281
|
conn, data_collection_info.visit_string
|
|
268
282
|
)
|
|
269
283
|
params |= {
|
|
270
|
-
k: v
|
|
284
|
+
k: v.item() if isinstance(v, np.generic) else v # Convert to native types
|
|
285
|
+
for k, v in asdict(data_collection_info).items()
|
|
286
|
+
if k != "visit_string"
|
|
271
287
|
}
|
|
272
288
|
|
|
273
289
|
return params
|
|
@@ -7,11 +7,11 @@ from ispyb import NoResult
|
|
|
7
7
|
from ispyb.connector.mysqlsp.main import ISPyBMySQLSPConnector as Connector
|
|
8
8
|
from ispyb.sp.core import Core
|
|
9
9
|
|
|
10
|
-
from mx_bluesky.common.parameters.constants import SimConstants
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
def get_ispyb_config() -> str:
|
|
12
|
+
ispyb_config = os.environ.get("ISPYB_CONFIG_PATH")
|
|
13
|
+
assert ispyb_config, "ISPYB_CONFIG_PATH must be set"
|
|
14
|
+
return ispyb_config
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def get_session_id_from_visit(conn: Connector, visit: str):
|
|
@@ -144,7 +144,7 @@ def create_detector_parameters(detector_params: DetectorParams) -> Detector:
|
|
|
144
144
|
list(
|
|
145
145
|
detector_params.get_beam_position_pixels(detector_params.detector_distance)
|
|
146
146
|
),
|
|
147
|
-
detector_params.
|
|
147
|
+
detector_params.exposure_time_s,
|
|
148
148
|
[(-1.0, 0.0, 0.0), (0.0, -1.0, 0.0)],
|
|
149
149
|
)
|
|
150
150
|
|