diff --git a/superxas_bec/scans/timepix_scan.py b/superxas_bec/scans/timepix_scan.py index 802442f..5e51e27 100644 --- a/superxas_bec/scans/timepix_scan.py +++ b/superxas_bec/scans/timepix_scan.py @@ -47,20 +47,23 @@ class TimepixScan(LineScan): # fmt: off time_roi_step: Annotated[int, ScanArgument(display_name="Time ROI step", description="Bin size", gt=0)], time_roi_n: Annotated[int, ScanArgument(display_name="Time ROI n", description="Number of time points", gt=0)], - idx_off_start: Annotated[int, ScanArgument(display_name="Index Off Start", description="Start Index for Off Level", ge=0)], - idx_off_stop: Annotated[int, ScanArgument(display_name="Index Off Stop", description="Stop Index for Off Level", ge=0)], - idx_on_start: Annotated[int, ScanArgument(display_name="Index On Start", description="Start Index for On Level", ge=0)], - idx_on_stop: Annotated[int, ScanArgument(display_name="Index On Stop", description="Stop Index for On Level", ge=0)], + idx_off_start: Annotated[int, ScanArgument(display_name="Index Off Start", description="Start Index for Off Level (first time bin)", ge=0)], + idx_off_stop: Annotated[int, ScanArgument(display_name="Index Off Stop", description="Stop Index for Off Level (exclusive)", ge=0)], + idx_on_start: Annotated[int, ScanArgument(display_name="Index On Start", description="Start Index for On Level (first time bin)", ge=0)], + idx_on_stop: Annotated[int, ScanArgument(display_name="Index On Stop", description="Stop Index for On Level (exclusive)", ge=0)], # fmt: on **kwargs, ): - if idx_off_start > idx_off_stop: + # The on/off windows are half-open index ranges [start, stop) over the time_roi_n time + # bins; the Timepix device averages each window into xes_level_*. An empty window + # (start == stop) would only produce NaN, so it is rejected here as well. + if idx_off_start >= idx_off_stop: raise ValueError( - f"idx_off_start ({idx_off_start}) cannot be bigger than idx_off_stop ({idx_off_stop})!" + f"idx_off_start ({idx_off_start}) must be smaller than idx_off_stop ({idx_off_stop})!" ) - if idx_on_start > idx_on_stop: + if idx_on_start >= idx_on_stop: raise ValueError( - f"idx_on_start ({idx_on_start}) cannot be bigger than idx_on_stop ({idx_on_stop})!" + f"idx_on_start ({idx_on_start}) must be smaller than idx_on_stop ({idx_on_stop})!" ) for name, idx in ( ("idx_off_start", idx_off_start), diff --git a/tests/tests_scans/test_timepix_scan.py b/tests/tests_scans/test_timepix_scan.py new file mode 100644 index 0000000..87c4dce --- /dev/null +++ b/tests/tests_scans/test_timepix_scan.py @@ -0,0 +1,75 @@ +# pylint: skip-file +import pytest +from bec_lib.bec_errors import ScanInputValidationError +from bec_server.scan_server.tests.scan_fixtures import * + +from superxas_bec.scans.timepix_scan import TimepixScan + +TIMEPIX_PARAMETERS = { + "time_roi_step": 1, + "time_roi_n": 10, + "idx_off_start": 0, + "idx_off_stop": 4, + "idx_on_start": 5, + "idx_on_stop": 10, +} + + +def _assemble_timepix_scan(scan_assembler, **overrides): + params = {**TIMEPIX_PARAMETERS, **overrides} + return scan_assembler( + "timepix_scan", "samx", -1.0, 1.0, steps=3, relative=False, exp_time=0.1, **params + ) + + +def test_timepix_scan_forwards_parameters_to_scan_info(scan_assembler): + """The Timepix device reads the parameters from additional_scan_parameters.""" + scan = _assemble_timepix_scan(scan_assembler) + + assert isinstance(scan, TimepixScan) + assert scan.scan_info.scan_name == "timepix_scan" + for name, value in TIMEPIX_PARAMETERS.items(): + assert scan.scan_info.additional_scan_parameters[name] == value + + +@pytest.mark.parametrize( + "overrides, match", + [ + ({"idx_off_start": 5, "idx_off_stop": 4}, "idx_off_start"), + ({"idx_on_start": 9, "idx_on_stop": 6}, "idx_on_start"), + # start == stop would be an empty window + ({"idx_off_start": 3, "idx_off_stop": 3}, "idx_off_start"), + ({"idx_on_start": 7, "idx_on_stop": 7}, "idx_on_start"), + ], +) +def test_timepix_scan_rejects_start_not_smaller_than_stop(scan_assembler, overrides, match): + with pytest.raises(ValueError, match=match): + _assemble_timepix_scan(scan_assembler, **overrides) + + +@pytest.mark.parametrize( + "overrides, name", + [ + ({"idx_off_stop": 11}, "idx_off_stop"), + ({"idx_on_stop": 11}, "idx_on_stop"), + ({"idx_on_start": 11, "idx_on_stop": 12}, "idx_on_start"), + ], +) +def test_timepix_scan_rejects_index_above_time_roi_n(scan_assembler, overrides, name): + with pytest.raises(ValueError, match=f"{name} with value"): + _assemble_timepix_scan(scan_assembler, **overrides) + + +def test_timepix_scan_accepts_stop_equal_to_time_roi_n(scan_assembler): + """stop is exclusive, so a window may end at time_roi_n.""" + scan = _assemble_timepix_scan(scan_assembler, idx_on_start=9, idx_on_stop=10) + + assert scan.scan_info.additional_scan_parameters["idx_on_stop"] == 10 + + +@pytest.mark.parametrize( + "overrides", [{"idx_off_start": -1}, {"time_roi_n": 0}, {"time_roi_step": 0}] +) +def test_timepix_scan_argument_bounds_are_validated(scan_assembler, overrides): + with pytest.raises(ScanInputValidationError): + _assemble_timepix_scan(scan_assembler, **overrides)