From 4962aab48fa0edb5895df6f7d12a42a5a31aad32 Mon Sep 17 00:00:00 2001 From: Codrea Date: Mon, 8 Apr 2024 13:12:40 -0500 Subject: [PATCH 01/10] plan done, TODO: documentation + cleanup --- apstools/plans/xpcs_mesh.py | 228 ++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 apstools/plans/xpcs_mesh.py diff --git a/apstools/plans/xpcs_mesh.py b/apstools/plans/xpcs_mesh.py new file mode 100644 index 000000000..b55f434f2 --- /dev/null +++ b/apstools/plans/xpcs_mesh.py @@ -0,0 +1,228 @@ +from bluesky import plan_stubs as bps +from bluesky import utils +from bluesky import plan_patterns +from bluesky import preprocessors as bpp + +import inspect +from itertools import zip_longest +from collections import defaultdict +import os + +try: + # cytools is a drop-in replacement for toolz, implemented in Cython + from cytools import partition +except ImportError: + from toolz import partition + + +def mesh_list_grid_scan(detectors, *args, number_of_collection_points, snake_axes=False, per_step=None, md=None): + """ + Scan over a mesh; each motor is on an independent trajectory. + + Parameters + ---------- + detectors: list + list of 'readable' objects + args: list + patterned like (``motor1, position_list1,`` + ``motor2, position_list2,`` + ``motor3, position_list3,`` + ``...,`` + ``motorN, position_listN``) + + The first motor is the "slowest", the outer loop. ``position_list``'s + are lists of positions, all lists must have the same length. Motors + can be any 'settable' object (motor, temp controller, etc.). + snake_axes: boolean or iterable, optional + which axes should be snaked, either ``False`` (do not snake any axes), + ``True`` (snake all axes) or a list of axes to snake. "Snaking" an axis + is defined as following snake-like, winding trajectory instead of a + simple left-to-right trajectory.The elements of the list are motors + that are listed in `args`. The list must not contain the slowest + (first) motor, since it can't be snaked. + per_step: callable, optional + hook for customizing action of inner loop (messages per step). + See docstring of :func:`bluesky.plan_stubs.one_nd_step` (the default) + for details. + md: dict, optional + metadata + + See Also + -------- + :func:`bluesky.plans.rel_list_grid_scan` + :func:`bluesky.plans.list_scan` + :func:`bluesky.plans.rel_list_scan` + """ + + full_cycler = plan_patterns.outer_list_product(args, snake_axes) + + md_args = [] + motor_names = [] + motors = [] + for i, (motor, pos_list) in enumerate(partition(2, args)): + md_args.extend([repr(motor), pos_list]) + motor_names.append(motor.name) + motors.append(motor) + # print("when") + _md = { + "shape": tuple(len(pos_list) for motor, pos_list in partition(2, args)), + "extents": tuple([min(pos_list), max(pos_list)] for motor, pos_list in partition(2, args)), + "snake_axes": snake_axes, + "plan_args": {"detectors": list(map(repr, detectors)), "args": md_args, "per_step": repr(per_step)}, + "plan_name": "list_grid_scan", + "plan_pattern": "outer_list_product", + "plan_pattern_args": dict(args=md_args, snake_axes=snake_axes), + "plan_pattern_module": plan_patterns.__name__, + "motors": tuple(motor_names), + "hints": {}, + } + _md.update(md or {}) + try: + _md["hints"].setdefault("dimensions", [(m.hints["fields"], "primary") for m in motors]) + except (AttributeError, KeyError): + ... + + return ( + yield from mesh_scan_nd(detectors, full_cycler, number_of_collection_points, per_step=per_step, md=_md) + ) + + +def mesh_scan_nd(detectors, cycler, number_of_collection_points, *, per_step=None, md=None): + """ + Scan over an arbitrary N-dimensional trajectory. + + Parameters + ---------- + detectors : list + cycler : Cycler + cycler.Cycler object mapping movable interfaces to positions + per_step : callable, optional + hook for customizing action of inner loop (messages per step). + See docstring of :func:`bluesky.plan_stubs.one_nd_step` (the default) + for details. + md : dict, optional + metadata + + See Also + -------- + :func:`bluesky.plans.inner_product_scan` + :func:`bluesky.plans.grid_scan` + + Examples + -------- + >>> from cycler import cycler + >>> cy = cycler(motor1, [1, 2, 3]) * cycler(motor2, [4, 5, 6]) + >>> scan_nd([sensor], cy) + """ + _md = { + "detectors": [det.name for det in detectors], + "motors": [motor.name for motor in cycler.keys], + "num_points": len(cycler), + "num_intervals": len(cycler) - 1, + "plan_args": {"detectors": list(map(repr, detectors)), "cycler": repr(cycler), "per_step": repr(per_step)}, + "plan_name": "scan_nd", + "hints": {}, + } + _md.update(md or {}) + try: + dimensions = [(motor.hints["fields"], "primary") for motor in cycler.keys] + except (AttributeError, KeyError): + # Not all motors provide a 'fields' hint, so we have to skip it. + pass + else: + # We know that hints exists. Either: + # - the user passed it in and we are extending it + # - the user did not pass it in and we got the default {} + # If the user supplied hints includes a dimension entry, do not + # change it, else set it to the one generated above + _md["hints"].setdefault("dimensions", dimensions) + + predeclare = per_step is None and os.environ.get("BLUESKY_PREDECLARE", False) + if per_step is None: + per_step = bps.one_nd_step + else: + # Ensure that the user-defined per-step has the expected signature. + sig = inspect.signature(per_step) + + def _verify_1d_step(sig): + if len(sig.parameters) < 3: + return False + for name, (p_name, p) in zip_longest(["detectors", "motor", "step"], sig.parameters.items()): + # this is one of the first 3 positional arguements, check that the name matches + if name is not None: + if name != p_name: + return False + # if there are any extra arguments, check that they have a default + else: + if p.kind is p.VAR_KEYWORD or p.kind is p.VAR_POSITIONAL: + continue + if p.default is p.empty: + return False + + return True + + def _verify_nd_step(sig): + if len(sig.parameters) < 3: + return False + for name, (p_name, p) in zip_longest(["detectors", "step", "pos_cache"], sig.parameters.items()): + # this is one of the first 3 positional arguements, check that the name matches + if name is not None: + if name != p_name: + return False + # if there are any extra arguments, check that they have a default + else: + if p.kind is p.VAR_KEYWORD or p.kind is p.VAR_POSITIONAL: + continue + if p.default is p.empty: + return False + + return True + + if sig == inspect.signature(bps.one_nd_step): + pass + elif _verify_nd_step(sig): + # check other signature for back-compatibility + pass + elif _verify_1d_step(sig): + # Accept this signature for back-compat reasons (because + # inner_product_scan was renamed scan). + dims = len(list(cycler.keys)) + if dims != 1: + raise TypeError( + "Signature of per_step assumes 1D trajectory " "but {} motors are specified.".format(dims) + ) + (motor,) = cycler.keys + user_per_step = per_step + + def adapter(detectors, step, pos_cache): + # one_nd_step 'step' parameter is a dict; one_id_step 'step' + # parameter is a value + (step,) = step.values() + return (yield from user_per_step(detectors, motor, step)) + + per_step = adapter + else: + raise TypeError( + "per_step must be a callable with the signature \n " + " or " + ". \n" + "per_step signature received: {}".format(sig) + ) + pos_cache = defaultdict(lambda: None) # where last position is stashed + cycler = utils.merge_cycler(cycler) + motors = list(cycler.keys) + + @bpp.stage_decorator(list(detectors) + motors) + @bpp.run_decorator(md=_md) + def scan_until_completion(): + if predeclare: + yield from bps.declare_stream(*motors, *detectors, name="primary") + iterations = 0 + while iterations < number_of_collection_points: + for step in list(cycler): + yield from per_step(detectors, step, pos_cache) + iterations += 1 + if iterations == number_of_collection_points: + break + + return (yield from scan_until_completion()) From b0868619ba9450670903e26438af4dabf117c1af Mon Sep 17 00:00:00 2001 From: Codrea Date: Mon, 8 Apr 2024 13:45:54 -0500 Subject: [PATCH 02/10] documented the mesh_grid_scan plan --- apstools/plans/xpcs_mesh.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apstools/plans/xpcs_mesh.py b/apstools/plans/xpcs_mesh.py index b55f434f2..c80afc814 100644 --- a/apstools/plans/xpcs_mesh.py +++ b/apstools/plans/xpcs_mesh.py @@ -33,6 +33,9 @@ def mesh_list_grid_scan(detectors, *args, number_of_collection_points, snake_axe The first motor is the "slowest", the outer loop. ``position_list``'s are lists of positions, all lists must have the same length. Motors can be any 'settable' object (motor, temp controller, etc.). + number_of_collection_points: int + The total number of collection points that must be collected within the + grid until the scan is ready to stop. snake_axes: boolean or iterable, optional which axes should be snaked, either ``False`` (do not snake any axes), ``True`` (snake all axes) or a list of axes to snake. "Snaking" an axis @@ -96,6 +99,9 @@ def mesh_scan_nd(detectors, cycler, number_of_collection_points, *, per_step=Non detectors : list cycler : Cycler cycler.Cycler object mapping movable interfaces to positions + number_of_collection_points: int + The total number of collection points that must be collected within the + grid until the scan is ready to stop. per_step : callable, optional hook for customizing action of inner loop (messages per step). See docstring of :func:`bluesky.plan_stubs.one_nd_step` (the default) @@ -215,6 +221,9 @@ def adapter(detectors, step, pos_cache): @bpp.stage_decorator(list(detectors) + motors) @bpp.run_decorator(md=_md) def scan_until_completion(): + """ + Scanning until the total number of required collection points is achieved + """ if predeclare: yield from bps.declare_stream(*motors, *detectors, name="primary") iterations = 0 From 5609d387ad7ff5ed8e2bf2ab9cc5b01461f57383 Mon Sep 17 00:00:00 2001 From: Codrea Date: Thu, 11 Apr 2024 14:07:06 -0500 Subject: [PATCH 03/10] Added documentation --- CHANGES.rst | 5 +++++ apstools/plans/__init__.py | 1 + docs/source/api/_plans.rst | 37 ++++++++++++++++++++----------------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index fda58dd04..d5ee6303e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -30,6 +30,11 @@ describe future plans. release expected by 2024-04-02 +New Features + ------------ + + * Added a mesh grid scan plan that will collect until number of collection points is met + Fixes ----- diff --git a/apstools/plans/__init__.py b/apstools/plans/__init__.py index 1337677a5..abadee10f 100644 --- a/apstools/plans/__init__.py +++ b/apstools/plans/__init__.py @@ -24,6 +24,7 @@ from .sscan_support import sscan_1D from .stage_sigs_support import restorable_stage_sigs from .stage_sigs_support import stage_sigs_wrapper +from .xpcs_mesh import mesh_list_grid_scan # ----------------------------------------------------------------------------- # :author: Pete R. Jemian diff --git a/docs/source/api/_plans.rst b/docs/source/api/_plans.rst index 49fdd7cfc..456d74b98 100644 --- a/docs/source/api/_plans.rst +++ b/docs/source/api/_plans.rst @@ -29,14 +29,15 @@ Custom Scans .. autosummary:: - ~apstools.plans.doc_run.documentation_run - ~apstools.plans.labels_to_streams.label_stream_decorator ~apstools.plans.alignment.lineup ~apstools.plans.alignment.lineup2 + ~apstools.plans.alignment.tune_axes + ~apstools.plans.alignment.TuneAxis + ~apstools.plans.doc_run.documentation_run + ~apstools.plans.labels_to_streams.label_stream_decorator ~apstools.plans.nscan_support.nscan ~apstools.plans.sscan_support.sscan_1D - ~apstools.plans.alignment.TuneAxis - ~apstools.plans.alignment.tune_axes + ~apstools.plans.xpcs_mesh.mesh_list_grid_scan .. _plans.overall: @@ -45,29 +46,31 @@ Overall .. autosummary:: - ~apstools.plans.doc_run.addDeviceDataAsStream + ~apstools.plans.alignment.lineup + ~apstools.plans.alignment.lineup2 + ~apstools.plans.alignment.tune_axes + ~apstools.plans.alignment.TuneAxis ~apstools.plans.command_list.command_list_as_table - ~apstools.plans.doc_run.documentation_run ~apstools.plans.command_list.execute_command_list ~apstools.plans.command_list.get_command_list - ~apstools.plans.alignment.lineup - ~apstools.plans.alignment.lineup2 - ~apstools.plans.nscan_support.nscan ~apstools.plans.command_list.parse_Excel_command_file ~apstools.plans.command_list.parse_text_command_file - ~apstools.plans.labels_to_streams.label_stream_decorator - ~apstools.plans.labels_to_streams.label_stream_stub - ~apstools.plans.labels_to_streams.label_stream_wrapper ~apstools.plans.command_list.register_command_handler ~apstools.plans.command_list.run_command_file + ~apstools.plans.command_list.summarize_command_file + ~apstools.plans.doc_run.addDeviceDataAsStream + ~apstools.plans.doc_run.documentation_run + ~apstools.plans.doc_run.write_stream ~apstools.plans.input_plan.request_input - ~apstools.plans.stage_sigs_support.restorable_stage_sigs + ~apstools.plans.labels_to_streams.label_stream_decorator + ~apstools.plans.labels_to_streams.label_stream_stub + ~apstools.plans.labels_to_streams.label_stream_wrapper + ~apstools.plans.nscan_support.nscan ~apstools.plans.run_blocking_function_plan.run_blocking_function ~apstools.plans.sscan_support.sscan_1D - ~apstools.plans.command_list.summarize_command_file - ~apstools.plans.alignment.TuneAxis - ~apstools.plans.alignment.tune_axes - ~apstools.plans.doc_run.write_stream + ~apstools.plans.stage_sigs_support.restorable_stage_sigs + ~apstools.plans.xpcs_mesh.mesh_list_grid_scan + Also consult the :ref:`Index ` under the *Bluesky* heading for links to the Callbacks, Devices, Exceptions, and Plans described From 70f2084205720f3e5754e9df3c12e78eeff99309 Mon Sep 17 00:00:00 2001 From: Codrea Date: Thu, 11 Apr 2024 14:10:44 -0500 Subject: [PATCH 04/10] Added cytools to pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 3467664cb..1b3c0d45c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ classifiers = [ dependencies = [ "area-detector-handlers", "bluesky>=1.6.2,!=1.11.0", + "cytools>=1.0.1", "databroker==1.2.5", "databroker-pack", "h5py", From 9dceaa2ba2a3475ddb48102e9fbc5dbec2164a82 Mon Sep 17 00:00:00 2001 From: Codrea Date: Thu, 11 Apr 2024 14:33:51 -0500 Subject: [PATCH 05/10] Replaced while loop --- apstools/plans/xpcs_mesh.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apstools/plans/xpcs_mesh.py b/apstools/plans/xpcs_mesh.py index c80afc814..96cffe0bf 100644 --- a/apstools/plans/xpcs_mesh.py +++ b/apstools/plans/xpcs_mesh.py @@ -4,7 +4,7 @@ from bluesky import preprocessors as bpp import inspect -from itertools import zip_longest +from itertools import zip_longest, cycle from collections import defaultdict import os @@ -17,7 +17,7 @@ def mesh_list_grid_scan(detectors, *args, number_of_collection_points, snake_axes=False, per_step=None, md=None): """ - Scan over a mesh; each motor is on an independent trajectory. + Scan over a multi-dimensional mesh, collecting a total of *n* points; each motor is on an independent trajectory. Parameters ---------- @@ -226,12 +226,12 @@ def scan_until_completion(): """ if predeclare: yield from bps.declare_stream(*motors, *detectors, name="primary") + iterations = 0 - while iterations < number_of_collection_points: - for step in list(cycler): - yield from per_step(detectors, step, pos_cache) - iterations += 1 - if iterations == number_of_collection_points: - break + for step in cycle(cycler): + yield from per_step(detectors, step, pos_cache) + iterations += 1 + if iterations == number_of_collection_points: + break return (yield from scan_until_completion()) From b9f7fafa539c1feb5dac25ca01edce6dfce1bb07 Mon Sep 17 00:00:00 2001 From: Codrea Date: Fri, 12 Apr 2024 11:42:16 -0500 Subject: [PATCH 06/10] Merged nd_step & 1d_step verify funtions --- apstools/plans/xpcs_mesh.py | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/apstools/plans/xpcs_mesh.py b/apstools/plans/xpcs_mesh.py index 96cffe0bf..9e4c51d04 100644 --- a/apstools/plans/xpcs_mesh.py +++ b/apstools/plans/xpcs_mesh.py @@ -150,27 +150,10 @@ def mesh_scan_nd(detectors, cycler, number_of_collection_points, *, per_step=Non # Ensure that the user-defined per-step has the expected signature. sig = inspect.signature(per_step) - def _verify_1d_step(sig): + def _verify_step(sig, expected_list): if len(sig.parameters) < 3: return False - for name, (p_name, p) in zip_longest(["detectors", "motor", "step"], sig.parameters.items()): - # this is one of the first 3 positional arguements, check that the name matches - if name is not None: - if name != p_name: - return False - # if there are any extra arguments, check that they have a default - else: - if p.kind is p.VAR_KEYWORD or p.kind is p.VAR_POSITIONAL: - continue - if p.default is p.empty: - return False - - return True - - def _verify_nd_step(sig): - if len(sig.parameters) < 3: - return False - for name, (p_name, p) in zip_longest(["detectors", "step", "pos_cache"], sig.parameters.items()): + for name, (p_name, p) in zip_longest(expected_list, sig.parameters.items()): # this is one of the first 3 positional arguements, check that the name matches if name is not None: if name != p_name: @@ -186,10 +169,11 @@ def _verify_nd_step(sig): if sig == inspect.signature(bps.one_nd_step): pass - elif _verify_nd_step(sig): + elif _verify_step(sig, ["detectors", "step", "pos_cache"]): # check other signature for back-compatibility pass - elif _verify_1d_step(sig): + + elif _verify_step(sig, ["detectors", "motor", "step"]): # Accept this signature for back-compat reasons (because # inner_product_scan was renamed scan). dims = len(list(cycler.keys)) From b93d7efe40c0956a5da0595c2495f32ed105ad4c Mon Sep 17 00:00:00 2001 From: Codrea Date: Fri, 12 Apr 2024 11:51:36 -0500 Subject: [PATCH 07/10] got rid of try/except in import, no more enumerate --- apstools/plans/xpcs_mesh.py | 9 ++------- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/apstools/plans/xpcs_mesh.py b/apstools/plans/xpcs_mesh.py index 9e4c51d04..710081ae3 100644 --- a/apstools/plans/xpcs_mesh.py +++ b/apstools/plans/xpcs_mesh.py @@ -8,11 +8,7 @@ from collections import defaultdict import os -try: - # cytools is a drop-in replacement for toolz, implemented in Cython - from cytools import partition -except ImportError: - from toolz import partition +from toolz import partition def mesh_list_grid_scan(detectors, *args, number_of_collection_points, snake_axes=False, per_step=None, md=None): @@ -62,11 +58,10 @@ def mesh_list_grid_scan(detectors, *args, number_of_collection_points, snake_axe md_args = [] motor_names = [] motors = [] - for i, (motor, pos_list) in enumerate(partition(2, args)): + for (motor, pos_list) in partition(2, args): md_args.extend([repr(motor), pos_list]) motor_names.append(motor.name) motors.append(motor) - # print("when") _md = { "shape": tuple(len(pos_list) for motor, pos_list in partition(2, args)), "extents": tuple([min(pos_list), max(pos_list)] for motor, pos_list in partition(2, args)), diff --git a/pyproject.toml b/pyproject.toml index 1b3c0d45c..e7786ef80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,6 @@ classifiers = [ dependencies = [ "area-detector-handlers", "bluesky>=1.6.2,!=1.11.0", - "cytools>=1.0.1", "databroker==1.2.5", "databroker-pack", "h5py", @@ -69,6 +68,7 @@ dependencies = [ "pyRestTable>=2020.0.8", "pysumreg", "spec2nexus>=2021.1.7", + "toolz>=0.12.1", "xlrd", ] From ccecf2f65f0e3a2badb6ab9b3c848f9bac518ab7 Mon Sep 17 00:00:00 2001 From: Codrea Date: Fri, 12 Apr 2024 11:55:18 -0500 Subject: [PATCH 08/10] fixed print statement syntax --- apstools/plans/xpcs_mesh.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apstools/plans/xpcs_mesh.py b/apstools/plans/xpcs_mesh.py index 710081ae3..59ff6c320 100644 --- a/apstools/plans/xpcs_mesh.py +++ b/apstools/plans/xpcs_mesh.py @@ -58,7 +58,7 @@ def mesh_list_grid_scan(detectors, *args, number_of_collection_points, snake_axe md_args = [] motor_names = [] motors = [] - for (motor, pos_list) in partition(2, args): + for motor, pos_list in partition(2, args): md_args.extend([repr(motor), pos_list]) motor_names.append(motor.name) motors.append(motor) @@ -173,9 +173,7 @@ def _verify_step(sig, expected_list): # inner_product_scan was renamed scan). dims = len(list(cycler.keys)) if dims != 1: - raise TypeError( - "Signature of per_step assumes 1D trajectory " "but {} motors are specified.".format(dims) - ) + raise TypeError(f"Signature of per_step assumes 1D trajectory but {dims} motors are specified.") (motor,) = cycler.keys user_per_step = per_step From c4d570cdd6d676d15e1c0bd6eeff0417f59ebb88 Mon Sep 17 00:00:00 2001 From: Codrea Date: Fri, 12 Apr 2024 13:57:33 -0500 Subject: [PATCH 09/10] Added toolz to environment.yaml --- environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/environment.yml b/environment.yml index 5479a2e2f..7c9b6e4c8 100644 --- a/environment.yml +++ b/environment.yml @@ -34,6 +34,7 @@ dependencies: - setuptools-scm - spec2nexus - sphinx >=5 + - toolz - aps-dm-api >=8 # linux-64 osx-64 From 9ab17464809921dca44b81bb1629f522620446ff Mon Sep 17 00:00:00 2001 From: Codrea Date: Fri, 12 Apr 2024 14:02:44 -0500 Subject: [PATCH 10/10] formatting --- environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 7c9b6e4c8..cfc4e86e8 100644 --- a/environment.yml +++ b/environment.yml @@ -34,7 +34,7 @@ dependencies: - setuptools-scm - spec2nexus - sphinx >=5 - - toolz + - toolz - aps-dm-api >=8 # linux-64 osx-64