From 0adc946fe5dbc49d9b76b2c6df869f46f9a6cd7f Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Fri, 1 May 2026 13:19:18 +0000 Subject: [PATCH 01/11] Refactor scans so they are flatter --- src/dodal/plans/wrapped.py | 327 +++++++++++++++++++++--------------- tests/plans/test_wrapped.py | 215 ++++++++++++------------ 2 files changed, 299 insertions(+), 243 deletions(-) diff --git a/src/dodal/plans/wrapped.py b/src/dodal/plans/wrapped.py index 1ea01906db8..043f7ad922f 100644 --- a/src/dodal/plans/wrapped.py +++ b/src/dodal/plans/wrapped.py @@ -62,31 +62,34 @@ def count( yield from bp.count(tuple(detectors), num, delay=delay, md=metadata) -def _make_num_scan_args( - params: list[tuple[Movable, list[float | int]]], num: int | None = None -): - shape = [] - if num: - shape = [num] - for param in params: - if len(param[1]) == 2: - pass - else: - raise ValueError("You must provide 'start stop' for each motor.") - else: - for param in params: - if len(param[1]) == 3: - shape.append(param[1][-1]) - else: - raise ValueError( - "You must provide 'start stop num' for each motor in a grid scan." - ) - - args = [] - for param in params: - args.append(param[0]) - args.extend(param[1]) - return args, shape +# def _make_num_scan_args( +# params: Sequence[Movable | float | int], num: int | None = None +# ) -> list[float]: +# # shape = [] +# # if num: +# # shape = [num] +# # for param in params: +# # if len(param[1]) == 2: +# # pass +# # else: +# # raise ValueError("You must provide 'start stop' for each motor.") +# # else: +# # for param in params: +# # if len(param[1]) == 3: +# # shape.append(param[1][-1]) +# # else: +# # raise ValueError( +# # "You must provide 'start stop num' for each motor in a grid scan." +# # ) + +# # args = [] +# # for param in params: +# # args.append(param[0]) +# # args.extend(param[1]) +# for param in params: +# if isinstance(param, Movable): + +# return args, shape @validate_call(config={"arbitrary_types_allowed": True}) @@ -98,11 +101,11 @@ def num_scan( ), ], params: Annotated[ - list[tuple[Movable, list[float | int]]], + Sequence[Movable | float | int], Field( description="List of tuples (device, parameter). For concurrent " - "trajectories, provide '[(movable1, [start1, stop1]), (movable2, [start2, " - "stop2]), ... , (movableN, [startN, stopN])]'." + "trajectories, provide '[movable1, start1, stop1, movable2, start2, stop2, " + "... , movableN, startN, stopN]'." ), ], num: int, @@ -113,12 +116,10 @@ def num_scan( The scan is defined by number of points along scan trajector(y/ies). Wraps bluesky.plans.scan(det, *args, num, md=metadata). """ - # TODO: move to using Range spec and spec_scan when stable and tested at v1.0 - args, shape = _make_num_scan_args(params, num) metadata = metadata or {} - metadata["shape"] = shape + metadata["shape"] = (num,) - yield from bp.scan(tuple(detectors), *args, num=num, md=metadata) + yield from bp.scan(tuple(detectors), *params, num=num, md=metadata) @validate_call(config={"arbitrary_types_allowed": True}) @@ -130,7 +131,7 @@ def num_grid_scan( ), ], params: Annotated[ - list[tuple[Movable, list[float | int]]], + Sequence[Movable | float | int], Field( description="List of tuples (device, parameter). For independent \ trajectories, provide '[(movable1, [start1, stop1, num1]), (movable2, \ @@ -146,12 +147,9 @@ def num_grid_scan( axes by default (all axes but the first axis provided). Wraps bluesky.plans.grid_scan(det, *args, snake_axes, md=metadata). """ - # TODO: move to using Range spec and spec_scan when stable and tested at v1.0 - args, shape = _make_num_scan_args(params) - metadata = metadata or {} - metadata["shape"] = shape - - yield from bp.grid_scan(tuple(detectors), *args, snake_axes=snake_axes, md=metadata) + yield from bp.grid_scan( + tuple(detectors), *params, snake_axes=snake_axes, md=metadata + ) @validate_call(config={"arbitrary_types_allowed": True}) @@ -163,11 +161,11 @@ def num_rscan( ), ], params: Annotated[ - list[tuple[Movable, list[float | int]]], + Sequence[Movable | float | int], Field( description="List of tuples (device, parameter). For concurrent \ - trajectories, provide '[(movable1, [start1, stop1]), (movable2, [start2, \ - stop2]), ... , (movableN, [startN, stopN])]'." + trajectories, provide '[movable1, start1, stop1, movable2, start2, stop2, \ + ... , movableN, startN, stopN]'." ), ], num: int | None = None, @@ -178,12 +176,10 @@ def num_rscan( The scan is defined by number of points along scan trajector(y/ies). Wraps bluesky.plans.rel_scan(det, *args, num, md=metadata). """ - # TODO: move to using Range spec and spec_scan when stable and tested at v1.0 - args, shape = _make_num_scan_args(params, num) metadata = metadata or {} - metadata["shape"] = shape + metadata["shape"] = (num,) - yield from bp.rel_scan(tuple(detectors), *args, num=num, md=metadata) + yield from bp.rel_scan(tuple(detectors), *params, num=num, md=metadata) @validate_call(config={"arbitrary_types_allowed": True}) @@ -195,7 +191,7 @@ def num_grid_rscan( ), ], params: Annotated[ - list[tuple[Movable, list[float | int]]], + Sequence[Movable | float | int], Field( description="List of tuples (device, parameter). For independent \ trajectories, provide '[(movable1, [start1, stop1, num1]), (movable2, \ @@ -211,30 +207,20 @@ def num_grid_rscan( axes by default (all axes but the first axis provided). Wraps bluesky.plans.rel_grid_scan(det, *args, snake_axes, md=metadata). """ - # TODO: move to using Range spec and spec_scan when stable and tested at v1.0 - args, shape = _make_num_scan_args(params) - metadata = metadata or {} - metadata["shape"] = shape - yield from bp.rel_grid_scan( - tuple(detectors), *args, snake_axes=snake_axes, md=metadata + tuple(detectors), *params, snake_axes=snake_axes, md=metadata ) -def _make_list_scan_args(params: list[tuple[Movable, list[float | int]]], grid: bool): - shape = [] - args = [] +def _make_list_scan_shape( + params: Sequence[Movable | list[float | int]], +) -> tuple[int, ...]: for param in params: - shape.append(len(param[1])) - args.append(param[0]) - args.append(param[1]) - - if not grid: - shape = list(set(shape)) - if len(shape) > 1: - raise ValueError("Lists of motor positions are not equal in length.") - - return args, shape + # List arg must all be same size. If list missing or not same size, this will + # be validated by bp.list_scan. + if isinstance(param, list): + return (len(param),) + return () @validate_call(config={"arbitrary_types_allowed": True}) @@ -246,7 +232,7 @@ def list_scan( ), ], params: Annotated[ - list[tuple[Movable, list[float | int]]], + list[Movable | list[float | int]], Field( description="List of tuples (device, positions). For concurrent \ trajectories, provide '[(movable1, [point1, point2, ...]), (movable2, \ @@ -261,11 +247,11 @@ def list_scan( The scan is defined by providing a list of points for each scan trajectory. Wraps bluesky.plans.list_scan(det, *args, md=metadata). """ - args, shape = _make_list_scan_args(params=params, grid=False) metadata = metadata or {} - metadata["shape"] = shape + metadata["shape"] = _make_list_scan_shape(params) - yield from bp.list_scan(tuple(detectors), *args, md=metadata) + # Not sure about this one + yield from bp.list_scan(tuple(detectors), *tuple(params), md=metadata) # type: ignore @validate_call(config={"arbitrary_types_allowed": True}) @@ -277,7 +263,7 @@ def list_grid_scan( ), ], params: Annotated[ - list[tuple[Movable, list[float | int]]], + Sequence[Movable | list[float | int]], Field( description="List of tuples (device, positions). For independent \ trajectories, provide '[(movable1, [point1, point2, ...]), (movable2, \ @@ -293,12 +279,15 @@ def list_grid_scan( all fast axes by default (all axes but the first axis provided). Wraps bluesky.plans.list_grid_scan(det, *args, md=metadata). """ - args, shape = _make_list_scan_args(params=params, grid=True) metadata = metadata or {} - metadata["shape"] = shape + shape = [] + for param in params: + if isinstance(param, list): + shape.append(len(param)) + metadata["shape"] = tuple(shape) yield from bp.list_grid_scan( - tuple(detectors), *args, snake_axes=snake_axes, md=metadata + tuple(detectors), *params, snake_axes=snake_axes, md=metadata ) @@ -311,7 +300,7 @@ def list_rscan( ), ], params: Annotated[ - list[tuple[Movable, list[float | int]]], + Sequence[Movable | list[float | int]], Field( description="List of tuples (device, positions). For concurrent \ trajectories, provide '[(movable1, [point1, point2, ...]), (movable2, \ @@ -326,11 +315,9 @@ def list_rscan( The scan is defined by providing a list of points for each scan trajectory. Wraps bluesky.plans.rel_list_scan(det, *args, md=metadata). """ - args, shape = _make_list_scan_args(params=params, grid=False) metadata = metadata or {} - metadata["shape"] = shape - - yield from bp.rel_list_scan(tuple(detectors), *args, md=metadata) + metadata["shape"] = _make_list_scan_shape(params) + yield from bp.rel_list_scan(tuple(detectors), *params, md=metadata) @validate_call(config={"arbitrary_types_allowed": True}) @@ -342,7 +329,7 @@ def list_grid_rscan( ), ], params: Annotated[ - list[tuple[Movable, list[float | int]]], + Sequence[Movable | list[float | int]], Field( description="List of tuples (device, positions). For independent \ trajectories, provide '[(movable1, [point1, point2, ...]), (movable2, \ @@ -358,16 +345,16 @@ def list_grid_rscan( all fast axes by default (all axes but the first axis provided). Wraps bluesky.plans.rel_list_grid_scan(det, *args, md=metadata). """ - args, shape = _make_list_scan_args(params=params, grid=True) metadata = metadata or {} - metadata["shape"] = shape - + metadata["shape"] = _make_list_scan_shape(params) yield from bp.rel_list_grid_scan( - tuple(detectors), *args, snake_axes=snake_axes, md=metadata + tuple(detectors), *params, snake_axes=snake_axes, md=metadata ) -def _round_list_elements(stepped_list, params) -> list[float]: +def _round_list_elements( + stepped_list: list[float | int], params: list[float | int] +) -> list[float | int]: decimals = [Decimal(str(param)) for param in params] exponents = [d.as_tuple().exponent for d in decimals] decimal_places = [-exponent for exponent in exponents] # type: ignore @@ -375,7 +362,9 @@ def _round_list_elements(stepped_list, params) -> list[float]: return np.round(stepped_list, decimals=max_decimal_places).tolist() -def _make_stepped_list_step(start: float, stop: float, step: float) -> list: +def _make_stepped_list_step( + start: float, stop: float, step: float +) -> list[float | int]: if start == stop: raise ValueError( f"Start ({start}) and stop ({stop}) values cannot be the same." @@ -392,7 +381,9 @@ def _make_stepped_list_step(start: float, stop: float, step: float) -> list: return rounded_stepped_list -def _make_stepped_list_num(start, step, num) -> list: +def _make_stepped_list_num(start: float, step: float, num: int) -> list[float | int]: + if num <= 0: + raise ValueError() stepped_list = [start + (n * step) for n in range(num)] rounded_stepped_list = _round_list_elements( stepped_list=stepped_list, params=[start, step] @@ -400,49 +391,108 @@ def _make_stepped_list_num(start, step, num) -> list: return rounded_stepped_list -def _make_step_scan_args( - params: list[tuple[Movable, list[float | int]]], grid: bool -) -> tuple[list[Any], list[float]]: - args = [] +def _make_step_scan_args_and_shape( + params: Sequence[Movable | float | int], grid: bool +) -> tuple[Sequence[Movable | list[float | int]], tuple[int, ...]]: + + args: list[Movable | list[float | int]] = [] shape = [] - stepped_list_length = None - - first_movable_param, *additional_movable_params = params - if len(first_movable_param[1]) == 3: - start, stop, step = first_movable_param[1] - stepped_list = _make_stepped_list_step(start, stop, step) - stepped_list_length = len(stepped_list) - args.append(first_movable_param[0]) - args.append(stepped_list) - shape.append(stepped_list_length) - else: - raise ValueError( - f"You provided {len(first_movable_param[1])} parameters for {first_movable_param[0]}, rather than 3." - ) - for param in additional_movable_params: - if grid: - if len(param[1]) == 3: - start, stop, step = param[1] - stepped_list = _make_stepped_list_step(start, stop, step) - args.append(param[0]) - args.append(stepped_list) - shape.append(len(stepped_list)) - else: - raise ValueError( - f"You provided {len(param[1])} parameters for {param[0]}, rather than 3." - ) - else: - if len(param[1]) == 2: - start, step = param[1] - stepped_list = _make_stepped_list_num(start, step, stepped_list_length) - args.append(param[0]) - args.append(stepped_list) - else: - raise ValueError( - f"You provided {len(param[1])} parameters {param[0]}, rather than 2." - ) - - return args, shape + stepped_list_length = 0 + + try: + for i, param in enumerate(params): + if isinstance(param, Movable): + movable = param + start = params[i + 1] + if not isinstance(start, (float, int)): + raise ValueError( + f"You provided movable {movable} with no start, stop, step." + ) + + stop = params[i + 2] + if not isinstance(stop, (float, int)): + raise ValueError( + f"You provided movable {movable} with start value {start} but no stop and step." + ) + + step = params[i + 3] + if not isinstance(step, (float, int)): + raise ValueError( + f"You provided movable {movable} with start value {start}, stop value {stop} but no step value." + ) + + movable_values = _make_stepped_list_step(start, stop, step) + stepped_list_length = len(movable_values) + args.append(movable) + args.append(movable_values) + + if not grid: + break + + if not grid: + # Skip first 4 values as already done them. + for i, param in enumerate(params[4:]): + if isinstance(param, Movable): + movable = param + start = params[i + 1] + if not isinstance(start, (float, int)): + raise ValueError( + f"You provided movable {movable} with no start, stop, step." + ) + + step = params[i + 2] + if not isinstance(step, (float, int)): + raise ValueError( + f"You provided movable {movable} with start value {start}, stop value {step} but no step value." + ) + + movable_values = _make_stepped_list_num( + start, step, stepped_list_length + ) + args.append(movable) + args.append(movable_values) + # shape.append(len(values)) + except IndexError as e: + raise ValueError("Incorrect parameters provided.") from e + + return args, tuple(shape) + + # first_movable_param, *additional_movable_params = params + # if len(first_movable_param[1]) == 3: + # start, stop, step = first_movable_param[1] + # stepped_list = _make_stepped_list_step(start, stop, step) + # stepped_list_length = len(stepped_list) + # args.append(first_movable_param[0]) + # args.append(stepped_list) + # shape.append(stepped_list_length) + # else: + # raise ValueError( + # f"You provided {len(first_movable_param[1])} parameters for {first_movable_param[0]}, rather than 3." + # ) + # for param in additional_movable_params: + # if grid: + # if len(param[1]) == 3: + # start, stop, step = param[1] + # stepped_list = _make_stepped_list_step(start, stop, step) + # args.append(param[0]) + # args.append(stepped_list) + # shape.append(len(stepped_list)) + # else: + # raise ValueError( + # f"You provided {len(param[1])} parameters for {param[0]}, rather than 3." + # ) + # else: + # if len(param[1]) == 2: + # start, step = param[1] + # stepped_list = _make_stepped_list_num(start, step, stepped_list_length) + # args.append(param[0]) + # args.append(stepped_list) + # else: + # raise ValueError( + # f"You provided {len(param[1])} parameters {param[0]}, rather than 2." + # ) + + # return args, shape @validate_call(config={"arbitrary_types_allowed": True}) @@ -454,7 +504,7 @@ def step_scan( ), ], params: Annotated[ - list[tuple[Movable, list[float | int]]], + Sequence[Movable | float | int], Field( description="List of tuples (device, parameter). For concurrent \ trajectories, provide '[(movable1, [start1, stop1, step1]), (movable2, \ @@ -469,11 +519,12 @@ def step_scan( bluesky.plans.list_scan(det, *args, md=metadata). """ # TODO: move to using Linspace spec and spec_scan when stable and tested at v1.0 - args, shape = _make_step_scan_args(params, grid=False) + args, shape = _make_step_scan_args_and_shape(params, grid=False) + print(args) metadata = metadata or {} metadata["shape"] = shape - yield from bp.list_scan(tuple(detectors), *args, md=metadata) + yield from bp.list_scan(tuple(detectors), *tuple(args), md=metadata) @validate_call(config={"arbitrary_types_allowed": True}) @@ -485,7 +536,7 @@ def step_grid_scan( ), ], params: Annotated[ - list[tuple[Movable, list[float | int]]], + Sequence[Movable | float | int], Field( description="List of tuples (device, parameter). For independent \ trajectories, provide '[(movable1, [start1, stop1, step1]), (movable2, \ @@ -502,7 +553,7 @@ def step_grid_scan( default (all axes but the first axis provided). """ # TODO: move to using Linspace spec and spec_scan when stable and tested at v1.0 - args, shape = _make_step_scan_args(params, grid=True) + args, shape = _make_step_scan_args_and_shape(params, grid=True) metadata = metadata or {} metadata["shape"] = shape @@ -520,7 +571,7 @@ def step_rscan( ), ], params: Annotated[ - list[tuple[Movable, list[float | int]]], + Sequence[Movable | float | int], Field( description="List of tuples (device, parameter). For concurrent \ trajectories, provide '[(movable1, [start1, stop1, step1]), (movable2, \ @@ -535,7 +586,7 @@ def step_rscan( bluesky.plans.rel_list_scan(det, *args, md=metadata). """ # TODO: move to using Linspace spec and spec_scan when stable and tested at v1.0 - args, shape = _make_step_scan_args(params, grid=False) + args, shape = _make_step_scan_args_and_shape(params, grid=False) metadata = metadata or {} metadata["shape"] = shape @@ -551,7 +602,7 @@ def step_grid_rscan( ), ], params: Annotated[ - list[tuple[Movable, list[float | int]]], + Sequence[Movable | float | int], Field( description="List of tuples (device, parameter). For independent \ trajectories, provide '[(movable1, [start1, stop1, step1]), (movable2, \ @@ -568,7 +619,7 @@ def step_grid_rscan( default (all axes but the first axis provided). """ # TODO: move to using Linspace spec and spec_scan when stable and tested at v1.0 - args, shape = _make_step_scan_args(params, grid=True) + args, shape = _make_step_scan_args_and_shape(params, grid=True) metadata = metadata or {} metadata["shape"] = shape diff --git a/tests/plans/test_wrapped.py b/tests/plans/test_wrapped.py index 43bf8b21ec3..4335e3799bf 100644 --- a/tests/plans/test_wrapped.py +++ b/tests/plans/test_wrapped.py @@ -20,9 +20,7 @@ from dodal.devices.motors import Motor from dodal.plans.wrapped import ( - _make_list_scan_args, - _make_num_scan_args, - _make_step_scan_args, + _make_step_scan_args_and_shape, _make_stepped_list_num, _make_stepped_list_step, _round_list_elements, @@ -187,26 +185,26 @@ def test_count_with_no_detector_raise_error(run_engine: RunEngine): run_engine(count([])) -@pytest.mark.parametrize( - "x_list, y_list, num, final_shape, final_length", - ( - [[0.0, 1.1], [2.2, 3.3], 3, [3], 6], - [[0.0, 1.1, 2], [2.2, 3.3, 3], None, [2, 3], 8], - ), -) -def test_make_num_scan_args( - x_axis: Motor, - y_axis: Motor, - x_list: list[float | int], - y_list: list[float | int], - num: int | None, - final_shape: list[int], - final_length: int, -): - args, shape = _make_num_scan_args([(x_axis, x_list), (y_axis, y_list)], num=num) - assert shape == final_shape - assert len(args) == final_length - assert args[0] == x_axis +# @pytest.mark.parametrize( +# "x_list, y_list, num, final_shape, final_length", +# ( +# [[0.0, 1.1], [2.2, 3.3], 3, [3], 6], +# [[0.0, 1.1, 2], [2.2, 3.3, 3], None, [2, 3], 8], +# ), +# ) +# def test_make_num_scan_args( +# x_axis: Motor, +# y_axis: Motor, +# x_list: list[float | int], +# y_list: list[float | int], +# num: int | None, +# final_shape: list[int], +# final_length: int, +# ): +# # args, shape = _make_num_scan_args([(x_axis, x_list), (y_axis, y_list)], num=num) +# assert shape == final_shape +# assert len(args) == final_length +# assert args[0] == x_axis def _assert_emitted( @@ -254,8 +252,9 @@ def test_num_scan_with_one_axis( x_list: list[float | int], num: int, ): - run_engine(num_scan(detectors=detectors, params=[(x_axis, x_list)], num=num)) + run_engine(num_scan(detectors=detectors, params=[x_axis, *x_list], num=num)) _assert_emitted(run_engine_documents, detectors, num) + print(run_engine_documents["start"][0]["shape"]) @pytest.mark.parametrize( @@ -274,7 +273,7 @@ def test_num_scan_with_two_axes( run_engine( num_scan( detectors=detectors, - params=[(x_axis, x_list), (y_axis, y_list)], + params=[x_axis, *x_list, y_axis, *y_list], num=num, ) ) @@ -285,7 +284,7 @@ def test_num_scan_fails_when_given_wrong_number_of_params( run_engine: RunEngine, detectors: Sequence[StandardDetector], x_axis: Motor ): with pytest.raises(ValueError): - run_engine(num_scan(detectors=detectors, params=[(x_axis, [-1, 1, 5])], num=5)) + run_engine(num_scan(detectors=detectors, params=[x_axis, -1, 1, 5], num=5)) @pytest.mark.parametrize( @@ -305,7 +304,7 @@ def test_num_scan_fails_when_given_bad_info( run_engine( num_scan( detectors=detectors, - params=[(x_axis, x_list), (y_axis, y_list)], + params=[x_axis, *x_list, y_axis, *y_list], num=num, ) ) @@ -327,7 +326,7 @@ def test_num_grid_scan( run_engine( num_grid_scan( detectors=detectors, - params=[(x_axis, x_list), (y_axis, y_list)], + params=[x_axis, *x_list, y_axis, *y_list], ) ) _assert_emitted(run_engine_documents, detectors, num) @@ -349,7 +348,7 @@ def test_num_grid_scan_when_not_snaking( run_engine( num_grid_scan( detectors=detectors, - params=[(x_axis, x_list), (y_axis, y_list)], + params=[x_axis, *x_list, y_axis, *y_list], snake_axes=False, ) ) @@ -364,9 +363,7 @@ def test_num_grid_scan_fails_when_given_wrong_number_of_params( ): with pytest.raises(ValueError): run_engine( - num_grid_scan( - detectors=detectors, params=[(x_axis, [0, 1.1, 2]), (y_axis, [1.1])] - ) + num_grid_scan(detectors=detectors, params=[x_axis, 0, 1.1, 2, y_axis, 1.1]) ) @@ -385,7 +382,7 @@ def test_num_scan_fails_when_asked_to_snake_slow_axis( run_engine( num_grid_scan( detectors=detectors, - params=[(x_axis, x_list), (y_axis, y_list)], + params=[x_axis, *x_list, y_axis, *y_list], snake_axes=[x_axis], ) ) @@ -400,7 +397,7 @@ def test_num_rscan( x_list: list[float | int], num: int, ): - run_engine(num_rscan(detectors=detectors, params=[(x_axis, x_list)], num=num)) + run_engine(num_rscan(detectors=detectors, params=[x_axis, *x_list], num=num)) _assert_emitted(run_engine_documents, detectors, num) @@ -419,7 +416,7 @@ def test_num_rscan_with_two_axes( ): run_engine( num_rscan( - detectors=detectors, params=[(x_axis, x_list), (y_axis, y_list)], num=num + detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list], num=num ) ) _assert_emitted(run_engine_documents, detectors, num) @@ -441,7 +438,7 @@ def test_num_rscan_fails_when_given_bad_info( run_engine( num_rscan( detectors=detectors, - params=[(x_axis, x_list), (y_axis, y_list)], + params=[x_axis, *x_list, y_axis, *y_list], num=num, ) ) @@ -463,7 +460,7 @@ def test_num_grid_rscan( run_engine( num_grid_rscan( detectors=detectors, - params=[(x_axis, x_list), (y_axis, y_list)], + params=[x_axis, *x_list, y_axis, *y_list], ) ) _assert_emitted(run_engine_documents, detectors, num) @@ -485,7 +482,7 @@ def test_num_grid_rscan_when_not_snaking( run_engine( num_grid_rscan( detectors=detectors, - params=[(x_axis, x_list), (y_axis, y_list)], + params=[x_axis, *x_list, y_axis, *y_list], snake_axes=False, ) ) @@ -507,7 +504,7 @@ def test_num_grid_rscan_fails_when_asked_to_snake_slow_axis( run_engine( num_grid_rscan( detectors=detectors, - params=[(x_axis, x_list), (y_axis, y_list)], + params=[x_axis, *x_list, y_axis, *y_list], snake_axes=[x_axis], ) ) @@ -526,8 +523,8 @@ def test_make_list_scan_args( final_shape: list, final_length: int, ): - args, shape = _make_list_scan_args( - params=[(x_axis, x_list), (y_axis, y_list)], grid=grid + args, shape = _make_step_scan_args_and_shape( + params=[x_axis, *x_list, y_axis, *y_list], grid=grid ) assert len(args) == final_length assert shape == final_shape @@ -538,8 +535,8 @@ def test_make_list_scan_args_fails_when_lists_are_different_lengths( y_axis: Motor, ): with pytest.raises(ValueError): - _make_list_scan_args( - params=[(x_axis, [0, 1, 2]), (y_axis, [0, 1, 2, 3])], grid=False + _make_step_scan_args_and_shape( + params=[x_axis, 0, 1, 2, y_axis, 0, 1, 2, 3], grid=False ) @@ -553,7 +550,7 @@ def test_list_scan( ): num = int(len(x_list)) - run_engine(list_scan(detectors=detectors, params=[(x_axis, x_list)])) + run_engine(list_scan(detectors=detectors, params=[x_axis, x_list])) _assert_emitted(run_engine_documents, detectors, num) @@ -574,9 +571,7 @@ def test_list_scan_with_two_axes( y_list: list, ): num = int(len(x_list)) - run_engine( - list_scan(detectors=detectors, params=[(x_axis, x_list), (y_axis, y_list)]) - ) + run_engine(list_scan(detectors=detectors, params=[x_axis, x_list, y_axis, y_list])) _assert_emitted(run_engine_documents, detectors, num) @@ -590,7 +585,7 @@ def test_list_scan_fails_with_differnt_list_lengths( run_engine( list_scan( detectors=detectors, - params=[(x_axis, [1, 2, 3, 4, 5]), (y_axis, [1, 2, 3, 4])], + params=[x_axis, [1, 2, 3, 4, 5], y_axis, [1, 2, 3, 4]], ) ) @@ -613,7 +608,7 @@ def test_list_grid_scan( ): num = int(len(x_list) * len(y_list)) run_engine( - list_grid_scan(detectors=detectors, params=[(x_axis, x_list), (y_axis, y_list)]) + list_grid_scan(detectors=detectors, params=[x_axis, x_list, y_axis, y_list]) ) _assert_emitted(run_engine_documents, detectors, num) @@ -627,7 +622,7 @@ def test_list_rscan( x_list: list, ): num = int(len(x_list)) - run_engine(list_rscan(detectors=detectors, params=[(x_axis, x_list)])) + run_engine(list_rscan(detectors=detectors, params=[x_axis, x_list])) _assert_emitted(run_engine_documents, detectors, num) @@ -649,9 +644,7 @@ def test_list_rscan_with_two_axes( ): num = int(len(x_list)) - run_engine( - list_rscan(detectors=detectors, params=[(x_axis, x_list), (y_axis, y_list)]) - ) + run_engine(list_rscan(detectors=detectors, params=[x_axis, x_list, y_axis, y_list])) _assert_emitted(run_engine_documents, detectors, num) @@ -665,7 +658,7 @@ def test_list_rscan_fails_with_differnt_list_lengths( run_engine( list_rscan( detectors=detectors, - params=[(x_axis, [1, 2, 3, 4, 5]), (y_axis, [1, 2, 3, 4])], + params=[x_axis, [1, 2, 3, 4, 5], y_axis, [1, 2, 3, 4]], ) ) @@ -689,9 +682,7 @@ def test_list_grid_rscan( num = int(len(x_list) * len(y_list)) run_engine( - list_grid_rscan( - detectors=detectors, params=[(x_axis, x_list), (y_axis, y_list)] - ) + list_grid_rscan(detectors=detectors, params=[x_axis, x_list, y_axis, y_list]) ) _assert_emitted(run_engine_documents, detectors, num) @@ -756,10 +747,25 @@ def test_make_stepped_list_fails_when_given_equal_start_and_stop_values(): @pytest.mark.parametrize( "x_list, y_list, grid, final_shape, final_length", ( - [[0, 1, 0.25], [0, 0.1], False, [5], 4], - [[0, 1, 0.25], [0, 1, 0.2], True, [5, 6], 4], - [[0, -1, -0.25], [0, -0.1], False, [5], 4], - [[0, -1, -0.25], [0, -1, -0.2], True, [5, 6], 4], + [[0, 1, 0.25], [0, 0.1], False, (5,), 4], + [ + [0, 1, 0.25], + [0, 1, 0.2], + True, + (5, 6), + 4, + ], + [[0, -1, -0.25], [0, -0.1], False, (5), 4], + [ + [0, -1, -0.25], + [0, -1, -0.2], + True, + ( + 5, + 6, + ), + 4, + ], ), ) def test_make_step_scan_args( @@ -771,13 +777,14 @@ def test_make_step_scan_args( final_shape: list, final_length: int, ): - args, shape = _make_step_scan_args( - params=[(x_axis, x_list), (y_axis, y_list)], grid=grid + args, shape = _make_step_scan_args_and_shape( + params=[x_axis, *x_list, y_axis, *y_list], grid=grid ) assert shape == final_shape - assert len(args) == final_length - assert args[0] == x_axis - assert args[2] == y_axis + assert args == [x_axis] + # assert len(args) == final_length + # assert args[0] == x_axis + # assert args[2] == y_axis @pytest.mark.parametrize( @@ -791,16 +798,16 @@ def test_make_step_scan_args( ) def test_make_step_scan_args_fails_when_given_incorrect_number_of_parameters( x_axis: Motor, - x_list: list, + x_list: list[float | int], y_axis: Motor, - y_list: list, + y_list: list[float | int], z_axis: Motor, - z_list: list, + z_list: list[float | int], grid: bool, ): with pytest.raises(ValueError): - _make_step_scan_args( - params=[(x_axis, x_list), (y_axis, y_list), (z_axis, z_list)], grid=grid + _make_step_scan_args_and_shape( + params=[x_axis, *x_list, y_axis, *y_list, z_axis, *z_list], grid=grid ) @@ -812,10 +819,10 @@ def test_step_scan( run_engine_documents: Mapping[str, list[dict]], detectors: Sequence[StandardDetector], x_axis: Motor, - x_list: list, + x_list: list[float | int], num, ): - run_engine(step_scan(detectors=detectors, params=[(x_axis, x_list)])) + run_engine(step_scan(detectors=detectors, params=[x_axis, *x_list])) _assert_emitted(run_engine_documents, detectors, num) @@ -832,13 +839,13 @@ def test_step_scan_with_multiple_axes( run_engine_documents: Mapping[str, list[dict]], detectors: Sequence[StandardDetector], x_axis: Motor, - x_list: list, + x_list: list[float | int], y_axis: Motor, - y_list: list, + y_list: list[float | int], num, ): run_engine( - step_scan(detectors=detectors, params=[(x_axis, x_list), (y_axis, y_list)]) + step_scan(detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list]) ) _assert_emitted(run_engine_documents, detectors, num) @@ -856,13 +863,13 @@ def test_step_grid_scan( run_engine_documents: Mapping[str, list[dict]], detectors: Sequence[StandardDetector], x_axis: Motor, - x_list: list, + x_list: list[float | int], y_axis: Motor, - y_list: list, + y_list: list[float | int], num, ): run_engine( - step_grid_scan(detectors=detectors, params=[(x_axis, x_list), (y_axis, y_list)]) + step_grid_scan(detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list]) ) _assert_emitted(run_engine_documents, detectors, num) @@ -879,15 +886,15 @@ def test_step_grid_scan_when_not_snaking( run_engine_documents: Mapping[str, list[dict]], detectors: Sequence[StandardDetector], x_axis: Motor, - x_list: list, + x_list: list[float | int], y_axis: Motor, - y_list: list, + y_list: list[float | int], num, ): run_engine( step_grid_scan( detectors=detectors, - params=[(x_axis, x_list), (y_axis, y_list)], + params=[x_axis, *x_list, y_axis, *y_list], snake_axes=False, ) ) @@ -901,14 +908,14 @@ def test_step_grid_scan_fails_when_given_incorrect_number_of_params( run_engine: RunEngine, detectors: Sequence[StandardDetector], x_axis: Motor, - x_list: list, + x_list: list[float | int], y_axis: Motor, - y_list: list, + y_list: list[float | int], ): with pytest.raises(ValueError): run_engine( step_grid_scan( - detectors=detectors, params=[(x_axis, x_list), (y_axis, y_list)] + detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list] ) ) @@ -921,10 +928,10 @@ def test_step_rscan( run_engine_documents: Mapping[str, list[dict]], detectors: Sequence[StandardDetector], x_axis: Motor, - x_list: list, - num, + x_list: list[float | int], + num: int, ): - run_engine(step_rscan(detectors=detectors, params=[(x_axis, x_list)])) + run_engine(step_rscan(detectors=detectors, params=[x_axis, *x_list])) _assert_emitted(run_engine_documents, detectors, num) @@ -941,13 +948,13 @@ def test_step_rscan_with_multiple_axes( run_engine_documents: Mapping[str, list[dict]], detectors: Sequence[StandardDetector], x_axis: Motor, - x_list: list, + x_list: list[float | int], y_axis: Motor, - y_list: list, - num, + y_list: list[float | int], + num: int, ): run_engine( - step_rscan(detectors=detectors, params=[(x_axis, x_list), (y_axis, y_list)]) + step_rscan(detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list]) ) _assert_emitted(run_engine_documents, detectors, num) @@ -965,15 +972,13 @@ def test_step_grid_rscan( run_engine_documents: Mapping[str, list[dict]], detectors: Sequence[StandardDetector], x_axis: Motor, - x_list: list, + x_list: list[float | int], y_axis: Motor, - y_list: list, - num, + y_list: list[float | int], + num: int, ): run_engine( - step_grid_rscan( - detectors=detectors, params=[(x_axis, x_list), (y_axis, y_list)] - ) + step_grid_rscan(detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list]) ) _assert_emitted(run_engine_documents, detectors, num) @@ -990,15 +995,15 @@ def test_step_grid_rscan_when_not_snaking( run_engine_documents: Mapping[str, list[dict]], detectors: Sequence[StandardDetector], x_axis: Motor, - x_list: list, + x_list: list[float | int], y_axis: Motor, - y_list: list, - num, + y_list: list[float | int], + num: int, ): run_engine( step_grid_rscan( detectors=detectors, - params=[(x_axis, x_list), (y_axis, y_list)], + params=[x_axis, *x_list, y_axis, *y_list], snake_axes=False, ) ) @@ -1012,13 +1017,13 @@ def test_step_grid_rscan_fails_when_given_incorrect_number_of_params( run_engine: RunEngine, detectors: Sequence[StandardDetector], x_axis: Motor, - x_list: list, + x_list: list[float | int], y_axis: Motor, - y_list: list, + y_list: list[float | int], ): with pytest.raises(ValueError): run_engine( step_grid_rscan( - detectors=detectors, params=[(x_axis, x_list), (y_axis, y_list)] + detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list] ) ) From 9ad324aa4fde7a53ab7f4344fa808443ba7fc88b Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Thu, 7 May 2026 10:07:38 +0000 Subject: [PATCH 02/11] Update step scan logic, tests pass --- src/dodal/plans/wrapped.py | 214 ++++++++++++++---------------------- tests/plans/test_wrapped.py | 51 +-------- 2 files changed, 88 insertions(+), 177 deletions(-) diff --git a/src/dodal/plans/wrapped.py b/src/dodal/plans/wrapped.py index 043f7ad922f..714506a2682 100644 --- a/src/dodal/plans/wrapped.py +++ b/src/dodal/plans/wrapped.py @@ -1,6 +1,6 @@ from collections.abc import Sequence from decimal import Decimal -from typing import Annotated, Any +from typing import Annotated, Any, TypeVar import bluesky.plans as bp import numpy as np @@ -25,6 +25,8 @@ - Limits and metadata (e.g. units). """ +T = TypeVar("T") + @attach_data_session_metadata_decorator() @validate_call(config={"arbitrary_types_allowed": True}) @@ -62,36 +64,6 @@ def count( yield from bp.count(tuple(detectors), num, delay=delay, md=metadata) -# def _make_num_scan_args( -# params: Sequence[Movable | float | int], num: int | None = None -# ) -> list[float]: -# # shape = [] -# # if num: -# # shape = [num] -# # for param in params: -# # if len(param[1]) == 2: -# # pass -# # else: -# # raise ValueError("You must provide 'start stop' for each motor.") -# # else: -# # for param in params: -# # if len(param[1]) == 3: -# # shape.append(param[1][-1]) -# # else: -# # raise ValueError( -# # "You must provide 'start stop num' for each motor in a grid scan." -# # ) - -# # args = [] -# # for param in params: -# # args.append(param[0]) -# # args.extend(param[1]) -# for param in params: -# if isinstance(param, Movable): - -# return args, shape - - @validate_call(config={"arbitrary_types_allowed": True}) def num_scan( detectors: Annotated[ @@ -392,108 +364,85 @@ def _make_stepped_list_num(start: float, step: float, num: int) -> list[float | def _make_step_scan_args_and_shape( - params: Sequence[Movable | float | int], grid: bool -) -> tuple[Sequence[Movable | list[float | int]], tuple[int, ...]]: - - args: list[Movable | list[float | int]] = [] - shape = [] - stepped_list_length = 0 - - try: - for i, param in enumerate(params): - if isinstance(param, Movable): - movable = param - start = params[i + 1] - if not isinstance(start, (float, int)): - raise ValueError( - f"You provided movable {movable} with no start, stop, step." - ) - - stop = params[i + 2] - if not isinstance(stop, (float, int)): - raise ValueError( - f"You provided movable {movable} with start value {start} but no stop and step." - ) - - step = params[i + 3] - if not isinstance(step, (float, int)): - raise ValueError( - f"You provided movable {movable} with start value {start}, stop value {stop} but no step value." - ) - - movable_values = _make_stepped_list_step(start, stop, step) - stepped_list_length = len(movable_values) - args.append(movable) - args.append(movable_values) - - if not grid: - break - - if not grid: - # Skip first 4 values as already done them. - for i, param in enumerate(params[4:]): - if isinstance(param, Movable): - movable = param - start = params[i + 1] - if not isinstance(start, (float, int)): - raise ValueError( - f"You provided movable {movable} with no start, stop, step." - ) - - step = params[i + 2] - if not isinstance(step, (float, int)): - raise ValueError( - f"You provided movable {movable} with start value {start}, stop value {step} but no step value." - ) - - movable_values = _make_stepped_list_num( - start, step, stepped_list_length - ) - args.append(movable) - args.append(movable_values) - # shape.append(len(values)) - except IndexError as e: - raise ValueError("Incorrect parameters provided.") from e + params: Sequence[Movable | float | int], + grid: bool, +) -> tuple[list[Movable | list[float]], tuple[int, ...]]: + + def require( + value: object, + expected: type[T] | tuple[type, ...], + name: str, + ) -> T: + expected_tuple = expected if isinstance(expected, tuple) else (expected,) + if not isinstance(value, expected_tuple): + allowed = ", ".join(t.__name__ for t in expected_tuple) + raise ValueError( + f"Parameter {name} must be one of type ({allowed}), got {type(value).__name__}" + ) + return value # type: ignore[return-value] + + def parse_full_axis( + values: Sequence[Movable | float | int], + ) -> tuple[Movable, float, float, float]: + if len(values) != 4: + raise ValueError( + f"Full axis must be movable, start, stop, step. You provided {values}" + ) + movable = require(values[0], Movable, "movable") + start = require(values[1], (int, float), "start") + stop = require(values[2], (int, float), "stop") + step = require(values[3], (int, float), "step") + + return movable, start, stop, step + + def parse_relative_axis( + values: Sequence[Movable | float | int], + ) -> tuple[Movable, float, float]: + if len(values) != 3: + raise ValueError( + f"Relative axis must be movable, start, step. You provided {values}" + ) + movable = require(values[0], Movable, "movable") + start = require(values[1], (int, float), "start") + step = require(values[2], (int, float), "step") + + return movable, start, step + + if len(params) < 4: + raise ValueError("At least one axis must provide (movable, start, stop, step)") + + args: list[Movable | list[float]] = [] + shape: list[int] = [] + + # First axis defines scan length + movable, start, stop, step = parse_full_axis(params[:4]) + + values = _make_stepped_list_step(start, stop, step) + stepped_list_length = len(values) + + args.extend([movable, values]) + shape.append(stepped_list_length) + + remaining = params[4:] + + chunk_size = 4 if grid else 3 + + if len(remaining) % chunk_size != 0: + raise ValueError("Incorrect number of parameters for additional axes") + + for i in range(0, len(remaining), chunk_size): + chunk = remaining[i : i + chunk_size] + if grid: + movable, start, stop, step = parse_full_axis(chunk) + values = _make_stepped_list_step(start, stop, step) + shape.append(len(values)) + else: + movable, start, step = parse_relative_axis(chunk) + values = _make_stepped_list_num(start, step, stepped_list_length) + args.extend([movable, values]) return args, tuple(shape) - # first_movable_param, *additional_movable_params = params - # if len(first_movable_param[1]) == 3: - # start, stop, step = first_movable_param[1] - # stepped_list = _make_stepped_list_step(start, stop, step) - # stepped_list_length = len(stepped_list) - # args.append(first_movable_param[0]) - # args.append(stepped_list) - # shape.append(stepped_list_length) - # else: - # raise ValueError( - # f"You provided {len(first_movable_param[1])} parameters for {first_movable_param[0]}, rather than 3." - # ) - # for param in additional_movable_params: - # if grid: - # if len(param[1]) == 3: - # start, stop, step = param[1] - # stepped_list = _make_stepped_list_step(start, stop, step) - # args.append(param[0]) - # args.append(stepped_list) - # shape.append(len(stepped_list)) - # else: - # raise ValueError( - # f"You provided {len(param[1])} parameters for {param[0]}, rather than 3." - # ) - # else: - # if len(param[1]) == 2: - # start, step = param[1] - # stepped_list = _make_stepped_list_num(start, step, stepped_list_length) - # args.append(param[0]) - # args.append(stepped_list) - # else: - # raise ValueError( - # f"You provided {len(param[1])} parameters {param[0]}, rather than 2." - # ) - - # return args, shape - @validate_call(config={"arbitrary_types_allowed": True}) def step_scan( @@ -520,11 +469,10 @@ def step_scan( """ # TODO: move to using Linspace spec and spec_scan when stable and tested at v1.0 args, shape = _make_step_scan_args_and_shape(params, grid=False) - print(args) metadata = metadata or {} metadata["shape"] = shape - yield from bp.list_scan(tuple(detectors), *tuple(args), md=metadata) + yield from bp.list_scan(tuple(detectors), *tuple(args), md=metadata) # type: ignore @validate_call(config={"arbitrary_types_allowed": True}) @@ -623,6 +571,8 @@ def step_grid_rscan( metadata = metadata or {} metadata["shape"] = shape + print(args) + yield from bp.rel_list_grid_scan( tuple(detectors), *args, snake_axes=snake_axes, md=metadata ) diff --git a/tests/plans/test_wrapped.py b/tests/plans/test_wrapped.py index 4335e3799bf..3664a47181e 100644 --- a/tests/plans/test_wrapped.py +++ b/tests/plans/test_wrapped.py @@ -512,9 +512,12 @@ def test_num_grid_rscan_fails_when_asked_to_snake_slow_axis( @pytest.mark.parametrize( "x_list, y_list, grid, final_shape, final_length", - ([[0, 1, 2], [3, 4, 5], False, [3], 4], [[0, 1, 2], [3, 4, 5, 6], True, [3, 4], 4]), + ( + [[0, 10, 1], [0, 5], False, (11,), 4], + [[0, 10, 1], [0, 5, 1], True, (11, 6), 4], + ), ) -def test_make_list_scan_args( +def test_make_step_scan_args_and_shape( x_axis: Motor, x_list: list, y_axis: Motor, @@ -526,6 +529,7 @@ def test_make_list_scan_args( args, shape = _make_step_scan_args_and_shape( params=[x_axis, *x_list, y_axis, *y_list], grid=grid ) + print(args) assert len(args) == final_length assert shape == final_shape @@ -744,49 +748,6 @@ def test_make_stepped_list_fails_when_given_equal_start_and_stop_values(): _make_stepped_list_step(start=1.1, stop=1.1, step=0.25) -@pytest.mark.parametrize( - "x_list, y_list, grid, final_shape, final_length", - ( - [[0, 1, 0.25], [0, 0.1], False, (5,), 4], - [ - [0, 1, 0.25], - [0, 1, 0.2], - True, - (5, 6), - 4, - ], - [[0, -1, -0.25], [0, -0.1], False, (5), 4], - [ - [0, -1, -0.25], - [0, -1, -0.2], - True, - ( - 5, - 6, - ), - 4, - ], - ), -) -def test_make_step_scan_args( - x_axis: Motor, - x_list: list[float], - y_axis: Motor, - y_list: list[float], - grid: bool, - final_shape: list, - final_length: int, -): - args, shape = _make_step_scan_args_and_shape( - params=[x_axis, *x_list, y_axis, *y_list], grid=grid - ) - assert shape == final_shape - assert args == [x_axis] - # assert len(args) == final_length - # assert args[0] == x_axis - # assert args[2] == y_axis - - @pytest.mark.parametrize( "x_list, y_list, z_list, grid", ( From 8a476aa37e7b3e921ca0844a09f6f869926a8cf5 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Thu, 7 May 2026 10:22:27 +0000 Subject: [PATCH 03/11] Add error message to _make_stepped_list_num --- src/dodal/plans/wrapped.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dodal/plans/wrapped.py b/src/dodal/plans/wrapped.py index 714506a2682..332b228099b 100644 --- a/src/dodal/plans/wrapped.py +++ b/src/dodal/plans/wrapped.py @@ -355,7 +355,7 @@ def _make_stepped_list_step( def _make_stepped_list_num(start: float, step: float, num: int) -> list[float | int]: if num <= 0: - raise ValueError() + raise ValueError("Number of steps must be greater than zero.") stepped_list = [start + (n * step) for n in range(num)] rounded_stepped_list = _round_list_elements( stepped_list=stepped_list, params=[start, step] @@ -428,7 +428,7 @@ def parse_relative_axis( chunk_size = 4 if grid else 3 if len(remaining) % chunk_size != 0: - raise ValueError("Incorrect number of parameters for additional axes") + raise ValueError("Incorrect number of parameters for additional axes.") for i in range(0, len(remaining), chunk_size): chunk = remaining[i : i + chunk_size] From 7a948f81478e38158d0566452a8b133a57f4ff89 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Fri, 8 May 2026 12:54:12 +0000 Subject: [PATCH 04/11] Update doc strings and _make_step_scan_args_and_shape logic --- src/dodal/plans/wrapped.py | 176 ++++++++++++++++++------------------- 1 file changed, 85 insertions(+), 91 deletions(-) diff --git a/src/dodal/plans/wrapped.py b/src/dodal/plans/wrapped.py index 332b228099b..5ff8cf5b218 100644 --- a/src/dodal/plans/wrapped.py +++ b/src/dodal/plans/wrapped.py @@ -237,9 +237,9 @@ def list_grid_scan( params: Annotated[ Sequence[Movable | list[float | int]], Field( - description="List of tuples (device, positions). For independent \ - trajectories, provide '[(movable1, [point1, point2, ...]), (movable2, \ - [point1, point2, ...]), ... , (movableN, [point1, point2, ...])]'." + description="For independent trajectories, provide" + "'[movable1, [point1, point2, ...], movable2, [point1, point2, ...], ..., " + "movableN, [point1, point2, ...]]'." ), ], snake_axes: bool = True, # Currently specifying axes to snake is not supported @@ -274,9 +274,9 @@ def list_rscan( params: Annotated[ Sequence[Movable | list[float | int]], Field( - description="List of tuples (device, positions). For concurrent \ - trajectories, provide '[(movable1, [point1, point2, ...]), (movable2, \ - [point1, point2, ...]), ... , (movableN, [point1, point2, ...])]'. Number \ + description="For concurrent trajectories, provide " + "'[movable1, [point1, point2, ...], movable2, [point1, point2, ...], ..., " + "movableN, [point1, point2, ...]]'. Number \ of points for each movable must be equal." ), ], @@ -303,9 +303,9 @@ def list_grid_rscan( params: Annotated[ Sequence[Movable | list[float | int]], Field( - description="List of tuples (device, positions). For independent \ - trajectories, provide '[(movable1, [point1, point2, ...]), (movable2, \ - [point1, point2, ...]), ... , (movableN, [point1, point2, ...])]'." + description="For independent trajectories, provide " + "'[movable1, [point1, point2, ...], movable2, [point1, point2, ...], ... , " + "movableN, [point1, point2, ...]]'." ), ], snake_axes: bool = True, # Currently specifying axes to snake is not supported @@ -363,85 +363,81 @@ def _make_stepped_list_num(start: float, step: float, num: int) -> list[float | return rounded_stepped_list -def _make_step_scan_args_and_shape( - params: Sequence[Movable | float | int], - grid: bool, -) -> tuple[list[Movable | list[float]], tuple[int, ...]]: - - def require( - value: object, - expected: type[T] | tuple[type, ...], - name: str, - ) -> T: - expected_tuple = expected if isinstance(expected, tuple) else (expected,) - if not isinstance(value, expected_tuple): - allowed = ", ".join(t.__name__ for t in expected_tuple) - raise ValueError( - f"Parameter {name} must be one of type ({allowed}), got {type(value).__name__}" - ) - return value # type: ignore[return-value] - - def parse_full_axis( - values: Sequence[Movable | float | int], - ) -> tuple[Movable, float, float, float]: - if len(values) != 4: - raise ValueError( - f"Full axis must be movable, start, stop, step. You provided {values}" - ) - movable = require(values[0], Movable, "movable") - start = require(values[1], (int, float), "start") - stop = require(values[2], (int, float), "stop") - step = require(values[3], (int, float), "step") - - return movable, start, stop, step - - def parse_relative_axis( - values: Sequence[Movable | float | int], - ) -> tuple[Movable, float, float]: - if len(values) != 3: - raise ValueError( - f"Relative axis must be movable, start, step. You provided {values}" - ) - movable = require(values[0], Movable, "movable") - start = require(values[1], (int, float), "start") - step = require(values[2], (int, float), "step") - - return movable, start, step - - if len(params) < 4: - raise ValueError("At least one axis must provide (movable, start, stop, step)") - - args: list[Movable | list[float]] = [] - shape: list[int] = [] +def require( + value: object, + expected: type[T] | tuple[type[T], ...], + name: str, +) -> T: + expected_tuple = expected if isinstance(expected, tuple) else (expected,) + if not isinstance(value, expected_tuple): + allowed = ", ".join(t.__name__ for t in expected_tuple) + raise ValueError( + f"Parameter {name} must be one of type ({allowed}), got {type(value).__name__}" + ) + return value # type: ignore[return-value] - # First axis defines scan length - movable, start, stop, step = parse_full_axis(params[:4]) - values = _make_stepped_list_step(start, stop, step) - stepped_list_length = len(values) +def parse_full_axis( + values: Sequence[Movable | float | int], +) -> tuple[Movable, float, float, float]: + if len(values) != 4: + raise ValueError( + f"The axis must be movable, start, stop, step. You provided {values}" + ) + movable = require(values[0], Movable, "movable") + start = require(values[1], (int, float), "start") + stop = require(values[2], (int, float), "stop") + step = require(values[3], (int, float), "step") + return movable, start, stop, step - args.extend([movable, values]) - shape.append(stepped_list_length) - remaining = params[4:] +def parse_relative_axis( + values: Sequence[Movable | float | int], +) -> tuple[Movable, float, float]: + if len(values) != 3: + raise ValueError( + f"The axis must be movable, start, step. You provided {', '.join(map(str, values))}" + ) + movable = require(values[0], Movable, "movable") + start = require(values[1], (int, float), "start") + step = require(values[2], (int, float), "step") + return movable, start, step - chunk_size = 4 if grid else 3 - if len(remaining) % chunk_size != 0: - raise ValueError("Incorrect number of parameters for additional axes.") +def _make_step_scan_args_and_shape( + params: Sequence[Movable | float | int], grid: bool +) -> tuple[list[Movable | list[float]], tuple[int, ...]]: + """Convert [x, 1, 4, 1, ...] to [x, [1, 2, 3, 4], ...].""" + list_of_movable_with_values: list[list[Movable | float | int]] = [] + current_list: list[Movable | float | int] = [] + for param in params: + if isinstance(param, Movable): + current_list = [param] + list_of_movable_with_values.append(current_list) + elif isinstance(param, (int, float)): + current_list.append(param) + else: + raise ValueError( + f'Scan syntax only takes movables or numbers for params. You provided "{param}".' + ) - for i in range(0, len(remaining), chunk_size): - chunk = remaining[i : i + chunk_size] - if grid: - movable, start, stop, step = parse_full_axis(chunk) - values = _make_stepped_list_step(start, stop, step) - shape.append(len(values)) + step_scan_args: list[Movable | list[float]] = [] + shape = [] + first_axis = True + for movable_with_values in list_of_movable_with_values: + if first_axis or grid: + movable, start, stop, step = parse_full_axis(movable_with_values) + movable_values = _make_stepped_list_step(start, stop, step) + shape.append(len(movable_values)) + first_axis = False else: - movable, start, step = parse_relative_axis(chunk) - values = _make_stepped_list_num(start, step, stepped_list_length) - args.extend([movable, values]) + # If not a grid scan, expects start, stop for all other axes and use the + # first axis shape for the number of steps. + movable, start, step = parse_relative_axis(movable_with_values) + movable_values = _make_stepped_list_num(start, step, shape[0]) + step_scan_args.extend([movable, movable_values]) - return args, tuple(shape) + return step_scan_args, tuple(shape) @validate_call(config={"arbitrary_types_allowed": True}) @@ -455,9 +451,9 @@ def step_scan( params: Annotated[ Sequence[Movable | float | int], Field( - description="List of tuples (device, parameter). For concurrent \ - trajectories, provide '[(movable1, [start1, stop1, step1]), (movable2, \ - [start2, step2]), ... , (movableN, [startN, stepN])]'." + description="For concurrent trajectories, provide " + "'[movable1, start1, stop1, step1, movable2, start2, step2, ... , " + "movableN, startN, stepN]'." ), ], metadata: dict[str, Any] | None = None, @@ -487,8 +483,8 @@ def step_grid_scan( Sequence[Movable | float | int], Field( description="List of tuples (device, parameter). For independent \ - trajectories, provide '[(movable1, [start1, stop1, step1]), (movable2, \ - [start2, stop2, step2]), ... , (movableN, [startN, stopN, stepN])]'." + trajectories, provide '[movable1, start1, stop1, step1, movable2, start2, " + "stop2, step2, ... , movableN, startN, stopN, stepN]'." ), ], snake_axes: bool = True, # Currently specifying axes to snake is not supported @@ -521,9 +517,9 @@ def step_rscan( params: Annotated[ Sequence[Movable | float | int], Field( - description="List of tuples (device, parameter). For concurrent \ - trajectories, provide '[(movable1, [start1, stop1, step1]), (movable2, \ - [start2, step2]), ... , (movableN, [startN, stepN])]'." + description="For concurrent trajectories, provide " + "'[movable1, start1, stop1, step1, movable2, start2, step2, ... , " + "movableN, startN, stepN]'." ), ], metadata: dict[str, Any] | None = None, @@ -553,8 +549,8 @@ def step_grid_rscan( Sequence[Movable | float | int], Field( description="List of tuples (device, parameter). For independent \ - trajectories, provide '[(movable1, [start1, stop1, step1]), (movable2, \ - [start2, stop2, step2]), ... , (movableN, [startN, stopN, stepN])]'." + trajectories, provide '[movable1, start1, stop1, step1, movable2, \ + start2, stop2, step2, ... , movableN, startN, stopN, stepN]'." ), ], snake_axes: bool = True, # Currently specifying axes to snake is not supported @@ -571,8 +567,6 @@ def step_grid_rscan( metadata = metadata or {} metadata["shape"] = shape - print(args) - yield from bp.rel_list_grid_scan( tuple(detectors), *args, snake_axes=snake_axes, md=metadata ) From 4473ec5e10a4bf878b327818b5b9c97dcb4da2dc Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Fri, 8 May 2026 12:54:24 +0000 Subject: [PATCH 05/11] Add stronger tests for invalid arguments --- tests/plans/test_wrapped.py | 89 ++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 25 deletions(-) diff --git a/tests/plans/test_wrapped.py b/tests/plans/test_wrapped.py index 3664a47181e..140295c2587 100644 --- a/tests/plans/test_wrapped.py +++ b/tests/plans/test_wrapped.py @@ -185,28 +185,6 @@ def test_count_with_no_detector_raise_error(run_engine: RunEngine): run_engine(count([])) -# @pytest.mark.parametrize( -# "x_list, y_list, num, final_shape, final_length", -# ( -# [[0.0, 1.1], [2.2, 3.3], 3, [3], 6], -# [[0.0, 1.1, 2], [2.2, 3.3, 3], None, [2, 3], 8], -# ), -# ) -# def test_make_num_scan_args( -# x_axis: Motor, -# y_axis: Motor, -# x_list: list[float | int], -# y_list: list[float | int], -# num: int | None, -# final_shape: list[int], -# final_length: int, -# ): -# # args, shape = _make_num_scan_args([(x_axis, x_list), (y_axis, y_list)], num=num) -# assert shape == final_shape -# assert len(args) == final_length -# assert args[0] == x_axis - - def _assert_emitted( run_engine_documents: Mapping[str, list[dict]], detectors: Sequence[StandardDetector], @@ -971,10 +949,30 @@ def test_step_grid_rscan_when_not_snaking( _assert_emitted(run_engine_documents, detectors, num) +@pytest.mark.parametrize("x_list, y_list", ([[0, 1], [0, 1, 0.1]], [[0], [0, 1, 0.1]])) +def test_step_grid_scan_fails_when_given_wrong_number_of_args_for_first_axes( + run_engine: RunEngine, + detectors: Sequence[StandardDetector], + x_axis: Motor, + x_list: list[float | int], + y_axis: Motor, + y_list: list[float | int], +): + with pytest.raises( + ValueError, + match="The axis must be movable, start, stop, step.", + ): + run_engine( + step_grid_scan( + detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list] + ) + ) + + @pytest.mark.parametrize( "x_list, y_list", ([[0, 1, 0.1], [0, 1, 0.1, 1]], [[0, 1, 0.1], [0]]) ) -def test_step_grid_rscan_fails_when_given_incorrect_number_of_params( +def test_step_grid_scan_fails_when_given_wrong_number_of_args_for_second_axes( run_engine: RunEngine, detectors: Sequence[StandardDetector], x_axis: Motor, @@ -982,9 +980,50 @@ def test_step_grid_rscan_fails_when_given_incorrect_number_of_params( y_axis: Motor, y_list: list[float | int], ): - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match="The axis must be movable, start, stop, step.", + ): run_engine( - step_grid_rscan( + step_grid_scan( detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list] ) ) + + +@pytest.mark.parametrize( + "x_list, y_list", ([[0, 1, 0.1], [0, 1, 0.1]], [[0, 1, 0.1], [0]]) +) +def test_step_scan_fails_when_given_wrong_number_of_args_for_second_axes( + run_engine: RunEngine, + detectors: Sequence[StandardDetector], + x_axis: Motor, + x_list: list[float | int], + y_axis: Motor, + y_list: list[float | int], +): + with pytest.raises( + ValueError, + match="The axis must be movable, start, stop.", + ): + run_engine( + step_scan(detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list]) + ) + + +def test_make_step_scan_args_and_shape_fails_with_invalid_type_args( + x_axis: Motor, + y_axis: Motor, +): + with pytest.raises( + ValueError, + match="Scan syntax only takes movables or numbers for params.", + ): + _make_step_scan_args_and_shape( + [x_axis, 1, "3", 1, y_axis, 1, "4", 1], # type: ignore + grid=True, + ) + _make_step_scan_args_and_shape( + [x_axis, 1, "3", 1, y_axis, 1, "4"], # type: ignore + grid=False, + ) From d93ba1a75126da32807bd0e35b1ce80ae7e2c707 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Fri, 8 May 2026 13:44:22 +0000 Subject: [PATCH 06/11] Added additional test to check for scan shape --- src/dodal/plans/wrapped.py | 25 +++--- tests/plans/test_wrapped.py | 150 +++++++++++++++++------------------- 2 files changed, 82 insertions(+), 93 deletions(-) diff --git a/src/dodal/plans/wrapped.py b/src/dodal/plans/wrapped.py index 5ff8cf5b218..9e789638ba2 100644 --- a/src/dodal/plans/wrapped.py +++ b/src/dodal/plans/wrapped.py @@ -185,14 +185,19 @@ def num_grid_rscan( def _make_list_scan_shape( - params: Sequence[Movable | list[float | int]], + params: Sequence[Movable | list[float | int]], grid: bool ) -> tuple[int, ...]: + shape = [] for param in params: # List arg must all be same size. If list missing or not same size, this will # be validated by bp.list_scan. if isinstance(param, list): - return (len(param),) - return () + dim = len(param) + shape.append(dim) + if not grid: + break + + return tuple(shape) @validate_call(config={"arbitrary_types_allowed": True}) @@ -220,7 +225,7 @@ def list_scan( Wraps bluesky.plans.list_scan(det, *args, md=metadata). """ metadata = metadata or {} - metadata["shape"] = _make_list_scan_shape(params) + metadata["shape"] = _make_list_scan_shape(params, grid=False) # Not sure about this one yield from bp.list_scan(tuple(detectors), *tuple(params), md=metadata) # type: ignore @@ -252,11 +257,7 @@ def list_grid_scan( bluesky.plans.list_grid_scan(det, *args, md=metadata). """ metadata = metadata or {} - shape = [] - for param in params: - if isinstance(param, list): - shape.append(len(param)) - metadata["shape"] = tuple(shape) + metadata["shape"] = _make_list_scan_shape(params, grid=False) yield from bp.list_grid_scan( tuple(detectors), *params, snake_axes=snake_axes, md=metadata @@ -288,7 +289,7 @@ def list_rscan( Wraps bluesky.plans.rel_list_scan(det, *args, md=metadata). """ metadata = metadata or {} - metadata["shape"] = _make_list_scan_shape(params) + metadata["shape"] = _make_list_scan_shape(params, grid=False) yield from bp.rel_list_scan(tuple(detectors), *params, md=metadata) @@ -318,7 +319,7 @@ def list_grid_rscan( bluesky.plans.rel_list_grid_scan(det, *args, md=metadata). """ metadata = metadata or {} - metadata["shape"] = _make_list_scan_shape(params) + metadata["shape"] = _make_list_scan_shape(params, grid=True) yield from bp.rel_list_grid_scan( tuple(detectors), *params, snake_axes=snake_axes, md=metadata ) @@ -396,7 +397,7 @@ def parse_relative_axis( ) -> tuple[Movable, float, float]: if len(values) != 3: raise ValueError( - f"The axis must be movable, start, step. You provided {', '.join(map(str, values))}" + f"The axis must be movable, start, stop. You provided {', '.join(map(str, values))}" ) movable = require(values[0], Movable, "movable") start = require(values[1], (int, float), "start") diff --git a/tests/plans/test_wrapped.py b/tests/plans/test_wrapped.py index 140295c2587..836c6f314da 100644 --- a/tests/plans/test_wrapped.py +++ b/tests/plans/test_wrapped.py @@ -40,6 +40,13 @@ ) +def assert_expected_shape( + run_engine_documents: Mapping[str, list[dict]], expected_shape: tuple[int, ...] +) -> None: + start = run_engine_documents["start"][0] + assert start["shape"] == expected_shape + + def test_count_delay_validation(det: StandardDetector, run_engine: RunEngine): args: dict[float | Sequence[float], str] = { # type: ignore # List wrong length @@ -59,7 +66,6 @@ def test_count_delay_validation(det: StandardDetector, run_engine: RunEngine): for delay, reason in args.items(): with pytest.raises((ValidationError, AssertionError), match=reason): run_engine(count([det], num=3, delay=delay)) - print(delay) def test_count_detectors_validation(run_engine: RunEngine): @@ -97,10 +103,10 @@ def test_count_plan_produces_expected_start_document( start = run_engine_documents.get("start") assert start and len(start) == 1 run_start = cast(RunStart, start[0]) - assert run_start.get("shape") == shape assert (hints := run_start.get("hints")) and ( hints.get("dimensions") == [(("time",), "primary")] ) + assert_expected_shape(run_engine_documents, (num,)) @pytest.mark.parametrize("num, length", ([1, 1], [3, 3])) @@ -232,7 +238,7 @@ def test_num_scan_with_one_axis( ): run_engine(num_scan(detectors=detectors, params=[x_axis, *x_list], num=num)) _assert_emitted(run_engine_documents, detectors, num) - print(run_engine_documents["start"][0]["shape"]) + assert_expected_shape(run_engine_documents, (num,)) @pytest.mark.parametrize( @@ -256,6 +262,7 @@ def test_num_scan_with_two_axes( ) ) _assert_emitted(run_engine_documents, detectors, num) + assert_expected_shape(run_engine_documents, (num,)) def test_num_scan_fails_when_given_wrong_number_of_params( @@ -289,16 +296,16 @@ def test_num_scan_fails_when_given_bad_info( @pytest.mark.parametrize( - "x_list, y_list", ([[-1.1, 1.1, 5], [2.2, -2.2, 3]], [[0, 1.1, 5], [2.2, 3.3, 5]]) + "x_list, y_list", ([(-1.1, 1.1, 5), (2.2, -2.2, 3)], [(0, 1.1, 5), (2.2, 3.3, 5)]) ) def test_num_grid_scan( run_engine: RunEngine, run_engine_documents: Mapping[str, list[dict]], detectors: Sequence[StandardDetector], x_axis: Motor, - x_list: list[float | int], + x_list: tuple[float, float, int], y_axis: Motor, - y_list: list[float | int], + y_list: tuple[float, float, int], ): num = int(x_list[-1] * y_list[-1]) run_engine( @@ -308,19 +315,20 @@ def test_num_grid_scan( ) ) _assert_emitted(run_engine_documents, detectors, num) + assert_expected_shape(run_engine_documents, (x_list[2], y_list[2])) @pytest.mark.parametrize( - "x_list, y_list", ([[-1.1, 1.1, 5], [2.2, -2.2, 3]], [[0, 1.1, 5], [2.2, 3.3, 5]]) + "x_list, y_list", ([(-1.1, 1.1, 5), (2.2, -2.2, 3)], [(0, 1.1, 5), (2.2, 3.3, 5)]) ) def test_num_grid_scan_when_not_snaking( run_engine: RunEngine, run_engine_documents: Mapping[str, list[dict]], detectors: Sequence[StandardDetector], x_axis: Motor, - x_list: list[float | int], + x_list: tuple[float, float, int], y_axis: Motor, - y_list: list[float | int], + y_list: tuple[float, float, int], ): num = int(x_list[-1] * y_list[-1]) run_engine( @@ -331,6 +339,7 @@ def test_num_grid_scan_when_not_snaking( ) ) _assert_emitted(run_engine_documents, detectors, num) + assert_expected_shape(run_engine_documents, (x_list[2], y_list[2])) def test_num_grid_scan_fails_when_given_wrong_number_of_params( @@ -377,6 +386,7 @@ def test_num_rscan( ): run_engine(num_rscan(detectors=detectors, params=[x_axis, *x_list], num=num)) _assert_emitted(run_engine_documents, detectors, num) + assert_expected_shape(run_engine_documents, (num,)) @pytest.mark.parametrize( @@ -398,6 +408,7 @@ def test_num_rscan_with_two_axes( ) ) _assert_emitted(run_engine_documents, detectors, num) + assert_expected_shape(run_engine_documents, (num,)) @pytest.mark.parametrize( @@ -423,16 +434,16 @@ def test_num_rscan_fails_when_given_bad_info( @pytest.mark.parametrize( - "x_list, y_list", ([[-1.1, 1.1, 5], [2.2, -2.2, 3]], [[0, 1.1, 5], [2.2, 3.3, 5]]) + "x_list, y_list", ([(-1.1, 1.1, 5), (2.2, -2.2, 3)], [(0, 1.1, 5), (2.2, 3.3, 5)]) ) def test_num_grid_rscan( run_engine: RunEngine, run_engine_documents: Mapping[str, list[dict]], detectors: Sequence[StandardDetector], x_axis: Motor, - x_list: list[float | int], + x_list: tuple[float, float, int], y_axis: Motor, - y_list: list[float | int], + y_list: tuple[float, float, int], ): num = int(x_list[-1] * y_list[-1]) run_engine( @@ -442,19 +453,20 @@ def test_num_grid_rscan( ) ) _assert_emitted(run_engine_documents, detectors, num) + assert_expected_shape(run_engine_documents, (x_list[2], y_list[2])) @pytest.mark.parametrize( - "x_list, y_list", ([[-1.1, 1.1, 5], [2.2, -2.2, 3]], [[0, 1.1, 5], [2.2, 3.3, 5]]) + "x_list, y_list", ([(-1.1, 1.1, 5), (2.2, -2.2, 3)], [(0, 1.1, 5), (2.2, 3.3, 5)]) ) def test_num_grid_rscan_when_not_snaking( run_engine: RunEngine, run_engine_documents: Mapping[str, list[dict]], detectors: Sequence[StandardDetector], x_axis: Motor, - x_list: list[float | int], + x_list: tuple[float, float, int], y_axis: Motor, - y_list: list[float | int], + y_list: tuple[float, float, int], ): num = int(x_list[-1] * y_list[-1]) run_engine( @@ -465,6 +477,7 @@ def test_num_grid_rscan_when_not_snaking( ) ) _assert_emitted(run_engine_documents, detectors, num) + assert_expected_shape(run_engine_documents, (x_list[2], y_list[2])) @pytest.mark.parametrize( @@ -507,7 +520,6 @@ def test_make_step_scan_args_and_shape( args, shape = _make_step_scan_args_and_shape( params=[x_axis, *x_list, y_axis, *y_list], grid=grid ) - print(args) assert len(args) == final_length assert shape == final_shape @@ -530,10 +542,10 @@ def test_list_scan( x_axis: Motor, x_list: list, ): - num = int(len(x_list)) - + num = len(x_list) run_engine(list_scan(detectors=detectors, params=[x_axis, x_list])) _assert_emitted(run_engine_documents, detectors, num) + assert_expected_shape(run_engine_documents, (num,)) @pytest.mark.parametrize( @@ -555,6 +567,7 @@ def test_list_scan_with_two_axes( num = int(len(x_list)) run_engine(list_scan(detectors=detectors, params=[x_axis, x_list, y_axis, y_list])) _assert_emitted(run_engine_documents, detectors, num) + assert_expected_shape(run_engine_documents, (num,)) def test_list_scan_fails_with_differnt_list_lengths( @@ -584,15 +597,16 @@ def test_list_grid_scan( run_engine_documents: Mapping[str, list[dict]], detectors: Sequence[StandardDetector], x_axis: Motor, - x_list: list, + x_list: list[float | int], y_axis: Motor, - y_list: list, + y_list: list[float | int], ): num = int(len(x_list) * len(y_list)) run_engine( list_grid_scan(detectors=detectors, params=[x_axis, x_list, y_axis, y_list]) ) _assert_emitted(run_engine_documents, detectors, num) + assert_expected_shape(run_engine_documents, (len(x_list), len(y_list))) @pytest.mark.parametrize("x_list", ([0, 1, 2, 3], [1.1, 2.2, 3.3])) @@ -606,6 +620,7 @@ def test_list_rscan( num = int(len(x_list)) run_engine(list_rscan(detectors=detectors, params=[x_axis, x_list])) _assert_emitted(run_engine_documents, detectors, num) + assert_expected_shape(run_engine_documents, (len(x_list),)) @pytest.mark.parametrize( @@ -628,6 +643,7 @@ def test_list_rscan_with_two_axes( run_engine(list_rscan(detectors=detectors, params=[x_axis, x_list, y_axis, y_list])) _assert_emitted(run_engine_documents, detectors, num) + assert_expected_shape(run_engine_documents, (num,)) def test_list_rscan_fails_with_differnt_list_lengths( @@ -667,6 +683,7 @@ def test_list_grid_rscan( list_grid_rscan(detectors=detectors, params=[x_axis, x_list, y_axis, y_list]) ) _assert_emitted(run_engine_documents, detectors, num) + assert_expected_shape(run_engine_documents, (len(x_list), len(y_list))) @pytest.mark.parametrize( @@ -763,6 +780,7 @@ def test_step_scan( ): run_engine(step_scan(detectors=detectors, params=[x_axis, *x_list])) _assert_emitted(run_engine_documents, detectors, num) + assert_expected_shape(run_engine_documents, (num,)) @pytest.mark.parametrize( @@ -787,14 +805,18 @@ def test_step_scan_with_multiple_axes( step_scan(detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list]) ) _assert_emitted(run_engine_documents, detectors, num) + assert_expected_shape(run_engine_documents, (num,)) @pytest.mark.parametrize( - "x_list, y_list, num", + "x_list, expected_num_x, y_list, expected_num_y, snake", ( - [[0, 1, 0.25], [0, 2, 0.5], 25], - [[-1, 1, 0.25], [1, -1, -0.5], 45], - [[0, 10, 2.5], [0, -10, -2.5], 25], + [[0, 1, 0.25], 5, [0, 2, 0.5], 5, True], + [[0, 1, 0.25], 5, [0, 2, 0.5], 5, False], + [[-1, 1, 0.25], 9, [1, -1, -0.5], 5, True], + [[-1, 1, 0.25], 9, [1, -1, -0.5], 5, False], + [[0, 10, 2.5], 5, [0, -10, -2.5], 5, True], + [[0, 10, 2.5], 5, [0, -10, -2.5], 5, False], ), ) def test_step_grid_scan( @@ -803,41 +825,21 @@ def test_step_grid_scan( detectors: Sequence[StandardDetector], x_axis: Motor, x_list: list[float | int], + expected_num_x: int, y_axis: Motor, y_list: list[float | int], - num, -): - run_engine( - step_grid_scan(detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list]) - ) - _assert_emitted(run_engine_documents, detectors, num) - - -@pytest.mark.parametrize( - "x_list, y_list, num", - ( - [[0, 1, 0.25], [0, 2, 0.5], 25], - [[-1, 1, 0.25], [1, -1, -0.5], 45], - ), -) -def test_step_grid_scan_when_not_snaking( - run_engine: RunEngine, - run_engine_documents: Mapping[str, list[dict]], - detectors: Sequence[StandardDetector], - x_axis: Motor, - x_list: list[float | int], - y_axis: Motor, - y_list: list[float | int], - num, + expected_num_y: int, + snake: bool, ): run_engine( step_grid_scan( detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list], - snake_axes=False, + snake_axes=snake, ) ) - _assert_emitted(run_engine_documents, detectors, num) + _assert_emitted(run_engine_documents, detectors, expected_num_x * expected_num_y) + assert_expected_shape(run_engine_documents, (expected_num_x, expected_num_y)) @pytest.mark.parametrize( @@ -860,7 +862,8 @@ def test_step_grid_scan_fails_when_given_incorrect_number_of_params( @pytest.mark.parametrize( - "x_list, num", ([[0, 1, 0.1], 11], [[-1, 1, 0.1], 21], [[0, 10, 1], 11]) + "x_list, num", + ([[0, 1, 0.1], 11], [[-1, 1, 0.1], 21], [[0, 10, 1], 11]), ) def test_step_rscan( run_engine: RunEngine, @@ -872,6 +875,7 @@ def test_step_rscan( ): run_engine(step_rscan(detectors=detectors, params=[x_axis, *x_list])) _assert_emitted(run_engine_documents, detectors, num) + assert_expected_shape(run_engine_documents, (num,)) @pytest.mark.parametrize( @@ -896,14 +900,18 @@ def test_step_rscan_with_multiple_axes( step_rscan(detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list]) ) _assert_emitted(run_engine_documents, detectors, num) + assert_expected_shape(run_engine_documents, (num,)) @pytest.mark.parametrize( - "x_list, y_list, num", + "x_list, expected_num_x, y_list, expected_num_y, snake", ( - [[0, 1, 0.25], [0, 2, 0.5], 25], - [[-1, 1, 0.25], [1, -1, -0.5], 45], - [[0, 10, 2.5], [0, -10, -2.5], 25], + [[0, 1, 0.25], 5, [0, 2, 0.5], 5, True], + [[0, 1, 0.25], 5, [0, 2, 0.5], 5, False], + [[-1, 1, 0.25], 9, [1, -1, -0.5], 5, True], + [[-1, 1, 0.25], 9, [1, -1, -0.5], 5, False], + [[0, 10, 2.5], 5, [0, -10, -2.5], 5, True], + [[0, 10, 2.5], 5, [0, -10, -2.5], 5, False], ), ) def test_step_grid_rscan( @@ -912,41 +920,21 @@ def test_step_grid_rscan( detectors: Sequence[StandardDetector], x_axis: Motor, x_list: list[float | int], + expected_num_x: int, y_axis: Motor, y_list: list[float | int], - num: int, -): - run_engine( - step_grid_rscan(detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list]) - ) - _assert_emitted(run_engine_documents, detectors, num) - - -@pytest.mark.parametrize( - "x_list, y_list, num", - ( - [[0, 1, 0.25], [0, 2, 0.5], 25], - [[-1, 1, 0.25], [1, -1, -0.5], 45], - ), -) -def test_step_grid_rscan_when_not_snaking( - run_engine: RunEngine, - run_engine_documents: Mapping[str, list[dict]], - detectors: Sequence[StandardDetector], - x_axis: Motor, - x_list: list[float | int], - y_axis: Motor, - y_list: list[float | int], - num: int, + expected_num_y: int, + snake: bool, ): run_engine( step_grid_rscan( detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list], - snake_axes=False, + snake_axes=snake, ) ) - _assert_emitted(run_engine_documents, detectors, num) + _assert_emitted(run_engine_documents, detectors, expected_num_x * expected_num_y) + assert_expected_shape(run_engine_documents, (expected_num_x, expected_num_y)) @pytest.mark.parametrize("x_list, y_list", ([[0, 1], [0, 1, 0.1]], [[0], [0, 1, 0.1]])) From 650398268f0a644fe6ffd453430665d5d236b523 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Fri, 8 May 2026 13:52:21 +0000 Subject: [PATCH 07/11] Fix shape test --- src/dodal/plans/wrapped.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/dodal/plans/wrapped.py b/src/dodal/plans/wrapped.py index 9e789638ba2..ceabf0ae66d 100644 --- a/src/dodal/plans/wrapped.py +++ b/src/dodal/plans/wrapped.py @@ -11,15 +11,16 @@ from dodal.common import MsgGenerator from dodal.plan_stubs.data_session import attach_data_session_metadata_decorator -"""This module wraps plan(s) from bluesky.plans until required handling for them is -moved into bluesky or better handled in downstream services. - -Required decorators are installed on plan import +"""This module wraps plan(s) from bluesky.plans so they are comptaible with blueapi. +Required decorators are installed on plan import. https://github.com/DiamondLightSource/blueapi/issues/474 -Non-serialisable fields are ignored when they are optional +Non-serialisable fields are ignored when they are optional. https://github.com/DiamondLightSource/blueapi/issues/711 +Using *args in plans is currently not supported. +https://github.com/DiamondLightSource/blueapi/issues/1450 + We may also need other adjustments for UI purposes, e.g. - Forcing uniqueness or orderedness of Readables. - Limits and metadata (e.g. units). @@ -257,7 +258,7 @@ def list_grid_scan( bluesky.plans.list_grid_scan(det, *args, md=metadata). """ metadata = metadata or {} - metadata["shape"] = _make_list_scan_shape(params, grid=False) + metadata["shape"] = _make_list_scan_shape(params, grid=True) yield from bp.list_grid_scan( tuple(detectors), *params, snake_axes=snake_axes, md=metadata From ed2c7e2f24cbbdd49468d38c182fd8589713765c Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Fri, 8 May 2026 14:34:59 +0000 Subject: [PATCH 08/11] Add test for require --- src/dodal/plans/wrapped.py | 6 ++++-- tests/plans/test_wrapped.py | 38 ++++++++++++++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/dodal/plans/wrapped.py b/src/dodal/plans/wrapped.py index ceabf0ae66d..da93e0983a8 100644 --- a/src/dodal/plans/wrapped.py +++ b/src/dodal/plans/wrapped.py @@ -343,6 +343,8 @@ def _make_stepped_list_step( raise ValueError( f"Start ({start}) and stop ({stop}) values cannot be the same." ) + if step <= 0: + raise ValueError("Step size must be greater than zero.") if abs(step) > abs(stop - start): step = stop - start step = abs(step) * np.sign(stop - start) @@ -357,7 +359,7 @@ def _make_stepped_list_step( def _make_stepped_list_num(start: float, step: float, num: int) -> list[float | int]: if num <= 0: - raise ValueError("Number of steps must be greater than zero.") + raise ValueError("Number of points must be greater than zero.") stepped_list = [start + (n * step) for n in range(num)] rounded_stepped_list = _round_list_elements( stepped_list=stepped_list, params=[start, step] @@ -374,7 +376,7 @@ def require( if not isinstance(value, expected_tuple): allowed = ", ".join(t.__name__ for t in expected_tuple) raise ValueError( - f"Parameter {name} must be one of type ({allowed}), got {type(value).__name__}" + f"Parameter {name} must be one of type {allowed}, got {type(value).__name__}." ) return value # type: ignore[return-value] diff --git a/tests/plans/test_wrapped.py b/tests/plans/test_wrapped.py index 836c6f314da..901bda4519b 100644 --- a/tests/plans/test_wrapped.py +++ b/tests/plans/test_wrapped.py @@ -1,3 +1,4 @@ +import re from collections.abc import Mapping, Sequence from typing import cast @@ -33,6 +34,7 @@ num_grid_scan, num_rscan, num_scan, + require, step_grid_rscan, step_grid_scan, step_rscan, @@ -738,9 +740,27 @@ def test_make_stepped_list_num(start: float, step: float): assert stepped_list[10] == 0 -def test_make_stepped_list_fails_when_given_equal_start_and_stop_values(): - with pytest.raises(ValueError): - _make_stepped_list_step(start=1.1, stop=1.1, step=0.25) +def test_make_stepped_list_num_fails_when_num_is_zero(): + start = stop = 1.1 + with pytest.raises( + ValueError, + match=re.escape( + f"Start ({start}) and stop ({stop}) values cannot be the same." + ), + ): + _make_stepped_list_step(start=start, stop=stop, step=0.25) + + +def test_make_stepped_list_num_fails_when_given_equal_start_and_stop_values(): + with pytest.raises(ValueError, match="Number of points must be greater than zero."): + _make_stepped_list_num(start=1, step=0.1, num=0) + + +def test_require_raises_error_if_not_correct_type(): + with pytest.raises( + ValueError, match="Parameter test must be one of type str, got int." + ): + require(value=5, expected=str, name="test") @pytest.mark.parametrize( @@ -1015,3 +1035,15 @@ def test_make_step_scan_args_and_shape_fails_with_invalid_type_args( [x_axis, 1, "3", 1, y_axis, 1, "4"], # type: ignore grid=False, ) + + +def test_step_scan_fails_with_step_size_zero( + run_engine: RunEngine, + detectors: Sequence[StandardDetector], + x_axis: Motor, +): + with pytest.raises( + ValueError, + match="Step size must be greater than zero.", + ): + run_engine(step_scan(detectors=detectors, params=[x_axis, 1, 5, 0])) From 6ee9bfd07ea3fb3018d3e43ea754f98e6706726d Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Fri, 8 May 2026 15:09:53 +0000 Subject: [PATCH 09/11] Fix and optimise tests --- src/dodal/plans/wrapped.py | 10 ++++--- tests/plans/test_wrapped.py | 54 +++++++++++++------------------------ 2 files changed, 25 insertions(+), 39 deletions(-) diff --git a/src/dodal/plans/wrapped.py b/src/dodal/plans/wrapped.py index da93e0983a8..8d85520d8ef 100644 --- a/src/dodal/plans/wrapped.py +++ b/src/dodal/plans/wrapped.py @@ -343,8 +343,8 @@ def _make_stepped_list_step( raise ValueError( f"Start ({start}) and stop ({stop}) values cannot be the same." ) - if step <= 0: - raise ValueError("Step size must be greater than zero.") + if step == 0: + raise ValueError(f"Step size {step} cannot be zero.") if abs(step) > abs(stop - start): step = stop - start step = abs(step) * np.sign(stop - start) @@ -358,8 +358,10 @@ def _make_stepped_list_step( def _make_stepped_list_num(start: float, step: float, num: int) -> list[float | int]: - if num <= 0: - raise ValueError("Number of points must be greater than zero.") + if num == 0 or step == 0: + raise ValueError( + f"Number of points ({num}) and number of steps ({step}) cannot be zero." + ) stepped_list = [start + (n * step) for n in range(num)] rounded_stepped_list = _round_list_elements( stepped_list=stepped_list, params=[start, step] diff --git a/tests/plans/test_wrapped.py b/tests/plans/test_wrapped.py index 901bda4519b..3d169c40fe5 100644 --- a/tests/plans/test_wrapped.py +++ b/tests/plans/test_wrapped.py @@ -268,10 +268,10 @@ def test_num_scan_with_two_axes( def test_num_scan_fails_when_given_wrong_number_of_params( - run_engine: RunEngine, detectors: Sequence[StandardDetector], x_axis: Motor + run_engine: RunEngine, x_axis: Motor ): with pytest.raises(ValueError): - run_engine(num_scan(detectors=detectors, params=[x_axis, -1, 1, 5], num=5)) + run_engine(num_scan(detectors=[], params=[x_axis, -1, 1, 5], num=5)) @pytest.mark.parametrize( @@ -280,7 +280,6 @@ def test_num_scan_fails_when_given_wrong_number_of_params( ) def test_num_scan_fails_when_given_bad_info( run_engine: RunEngine, - detectors: Sequence[StandardDetector], x_axis: Motor, x_list: list[float | int], y_axis: Motor, @@ -290,7 +289,7 @@ def test_num_scan_fails_when_given_bad_info( with pytest.raises(ValueError): run_engine( num_scan( - detectors=detectors, + detectors=[], params=[x_axis, *x_list, y_axis, *y_list], num=num, ) @@ -346,14 +345,11 @@ def test_num_grid_scan_when_not_snaking( def test_num_grid_scan_fails_when_given_wrong_number_of_params( run_engine: RunEngine, - detectors: Sequence[StandardDetector], x_axis: Motor, y_axis: Motor, ): with pytest.raises(ValueError): - run_engine( - num_grid_scan(detectors=detectors, params=[x_axis, 0, 1.1, 2, y_axis, 1.1]) - ) + run_engine(num_grid_scan(detectors=[], params=[x_axis, 0, 1.1, 2, y_axis, 1.1])) @pytest.mark.parametrize( @@ -361,7 +357,6 @@ def test_num_grid_scan_fails_when_given_wrong_number_of_params( ) def test_num_scan_fails_when_asked_to_snake_slow_axis( run_engine: RunEngine, - detectors: Sequence[StandardDetector], x_axis: Motor, x_list: list[float | int], y_axis: Motor, @@ -370,7 +365,7 @@ def test_num_scan_fails_when_asked_to_snake_slow_axis( with pytest.raises(ValueError): run_engine( num_grid_scan( - detectors=detectors, + detectors=[], params=[x_axis, *x_list, y_axis, *y_list], snake_axes=[x_axis], ) @@ -418,7 +413,6 @@ def test_num_rscan_with_two_axes( ) def test_num_rscan_fails_when_given_bad_info( run_engine: RunEngine, - detectors: Sequence[StandardDetector], x_axis: Motor, x_list: list[float | int], y_axis: Motor, @@ -428,7 +422,7 @@ def test_num_rscan_fails_when_given_bad_info( with pytest.raises(ValueError): run_engine( num_rscan( - detectors=detectors, + detectors=[], params=[x_axis, *x_list, y_axis, *y_list], num=num, ) @@ -487,7 +481,6 @@ def test_num_grid_rscan_when_not_snaking( ) def test_num_grid_rscan_fails_when_asked_to_snake_slow_axis( run_engine: RunEngine, - detectors: Sequence[StandardDetector], x_axis: Motor, x_list: list[float | int], y_axis: Motor, @@ -496,7 +489,7 @@ def test_num_grid_rscan_fails_when_asked_to_snake_slow_axis( with pytest.raises(ValueError): run_engine( num_grid_rscan( - detectors=detectors, + detectors=[], params=[x_axis, *x_list, y_axis, *y_list], snake_axes=[x_axis], ) @@ -574,14 +567,13 @@ def test_list_scan_with_two_axes( def test_list_scan_fails_with_differnt_list_lengths( run_engine: RunEngine, - detectors: Sequence[StandardDetector], x_axis: Motor, y_axis: Motor, ): with pytest.raises(ValueError): run_engine( list_scan( - detectors=detectors, + detectors=[], params=[x_axis, [1, 2, 3, 4, 5], y_axis, [1, 2, 3, 4]], ) ) @@ -650,14 +642,13 @@ def test_list_rscan_with_two_axes( def test_list_rscan_fails_with_differnt_list_lengths( run_engine: RunEngine, - detectors: Sequence[StandardDetector], x_axis: Motor, y_axis: Motor, ): with pytest.raises(ValueError): run_engine( list_rscan( - detectors=detectors, + detectors=[], params=[x_axis, [1, 2, 3, 4, 5], y_axis, [1, 2, 3, 4]], ) ) @@ -752,8 +743,11 @@ def test_make_stepped_list_num_fails_when_num_is_zero(): def test_make_stepped_list_num_fails_when_given_equal_start_and_stop_values(): - with pytest.raises(ValueError, match="Number of points must be greater than zero."): - _make_stepped_list_num(start=1, step=0.1, num=0) + with pytest.raises( + ValueError, + match=re.escape("Number of points (0) and number of steps (0) cannot be zero."), + ): + _make_stepped_list_num(start=1, step=0, num=0) def test_require_raises_error_if_not_correct_type(): @@ -960,7 +954,6 @@ def test_step_grid_rscan( @pytest.mark.parametrize("x_list, y_list", ([[0, 1], [0, 1, 0.1]], [[0], [0, 1, 0.1]])) def test_step_grid_scan_fails_when_given_wrong_number_of_args_for_first_axes( run_engine: RunEngine, - detectors: Sequence[StandardDetector], x_axis: Motor, x_list: list[float | int], y_axis: Motor, @@ -971,9 +964,7 @@ def test_step_grid_scan_fails_when_given_wrong_number_of_args_for_first_axes( match="The axis must be movable, start, stop, step.", ): run_engine( - step_grid_scan( - detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list] - ) + step_grid_scan(detectors=[], params=[x_axis, *x_list, y_axis, *y_list]) ) @@ -982,7 +973,6 @@ def test_step_grid_scan_fails_when_given_wrong_number_of_args_for_first_axes( ) def test_step_grid_scan_fails_when_given_wrong_number_of_args_for_second_axes( run_engine: RunEngine, - detectors: Sequence[StandardDetector], x_axis: Motor, x_list: list[float | int], y_axis: Motor, @@ -993,9 +983,7 @@ def test_step_grid_scan_fails_when_given_wrong_number_of_args_for_second_axes( match="The axis must be movable, start, stop, step.", ): run_engine( - step_grid_scan( - detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list] - ) + step_grid_scan(detectors=[], params=[x_axis, *x_list, y_axis, *y_list]) ) @@ -1004,7 +992,6 @@ def test_step_grid_scan_fails_when_given_wrong_number_of_args_for_second_axes( ) def test_step_scan_fails_when_given_wrong_number_of_args_for_second_axes( run_engine: RunEngine, - detectors: Sequence[StandardDetector], x_axis: Motor, x_list: list[float | int], y_axis: Motor, @@ -1014,9 +1001,7 @@ def test_step_scan_fails_when_given_wrong_number_of_args_for_second_axes( ValueError, match="The axis must be movable, start, stop.", ): - run_engine( - step_scan(detectors=detectors, params=[x_axis, *x_list, y_axis, *y_list]) - ) + run_engine(step_scan(detectors=[], params=[x_axis, *x_list, y_axis, *y_list])) def test_make_step_scan_args_and_shape_fails_with_invalid_type_args( @@ -1039,11 +1024,10 @@ def test_make_step_scan_args_and_shape_fails_with_invalid_type_args( def test_step_scan_fails_with_step_size_zero( run_engine: RunEngine, - detectors: Sequence[StandardDetector], x_axis: Motor, ): with pytest.raises( ValueError, - match="Step size must be greater than zero.", + match="Step size 0 cannot be zero.", ): - run_engine(step_scan(detectors=detectors, params=[x_axis, 1, 5, 0])) + run_engine(step_scan(detectors=[], params=[x_axis, 1, 5, 0])) From 2c4587a719d390092f79fc6e54adcd9f89310670 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Fri, 8 May 2026 15:21:04 +0000 Subject: [PATCH 10/11] Update comments --- src/dodal/plans/wrapped.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/dodal/plans/wrapped.py b/src/dodal/plans/wrapped.py index 8d85520d8ef..fd2db2b28fb 100644 --- a/src/dodal/plans/wrapped.py +++ b/src/dodal/plans/wrapped.py @@ -11,7 +11,7 @@ from dodal.common import MsgGenerator from dodal.plan_stubs.data_session import attach_data_session_metadata_decorator -"""This module wraps plan(s) from bluesky.plans so they are comptaible with blueapi. +"""This module wraps plan(s) from bluesky.plans so they are compatible with blueapi. Required decorators are installed on plan import. https://github.com/DiamondLightSource/blueapi/issues/474 @@ -228,7 +228,6 @@ def list_scan( metadata = metadata or {} metadata["shape"] = _make_list_scan_shape(params, grid=False) - # Not sure about this one yield from bp.list_scan(tuple(detectors), *tuple(params), md=metadata) # type: ignore From 647d095cc3c08c1753ba848f019909f9eb484ae1 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Mon, 11 May 2026 07:43:39 +0000 Subject: [PATCH 11/11] Update doc strings and error msgs --- src/dodal/plans/wrapped.py | 9 +++++---- tests/plans/test_wrapped.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/dodal/plans/wrapped.py b/src/dodal/plans/wrapped.py index fd2db2b28fb..ef17aaad961 100644 --- a/src/dodal/plans/wrapped.py +++ b/src/dodal/plans/wrapped.py @@ -423,7 +423,8 @@ def _make_step_scan_args_and_shape( current_list.append(param) else: raise ValueError( - f'Scan syntax only takes movables or numbers for params. You provided "{param}".' + "Scan syntax only takes movables or numbers as parameters. " + f'You provided "{param}".' ) step_scan_args: list[Movable | list[float]] = [] @@ -553,9 +554,9 @@ def step_grid_rscan( params: Annotated[ Sequence[Movable | float | int], Field( - description="List of tuples (device, parameter). For independent \ - trajectories, provide '[movable1, start1, stop1, step1, movable2, \ - start2, stop2, step2, ... , movableN, startN, stopN, stepN]'." + description="For independent trajectories, provide " + "'[movable1, start1, stop1, step1, movable2, start2, stop2, step2, ... , " + "movableN, startN, stopN, stepN]'." ), ], snake_axes: bool = True, # Currently specifying axes to snake is not supported diff --git a/tests/plans/test_wrapped.py b/tests/plans/test_wrapped.py index 3d169c40fe5..c27c208a4ba 100644 --- a/tests/plans/test_wrapped.py +++ b/tests/plans/test_wrapped.py @@ -1010,7 +1010,7 @@ def test_make_step_scan_args_and_shape_fails_with_invalid_type_args( ): with pytest.raises( ValueError, - match="Scan syntax only takes movables or numbers for params.", + match="Scan syntax only takes movables or numbers as parameters.", ): _make_step_scan_args_and_shape( [x_axis, 1, "3", 1, y_axis, 1, "4", 1], # type: ignore