As part of moving plans from dls-bluesky-core, the scan
plan (an equivalent to the GDA mscan command: mscan
constructs a ScanPointGenerator
, scan
is passed a constructed ScanSpec
: each just a description of an N-dimensional sequence of points) should be re-examined.
@callumforrester is fairly sure that axes_to_move was a Pydantic, APISchema or Mypy specific requirement (for type checking?): if it can be removed and the string value of an axis in a Spec converted to its underlying device, it would make typing a Scan with a Spec a little less verbose.
Alternatively, a default factory could be provided for the argument with {x: x for x in Spec.axes}, and the argument made optional.
For posterity, here is the source code of scan
:
import operator
from functools import reduce
from typing import Any, List, Mapping, Optional
import bluesky.plans as bp
from bluesky.protocols import Movable, Readable
from cycler import Cycler, cycler
from scanspec.specs import Spec
"""
Plans related to the use of the `ScanSpec https://github.com/dls-controls/scanspec`
library for constructing arbitrarily complex N-dimensional trajectories, similar to
Diamond's "mapping scans" using ScanPointGenerator.
"""
def scan(
detectors: List[Readable],
axes_to_move: Mapping[str, Movable],
spec: Spec[str],
metadata: Optional[Mapping[str, Any]] = None,
) -> MsgGenerator:
"""
Scan wrapping `bp.scan_nd`
Args:
detectors: List of readable devices, will take a reading at
each point
axes_to_move: All axes involved in this scan, names and
objects
spec: ScanSpec modelling the path of the scan
metadata: Key-value metadata to include
in exported data, defaults to
None.
Returns:
MsgGenerator: Plan
Yields:
Iterator[MsgGenerator]: Bluesky messages
"""
_md = {
"plan_args": {
"detectors": list(map(repr, detectors)),
"axes_to_move": {k: repr(v) for k, v in axes_to_move.items()},
"spec": repr(spec),
},
"plan_name": "scan",
"shape": spec.shape(),
**(metadata or {}),
}
cycler = _scanspec_to_cycler(spec, axes_to_move)
yield from bp.scan_nd(detectors, cycler, md=_md)
def _scanspec_to_cycler(spec: Spec[str], axes: Mapping[str, Movable]) -> Cycler:
"""
Convert a scanspec to a cycler for compatibility with legacy Bluesky plans such as
`bp.scan_nd`. Use the midpoints of the scanspec since cyclers are normally used
for software triggered scans.
Args:
spec: A scanspec
axes: Names and axes to move
Returns:
Cycler: A new cycler
"""
midpoints = spec.frames().midpoints
midpoints = {axes[name]: points for name, points in midpoints.items()}
# Need to "add" the cyclers for all the axes together. The code below is
# effectively: cycler(motor1, [...]) + cycler(motor2, [...]) + ...
return reduce(operator.add, map(lambda args: cycler(*args), midpoints.items()))