From 9825745ada705e85f811d434e8ec3e2072cc9961 Mon Sep 17 00:00:00 2001 From: Jacob Williamson Date: Thu, 12 Nov 2026 10:58:37 +0000 Subject: [PATCH 1/3] Get different collection spec from config server based on qmax --- .../i15_1/plans/blower_collection.py | 4 +- .../plans/single_temperature_collection.py | 21 +++++++--- .../test_single_temperature_collection.py | 39 ++++++++++++++++++- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/crystallography_bluesky/i15_1/plans/blower_collection.py b/src/crystallography_bluesky/i15_1/plans/blower_collection.py index 23a8398..d4d5c64 100644 --- a/src/crystallography_bluesky/i15_1/plans/blower_collection.py +++ b/src/crystallography_bluesky/i15_1/plans/blower_collection.py @@ -20,6 +20,7 @@ setup_zebra_for_hardware_triggering, ) from crystallography_bluesky.i15_1.plans.single_temperature_collection import ( + QMAX_OPTIONS, CollectionSpecPerTemp, TemperatureCollectionSpecification, get_collection_specification, @@ -58,6 +59,7 @@ def blower_collection( ramp_rate_c_per_min: float, settle_time: float, scan_type: ScanType, + qmax: QMAX_OPTIONS | None = None, generic_collection_devices: GenericCollectionDevices = devices, blower: Blower = blower, baseline_devices: list[StandardReadable] | None = None, @@ -69,7 +71,7 @@ def blower_collection( yield from bps.abs_set(blower.ramp_rate_c_per_sec, ramp_rate_c_per_min / 60) single_collection_spec, total_frames = get_collection_specification( - time_per_collection, exposure_time_per_frame + time_per_collection, exposure_time_per_frame, qmax ) minimum_dead_time = 0.0001 # Minimum value independent of timebase trigger_pulse_width = 0.0001 # Assumes zebra is set to seconds timebase diff --git a/src/crystallography_bluesky/i15_1/plans/single_temperature_collection.py b/src/crystallography_bluesky/i15_1/plans/single_temperature_collection.py index 692eb36..5d6f86e 100644 --- a/src/crystallography_bluesky/i15_1/plans/single_temperature_collection.py +++ b/src/crystallography_bluesky/i15_1/plans/single_temperature_collection.py @@ -75,12 +75,15 @@ def no_temperature_must_not_have_units(self) -> Self: TemperatureCollectionSpecification: TypeAlias = list[CollectionSpecPerTemp] -COLLECTION_SPEC_FILEPATH = ( - "/dls_sw/i15-1/software/daq_configuration/collection_specification.txt" +BASE_COLLECTION_SPEC_FILEPATH = ( + "/dls_sw/i15-1/software/daq_configuration/" + + "collection_specifications/collection_specification" ) devices = inject("") +QMAX_OPTIONS = Literal[25, 30, 35, 40] + def _calculate_number_of_frames( fraction_of_time: float, @@ -90,8 +93,14 @@ def _calculate_number_of_frames( return ceil((fraction_of_time * full_collection_time) / exposure_time_per_frame) +def _get_collection_spec_filepath(qmax: QMAX_OPTIONS | None): + return BASE_COLLECTION_SPEC_FILEPATH + (f"_{str(qmax)}" if qmax else "") + ".txt" + + def get_collection_specification( - full_collection_time: float, exposure_time_per_frame: float + full_collection_time: float, + exposure_time_per_frame: float, + qmax: QMAX_OPTIONS | None = None, ) -> tuple[CollectionSpecification, int]: """The standard collection specification is defined in configuration but needs conversion: @@ -100,9 +109,10 @@ def get_collection_specification( * The transmission is just a float, we want to convert to one of the aperture options. """ + filepath = _get_collection_spec_filepath(qmax) config_client = get_config_client() collection_spec_from_config = config_client.get_file_contents( - COLLECTION_SPEC_FILEPATH, + filepath, CollectionSpecFromConfig, reset_cached_result=True, ).tth_angle_to_specification @@ -222,6 +232,7 @@ def data_collection( full_collection_time: float, exposure_time_per_frame: float, scan_type: DataCollectionScanType | AuxiliaryScanType, + qmax: QMAX_OPTIONS | None = None, generic_collection_devices: GenericCollectionDevices = devices, baseline_devices: list[StandardReadable] | None = None, metadata: dict[str, Any] | None = None, @@ -231,7 +242,7 @@ def data_collection( trigger_pulse_width = 0.0001 # Assumes zebra is set to seconds timebase collection_spec, total_frames = get_collection_specification( - full_collection_time, exposure_time_per_frame + full_collection_time, exposure_time_per_frame, qmax ) first_frames_value = next(iter(collection_spec)).frames diff --git a/tests/unit_tests/i15_1/test_single_temperature_collection.py b/tests/unit_tests/i15_1/test_single_temperature_collection.py index 143ab31..836b018 100644 --- a/tests/unit_tests/i15_1/test_single_temperature_collection.py +++ b/tests/unit_tests/i15_1/test_single_temperature_collection.py @@ -27,9 +27,11 @@ GenericCollectionDevices, ) from crystallography_bluesky.i15_1.plans.single_temperature_collection import ( - COLLECTION_SPEC_FILEPATH, + BASE_COLLECTION_SPEC_FILEPATH, + QMAX_OPTIONS, CollectionSpecPerPosition, _calculate_number_of_frames, + _get_collection_spec_filepath, data_collection, inner_collection, ) @@ -262,7 +264,9 @@ def test_data_collection_gets_positions_to_fraction_from_config_server( ) ) mock_collection_spec_config_client.get_file_contents.assert_called_once_with( - COLLECTION_SPEC_FILEPATH, CollectionSpecification, reset_cached_result=True + BASE_COLLECTION_SPEC_FILEPATH + ".txt", + CollectionSpecification, + reset_cached_result=True, ) @@ -431,3 +435,34 @@ async def test_if_software_read_faster_than_time_between_frames_then_sleep_calle assert not caplog.text assert mock_sleep.call_count == 10 assert mock_sleep.call_args_list == [call(pytest.approx(0.9)) for _ in range(10)] + + +@pytest.mark.parametrize( + "qmax, expected_filepath", + [ + ( + 25, + "/dls_sw/i15-1/software/daq_configuration/collection_specifications/collection_specification_25.txt", + ), + ( + 30, + "/dls_sw/i15-1/software/daq_configuration/collection_specifications/collection_specification_30.txt", + ), + ( + 35, + "/dls_sw/i15-1/software/daq_configuration/collection_specifications/collection_specification_35.txt", + ), + ( + 40, + "/dls_sw/i15-1/software/daq_configuration/collection_specifications/collection_specification_40.txt", + ), + ( + None, + "/dls_sw/i15-1/software/daq_configuration/collection_specifications/collection_specification.txt", + ), + ], +) +def test__get_collection_spec_filepath_returns_expected_filepath( + qmax: QMAX_OPTIONS | None, expected_filepath: str +): + assert _get_collection_spec_filepath(qmax) == expected_filepath From 2b77c7b9d93a3fc17a4e1b0aa83f8bee5bdcf150 Mon Sep 17 00:00:00 2001 From: Jacob Williamson Date: Thu, 12 Nov 2026 12:57:39 +0000 Subject: [PATCH 2/3] Change argument name --- .../i15_1/plans/blower_collection.py | 6 +++--- .../i15_1/plans/single_temperature_collection.py | 14 +++++++------- .../i15_1/test_single_temperature_collection.py | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/crystallography_bluesky/i15_1/plans/blower_collection.py b/src/crystallography_bluesky/i15_1/plans/blower_collection.py index d4d5c64..34e7f1b 100644 --- a/src/crystallography_bluesky/i15_1/plans/blower_collection.py +++ b/src/crystallography_bluesky/i15_1/plans/blower_collection.py @@ -20,7 +20,7 @@ setup_zebra_for_hardware_triggering, ) from crystallography_bluesky.i15_1.plans.single_temperature_collection import ( - QMAX_OPTIONS, + Q_MAX_OPTIONS, CollectionSpecPerTemp, TemperatureCollectionSpecification, get_collection_specification, @@ -59,7 +59,7 @@ def blower_collection( ramp_rate_c_per_min: float, settle_time: float, scan_type: ScanType, - qmax: QMAX_OPTIONS | None = None, + q_max: Q_MAX_OPTIONS | None = None, generic_collection_devices: GenericCollectionDevices = devices, blower: Blower = blower, baseline_devices: list[StandardReadable] | None = None, @@ -71,7 +71,7 @@ def blower_collection( yield from bps.abs_set(blower.ramp_rate_c_per_sec, ramp_rate_c_per_min / 60) single_collection_spec, total_frames = get_collection_specification( - time_per_collection, exposure_time_per_frame, qmax + time_per_collection, exposure_time_per_frame, q_max ) minimum_dead_time = 0.0001 # Minimum value independent of timebase trigger_pulse_width = 0.0001 # Assumes zebra is set to seconds timebase diff --git a/src/crystallography_bluesky/i15_1/plans/single_temperature_collection.py b/src/crystallography_bluesky/i15_1/plans/single_temperature_collection.py index 5d6f86e..3f30c05 100644 --- a/src/crystallography_bluesky/i15_1/plans/single_temperature_collection.py +++ b/src/crystallography_bluesky/i15_1/plans/single_temperature_collection.py @@ -82,7 +82,7 @@ def no_temperature_must_not_have_units(self) -> Self: devices = inject("") -QMAX_OPTIONS = Literal[25, 30, 35, 40] +Q_MAX_OPTIONS = Literal[25, 30, 35, 40] def _calculate_number_of_frames( @@ -93,14 +93,14 @@ def _calculate_number_of_frames( return ceil((fraction_of_time * full_collection_time) / exposure_time_per_frame) -def _get_collection_spec_filepath(qmax: QMAX_OPTIONS | None): - return BASE_COLLECTION_SPEC_FILEPATH + (f"_{str(qmax)}" if qmax else "") + ".txt" +def _get_collection_spec_filepath(q_max: Q_MAX_OPTIONS | None): + return BASE_COLLECTION_SPEC_FILEPATH + (f"_{str(q_max)}" if q_max else "") + ".txt" def get_collection_specification( full_collection_time: float, exposure_time_per_frame: float, - qmax: QMAX_OPTIONS | None = None, + q_max: Q_MAX_OPTIONS | None = None, ) -> tuple[CollectionSpecification, int]: """The standard collection specification is defined in configuration but needs conversion: @@ -109,7 +109,7 @@ def get_collection_specification( * The transmission is just a float, we want to convert to one of the aperture options. """ - filepath = _get_collection_spec_filepath(qmax) + filepath = _get_collection_spec_filepath(q_max) config_client = get_config_client() collection_spec_from_config = config_client.get_file_contents( filepath, @@ -232,7 +232,7 @@ def data_collection( full_collection_time: float, exposure_time_per_frame: float, scan_type: DataCollectionScanType | AuxiliaryScanType, - qmax: QMAX_OPTIONS | None = None, + q_max: Q_MAX_OPTIONS | None = None, generic_collection_devices: GenericCollectionDevices = devices, baseline_devices: list[StandardReadable] | None = None, metadata: dict[str, Any] | None = None, @@ -242,7 +242,7 @@ def data_collection( trigger_pulse_width = 0.0001 # Assumes zebra is set to seconds timebase collection_spec, total_frames = get_collection_specification( - full_collection_time, exposure_time_per_frame, qmax + full_collection_time, exposure_time_per_frame, q_max ) first_frames_value = next(iter(collection_spec)).frames diff --git a/tests/unit_tests/i15_1/test_single_temperature_collection.py b/tests/unit_tests/i15_1/test_single_temperature_collection.py index 836b018..dd4f3a6 100644 --- a/tests/unit_tests/i15_1/test_single_temperature_collection.py +++ b/tests/unit_tests/i15_1/test_single_temperature_collection.py @@ -28,7 +28,7 @@ ) from crystallography_bluesky.i15_1.plans.single_temperature_collection import ( BASE_COLLECTION_SPEC_FILEPATH, - QMAX_OPTIONS, + Q_MAX_OPTIONS, CollectionSpecPerPosition, _calculate_number_of_frames, _get_collection_spec_filepath, @@ -438,7 +438,7 @@ async def test_if_software_read_faster_than_time_between_frames_then_sleep_calle @pytest.mark.parametrize( - "qmax, expected_filepath", + "q_max, expected_filepath", [ ( 25, @@ -463,6 +463,6 @@ async def test_if_software_read_faster_than_time_between_frames_then_sleep_calle ], ) def test__get_collection_spec_filepath_returns_expected_filepath( - qmax: QMAX_OPTIONS | None, expected_filepath: str + q_max: Q_MAX_OPTIONS | None, expected_filepath: str ): - assert _get_collection_spec_filepath(qmax) == expected_filepath + assert _get_collection_spec_filepath(q_max) == expected_filepath From bef5e149c3e0371f0167bd11e828e3b595264a2c Mon Sep 17 00:00:00 2001 From: Jacob Williamson Date: Thu, 12 Nov 2026 13:44:03 +0000 Subject: [PATCH 3/3] Add log --- .../i15_1/plans/single_temperature_collection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/crystallography_bluesky/i15_1/plans/single_temperature_collection.py b/src/crystallography_bluesky/i15_1/plans/single_temperature_collection.py index 3f30c05..392bd6a 100644 --- a/src/crystallography_bluesky/i15_1/plans/single_temperature_collection.py +++ b/src/crystallography_bluesky/i15_1/plans/single_temperature_collection.py @@ -110,6 +110,7 @@ def get_collection_specification( options. """ filepath = _get_collection_spec_filepath(q_max) + LOGGER.info(f"Using collection spec from {filepath}") config_client = get_config_client() collection_spec_from_config = config_client.get_file_contents( filepath,