计划 是Bluesky中描述实验流程的概念。计划可以是任何可迭代对象(如列表、元组、自定义可迭代类等),但最常见通过Python生成器实现。有关技术细节的讨论,请参阅消息协议章节。
我们提供了多种预置计划。就像熟食店菜单上的三明治一样,您可以直接使用这些预置计划,也可以利用下方Stub计划部分列出的基础组件自行组合构建。
注意: 在以下示例中,我们将假设您已有一个名为 RE 的运行引擎实例。如果您是在已部署Bluesky的实验设施中工作的用户,该实例可能已为您预先配置。请参阅本教程的此章节以确认是否已拥有运行引擎,或在需要时了解如何快速创建。
预置计划
在此汇总表格下方,我们将按类别分项详解这些计划,并提供配图示例。
概要
|--------------------|--------------------------------------|
| count | 从探测器获取一个或多个读数 |
| scan | 沿单个多电机轨迹进行扫描。 |
| rel_scan | 相对于当前位置沿单个多电机轨迹进行扫描。 |
| list_scan | 同步步进扫描一个或多个变量(内积扫描法)。 |
| rel_list_scan | 相对当前位置步进扫描一个变量 |
| list_grid_scan | 网格扫描, 每个电机在一个独立轨迹上. |
| rel_list_grid_scan | 网格扫描,, 每个电机相对于当前位置在一个独立轨迹上. |
| log_scan | 以对数间隔的步长扫描一个变量。 |
| rel_log_scan | 相对于当前位置以对数间隔的步长扫描一个变量。 |
| grid_scan | 网格扫描, 每个电机在一个独立轨迹上. |
| rel_grid_scan | 相对于当前位置的网格扫描 |
| scan_nd | 在任意N维轨迹上扫描 |
| spiral | 螺旋扫描,以 (x_start, y_start) 为中心。 |
| spiral_fermat | 绝对费马螺旋扫描,以 (x_start, y_start) 为中心。 |
| spiral_square | 绝对方形螺旋扫描,以 (x_center, y_center) 为中心。 |
| rel_spiral | 相对螺旋扫描 |
| rel_spiral_fermat | 相对费马螺旋扫描 |
| rel_spiral_square | 相对方形螺旋扫描,以 (x_center, y_center) 为中心。 |
| adaptive_scan | 以自适应调整的步长扫描一个变量。 |
| rel_adaptive_scan | 以自适应调整的步长相对扫描一个变量。 |
| tune_centroid | 计划:通过扫描将电机调整至信号分布的质心位置。 |
| tweak | 通过交互式提示移动电机并读取探测器数据的扫描计划。 |
| ramp_plan | 斜坡扫描一个或多个定位器并同步采集数据。 |
| fly | 执行一个或多个'飞扫器'的飞扫。 |
时间序列("count")
示例:
python
from bluesky import RunEngine
from bluesky.callbacks.best_effort import BestEffortCallback
from ophyd.sim import det
from bluesky.plans import count
bc = BestEffortCallback()
RE = RunEngine({})
RE.subscribe(bc)
# 单次读取探测器'det'
RE(count([det]))
# 5次连续读取
RE(count([det], num=5))
# 5次连续读取,两次之间延迟1秒
RE(count([det], num=5, delay=1))
# 5次连续读取,两次之间分别延迟1,2,3,4秒
RE(count([det], num=5, delay=[1,2,3,4]))
# 不间断读取, 直到中断(例如, 用Ctrl+C)
RE(count([det], num=None))
python
from ophyd.sim import noisy_det
# 为更有趣的绘图使用'noisy_det'示例探测器
RE(count([noisy_det], num=5, delay=1))

为什么count() 函数没有 exposure_time 参数?
现代CCD探测器用 acquire_time(采集时间)、acquire_period(采集周期)、num_exposures(曝光次数)参数化曝光时间, 而scalers用使用 preset_time(预设时间)或 auto_count_time(自动计数时间)参数化曝光时间. 没有适用于所有探测器的"曝光时间".
当同时计数多个探测器时(如 count([det1, det2, det3])),一般情况下, 使用者需要为每个探测提供单独的曝光时间, 此举变得冗长.
保持计划的通用性,将设备特定的配置留给设备类本身管理。
对于交互使用:
python
# 仅用作示例. 你的探测器可能有不同名称或者数目的曝光相关参数--这正是关键所在。
det.exposure_time.set(3)
det.acquire_period.set(3.5)
从计划:
python
# 仅用作示例. 你的探测器可能有不同名称或者数目的曝光相关参数--这正是关键所在。
yield from bluesky.plan_stubs.mv(
det.exposure_time, 3,
det.acquire_period, 3.5)
另一种解决方案是编写一个自定义计划,该计划封装count()并设置曝光时间。这个自定义计划可以编码那些Bluesky框架本身无法知晓的具体细节。
python
def count_with_time(detectors, num, delay, exposure_time, *, md=None):
# 假设所有探测器都有一个完全指定其曝光的名为'exposure_time'的曝光时间组件.
for detector in detectors:
yield from bluesky.plan_stubs.mv(detector.exposure_time, exposure_time)
yield from bluesky.plans.count(detectors, num, delay, md=md)
在一个维度上扫描
此"维度"可以是物理电机位置、温度参数或伪轴。对于扫描计划而言,它们的处理方式都是相同的。例如:
python
from ophyd.sim import det, motor
from bluesky.plans import scan , rel_scan, list_scan
# 将电机从1扫描到5,对'det'进行5次等间距读数采集。
RE(scan([det], motor, 1, 5, 5))
# 将电机相对于它的当前位置从1扫描到5,对'det'进行5次等间距读数采集。
RE(rel_scan([det], motor, 1, 5, 5))
# 通过一个用户指定位置的列表扫描电机
RE(list_scan([det], motor, [1,1,2,3,5,8]))

注意: 为何扫描函数没有 delay 参数?
您可能注意到 count() 函数有 delay 参数,但所有扫描函数均未提供此参数。这是有意为之的设计。
通常希望在扫描中添加延迟的常见原因是为了:等待电机稳定到位或者让温度控制器达到平衡状态
更好的解决方案是将延迟配置在相应的设备上,这样无论使用哪种扫描计划,都会自动为该特定设备添加适当的延迟。
python
motor.settle_time = 1
temperature_controller.settle_time = 10
在多数情况下,将延迟配置在设备层面比每次调用扫描时都手动输入delay参数更为便捷和可靠。您只需设置一次,即可持续生效。
这正是Bluesky在设计扫描函数时有意省略delay参数的原因------旨在引导用户采用一种比他们最初可能想到的方法更合适的方案。当然,对于确实需要使用delay参数的特殊情况,您完全可以通过编写自定义计划来实现。以下是利用per_step钩子的一种实现方式。
python
import bluesky.plans
import bluesky.plan_stubs
def scan_with_delay(*args, delay=0, **kwargs):
# 接收所有正常的'scan'参数,添上一个可选的delay.
def one_nd_step_with_delay(detectors, step, pos_cache):
# 这是添加了一个sleep的bluesky.plan_stubs.one_nd_step副本
motors = step.keys()
yield from bluesky.plan_stubs.move_per_step(step, pos_cache)
yield from bluesky.plan_stubs.sleep(delay)
yield from bluesky.plan_stubs.trigger_and_read(list(detectors) + list(motors))
kwargs.setdefault('per_step', one_nd_step_with_delay)
yield from bluesky.plans.scan(*args, **kwargs)
多维扫描
关于多电机协同运动的常见情况(例如沿对角线协同移动X轴和Y轴电机,或在网格中移动),请参见教程中的同时扫描多个电机章节。下文重现了其中的关键示例,更多详细解释仍请参阅所链接的章节。
python
from ophyd.sim import det, motor1, motor2, motor3
from bluesky.plans import scan, grid_scan, list_scan, list_grid_scan
# 电机1从-1.5到1.5同时电机2从-0.1到0.1都扫描11个点
RE(scan([det], motor1,-1.5,1.5,motor2,-0.1,0.1,11))

python
# 电机1和电机2一起通过5个点的轨迹
RE(list_scan([noisy_det], motor1,[1,1,3,5,8], motor2,[25,16,9,4,1]))

python
# 扫描3*2个网格
RE(grid_scan([noisy_det], motor1,-1.5,1.5,3, motor3,-200,200,2,False))

python
# 按照给定的特定位置进行任意间距的网格扫描。
RE(list_grid_scan([noisy_det], motor1,[1,2,3],motor2,[4,5,6]))

所有上述计划都建立在更通用的scan_nd()计划基础之上,该计划可用于处理更特殊的扫描场景。
引入一些术语:我们将scan()式的协同运动称为轨迹的 "内积扫描" ,而将grid_scan()式的运动称为轨迹的 "外积扫描" 。更一般的情况,即在 "外积扫描" 中协同移动某些电机的同时,对另一电机(或多个电机)进行 "内积扫描" ,可以通过cycler对象来处理。请注意当我们对cycler对象进行加法或乘法运算时发生的情况。
python
from cycler import cycler
from ophyd.sim import motor1, motor2, motor3
traj1 = cycler(motor1, [1,2,3])
traj2 = cycler(motor2, [10,20,30])
traj3 = cycler(motor3, [100,200,300])

我们已分别展示了内积扫描和外积扫描。真正的功能出现在我们将它们组合使用时,如下所示。这里,motor1 和 motor2 在一个网格中协同运动,并与 motor3 进行扫描。

螺旋轨迹
我们提供沿螺旋轨迹进行扫描的二维扫描方法。
一种简单螺旋轨迹扫描:
python
from bluesky.simulators import plot_raster_path
from ophyd.sim import motor1, motor2, det
from bluesky.plans import spiral
plan = spiral([det], motor1, motor2, x_start=0.0, y_start=0.0, x_range=1.,
y_range=1.0, dr=0.1, nth=10)
plot_raster_path(plan, 'motor1', 'motor2', probe_size=.01)

费马螺旋:
python
from bluesky.simulators import plot_raster_path
from ophyd.sim import motor1, motor2, det
from bluesky.plans import spiral_fermat
plan = spiral_fermat([det], motor1, motor2, x_start=0.0, y_start=0.0,
x_range=2.0, y_range=2.0, dr=0.1, factor=2.0, tilt=0.0)
plot_raster_path(plan, 'motor1', 'motor2', probe_size=.01, lw=0.1)

方形螺旋:
python
from bluesky.simulators import plot_raster_path
from ophyd.sim import motor1, motor2, det
from bluesky.plans import spiral_square
plan = spiral_square([det], motor1, motor2, x_center=0.0, y_center=0.0,
x_range=1.0, y_range=1.0, x_num=11, y_num=11)
plot_raster_path(plan, 'motor1', 'motor2', probe_size=.01)

自适应扫描
这是一种一维扫描技术,通过自适应调整步长来实现:在变化平缓的区域快速移动,而在变化显著的区域通过计算局部斜率、以目标相邻点间y值变化量为基准来集中采集读数。
这是自适应计划逻辑功能的一个基本示例。
python
from bluesky.plans import adaptive_scan
from ophyd.sim import motor, det
RE(adaptive_scan([det], 'det', motor,
start=-15,
stop=10,
min_step=0.01,
max_step=5,
target_delta=.05,
backstep=True))

从左到右观察扫描过程:在平坦区域,扫描会增大步幅快速跨越。初次扫描时,它直接越过了峰值。由于这次大幅跳跃检测到显著变化,算法随即折返,并在峰值区域进行更密集的采样。随着峰值区域变化趋于平缓,扫描再次拉大步幅继续前进。
Stubs计划
这些正是前文提及的"可重组基础单元",即用于构建上述预置计划的组成部分。关于如何实际运用这些组件编写自定义计划,请参阅教程中的编写自定义计划章节,以获取实践指南。
以下是与硬件交互的基础计划:
|-----------|--------------------------------------|
| abs_set | 设置一个值 |
| rel_set | 相对于当前值设置一个值 |
| mv | 移动一个或多个设备到设定点 |
| mvr | 移动一个或多个设备到相对设定点 |
| trigger | 触发并采集 |
| read | 进行一次读数并将其添加到当前的读数组中。 |
| rd | 读取一个单值的非触发对象。 |
| stage | "设就"设备(即准备设备使其进入可用状态,或称"预置"或"就绪")。 |
| unstage | "解除就绪"设备(即将其置于待机状态,或称"撤防"或"解除工作状态")。 |
| configure | 更改设备配置并且发出一个更新的事件描述符文档 |
| stop | 停止设备 |
用于异步采集的计划:
|-----------|------------------------|
| monitor | 用于新值并且发出事件文档的异步监视器 |
| unmonitor | 停止监视 |
| kickoff | 触发一个飞扫设备 |
| complete | 在你准备好时,告诉一个飞扫器, '停止采集' |
| collect | 采集由飞扫设备缓存的数据并且发出文档 |
控制运行引擎的计划:
|-------------------|---------------------------------------|
| open_run | 标记一个新'运行'起始 |
| close_run | 标记当前'运行'结束 |
| create | 将后续的读数归集到一个新的事件文档中。 |
| save | 关闭一个读数包并且发出一个完成的事件文档 |
| drop | 丢弃一个读数包,且不发出已完成的事件文档。 |
| pause | 暂停并且等待用户继续 |
| deferred_pause | 暂停在下个检查点 |
| checkpoint | 若执行被中断,则回退至此标记点。 |
| clear_checkpoint | 标识其为不安全的恢复点。 |
| sleep | 告诉运行引擎睡眠, 同时异步执行其它运行 |
| input_plan | 提示用户文本输入 |
| subscribe | 订阅发出的文档流 |
| unsubscribe | 删除一个订阅 |
| install_suspender | 在执行计划时安装一个挂起器。 |
| remove_suspender | 在执行计划时删除一个挂起器。 |
| wait | 等待一个组中所有状态都报告结束 |
| wait_for | 底层操作: 等待一系列 asyncio.Future 对象设置(完成) |
| null | 生成一个空操作消息。 |
上述各项的常用组合:
|-------------------------------------------------|------------------------------|
| trigger_and_read(devices[,name]) | 触发并读取探测器列表,并将所有读数打包至一个事件文档中。 |
| one_1d_step(detectors, motor, step[,...]) | 1维步进扫描的内层循环 |
| one_nd_step(detectors, step, pos_cache[,...]) | N维步进扫描的内层循环 |
| one_shot(detectors[,tabke_reading]) | 一个计数的内层循环 |
| move_per_step(step, pos_cache) | 不带任何读数的N维步进扫描的内层循环 |
特殊工具:
|--------------------------------------------------|-------------------------------|
| repeat(plan[,num,delay]) | 按指定次数重复执行某个计划,每次重复之间设有延迟与检查点。 |
| repeater(n,gen_func,*args, **kwargs) | 生成 n 个来自 gen_func 的链式消息副本。 |
| caching_repeater(n,plan) | 在一个计划中生成n个链式消息副本 |
| broadcast_msg(command, objs, *args, **kwargs) | 生成一条消息的多个副本,并将其应用于设备列表。 |
计划预处理器
补充数据
计划预处理器可在运行时动态修改计划内容。一个常见的用途是,在每个运行(run)的开始和结束时,自动对一组设备采集"基线"读数。通过补充数据(SupplementalData) 机制,可以方便地将此功能应用于运行引擎执行的所有计划。
python
class bluesky.preprocessors.SupplementalData(*, baseline=None, monitors=None, flyers=None)
一个用于补充测量的可配置预处理器.这是一个计划预处理器. 它插入消息到计划以实现:
- 在其 baseline 属性中列出的设备,会在每个运行的开始和结束时自动进行 "基线"读数。
- 在其 monitors 属性中列出的信号,会在每个运行期间进行异步更新监视。
- 在其 flyers 属性中列出的"可飞扫"设备,会在每个运行开始时启动,并在运行结束时收集它们的数据。
内部, 它使用这些计划预处理器:
- baseline_wrapper()
- monitor_during_wrapper()
- fly_during_wrapper()
参数:
- baseline:list 在每次运行开始和结束要被读取的设备.
- monitor:list 会在每个运行期间进行异步更新监视的设备。
- flyers: list 在每次运行前要被触发并且运行结束时采集的"可飞行"设备.
示例
创建一个SupplementalData的实例并且应用它于运行引擎.
python
sd = SupplementalData(baseline=[some_motor, some_detector]),
monitors=[some_signal],
flyers=[some_flyer])
RE = RunEngine({})
RE.preprocessors.append(sd)
现在由RE执行的所有计划将被修改为在每次运行前后添加基线读数, (在运行期间)添加监视, 以及(在每次运行前触发并且之后采集)飞行器.
交互地检查或更新列表:
python
from bluesky.preprocessors import SupplementalData
sd = SupplementalData(baseline=[motor])
sd.baseline

python
sd.baseline.append(det)
sd.baseline

python
sd.baseline.remove(det)
sd.baseline

每个属性(baseline, monitors, flyers)时一个普通地Python列表, 支持所有标准的列表方法,诸如:
python
sd.baseline.clear()
传给SupplementalData的参数是可选的, 所有列表默认是空的. 入以上显示,它们可以被交互地填充:
python
sd = SupplementalData()
RE = RunEngine({})
RE.preprocessors.append(sd)
sd.baseline.append(some_detector)
我们已在运行引擎上安装了一个 "预处理器"。预处理器能够修改计划,以某种方式补充或更改其指令。从现在起,每次我们执行 RE(some_plan()) 时,运行引擎都会静默地将 some_plan() 转换为 sd(some_plan())。其中,sd(即预处理器)可以插入一些额外的指令。可以这样理解指令的流动路径:指令从 some_plan 流出,经过 sd 处理,最后到达 RE。sd 预处理器在指令传递过程中有机会检查流经的指令,并在它们被运行引擎处理之前,根据需要进行修改。
预处理器包装器和装饰器
预处理器可以对计划进行任意修改,甚至可以设计得相当巧妙。例如,relative_set_wrapper() 预处理器会将所有位置指令重写为相对于初始位置的相对位移。
python
def rel_scan(detectors, motor, start, stop, num):
absolute = scan(detectors, motor, start, stop, num)
relative = relative_set_wrapper(absolute, [motor])
yield from relative
这是一个微妙却异常强大的特性。
像 relative_set_wrapper() 这样的包装器作用于生成器实例,例如 scan(...) 返回的实例。同时,也存在对应的装饰器函数(如 relative_set_decorator),它们作用于生成器函数本身,例如 scan() 函数。
python
# 使用装饰器修改一个生成器函数
def rel_scan(detectors, motor, start, stop, num):
@relative_set_decorator([motor])
def inner_relative_scan():
yield from scan(detectors, motor, start, stop, num)
yield from inner_relative_scan()
inner_relative_scan 只是一个内部变量名,那么我们为何选择这样一个冗长的名称?为什么不直接命名为 f?当然,使用 f 在功能上完全可行。但采用一个描述性的名称可以使调试过程更加容易。在处理复杂、深度嵌套的调用栈时,清晰的内部变量名能为我们提供宝贵的线索,帮助我们快速定位问题根源。
注意: 装饰器语法------即 @ 符号------是一种将函数传递给另一个函数的简洁方式。
python
@g
def f(...):
pass
f(...)
# 等价于
g(f)(...)
内建预处理器
以下每个名为 <某名称>_wrapper 的函数都作用于一个生成器实例。而对应的名为 <某名称>_decorator 的函数则作用于生成器函数本身。
|---------------------------|-----------------------------------------|
| baseline_decorator | 在open_run后记录所有设备基线的预处理器 |
| baseline_wrapper | 在open_run后记录所有设备基线的预处理器 |
| contingency_wrapper | try...except..else..finally帮助程序 |
| finalize_decorator | try..finally帮助程序 |
| finalize_wrapper | try..finally帮助程序 |
| fly_during_decorator | 触发并且采集运行时"飞行器"(异步采集)对象 |
| fly_during_wrapper | 触发并且采集运行时"飞行器"(异步采集)对象 |
| inject_md_decorator | 向运行注入更多元数据 |
| inject_md_wrapper | 向运行注入更多元数据 |
| lazily_stage_decorator | 这是一个插入'stage'消息和添加'unstage'的预处理器 |
| lazily_stage_wrapper | 这是一个插入'stage'消息和添加'unstage'的预处理器 |
| monitor_during_decorator | 在运行中监视(异步读取)设备 |
| monitor_during_wrapper | 在运行中监视(异步读取)设备 |
| relative_set_decorator | 解析设备'设置'消息为相对于初始位置 |
| relative_set_wrapper | 解析设备'设置'消息为相对于初始位置 |
| reset_positions_decorator | 结束时移动可移动设备到它们的初始位置 |
| reset_positions_wrapper | 结束时移动可移动设备到它们的初始位置 |
| run_decorator | 封装在 open_run 和 close_run 消息中。 |
| run_wrapper | 封装在 open_run 和 close_run 消息中。 |
| stage_decorator | 'Stage'设备(即,准备使用它们, 'arm'它们)并且接着unstage |
| stage_wrapper | 'Stage'设备(即,准备使用它们, 'arm'它们)并且接着unstage |
| stubs_decorator | 订阅对文档流的回调,最后,取消订阅 |
| stubs_wrapper | 订阅对文档流的回调,最后,取消订阅 |
| suspend_decorator | 对运行引擎安装挂起器, 并且在结束时移除它们 |
| suspend_wrapper | 对运行引擎安装挂起器, 并且在结束时移除它们 |
自定义预处理器
使用msg_mutator()(用于就地更改消息)和plan_mutator()(用于插入消息到计划或者移除消息)实现预处理器.
学习此的最简单方式是通过示例, 在plans模块的源代码中学习内建处理器的实现.
用per_step自定义步进扫描
一维与多维扫描计划均采用三部分结构:(1) 准备阶段,(2) 在每一点执行的多步执行循环,以及 (3) 清理阶段。
我们提供了一个用于自定义第(2)步的钩子。这使得您能够基于现有计划编写其变体,而无需从零开始。
对于一维计划, 默认内层循环是:
python
from bluesky.plan_stubs import checkpoint, abs_set, trigger_and_read
def one_1d_step(detectors, motor, step):
"""
1维步进扫描的内层循环
这是1维计划中"per_step"的默认函数
"""
yield from checkpoint()
yield from abs_set(motor, step, wait=True)
return (yield from trigger_and_read(list(detectors) + [motor]))
某些用户定义的有相同签名的函数custom_step可以在其位置上使用.
python
scan([det], motor, 1, 5, 5, per_step=custom_step)
为了方便, 这可以被封装到一个新计划的定义中:
python
def custom_scan(detectors, motor, start, stop, step, *, md=None):
yield from scan([det], motor, start, stop, step, md=md
per_step=custom_step)
对于多维计划, 默认的内层循环是:
python
from bluesky.utils import short_uid
from bluesky.plan_stubs import checkpoint, abs_set, wait, trigger_and_read
def one_nd_step(detectors, step, pos_cache):
"""
一个N维步进扫描的内层循环
这是ND计划中'per_step'的默认函数
参数:
----------
detectors : 可迭代, 要读取的设备
step : dict, 映射电机到在这个step中的位置
pos_cache : dict, 映射电机到末尾设置位置
"""
def move():
yield from checkpoint()
grp = short_uid('set')
for motor, pos in step.items():
if pos == pos_cache[motor]:
# 这个位置不移动这个电机
continue
yield from abs_set(motor, pos, group=grp)
pos_cache[motor] = pos
yield from wait(group=grp)
motors = step.keys()
yield from move()
yield from trigger_and_read(list(detectors) + list(motors))
同样, 一个相同签名的自定义函数可以被传递给任何多维扫描计划的per_step参数.
异步计划:"飞行扫描"和"监视"
关于这些术语的上下文说明及一些示例计划,请参阅 异步采集 部分,该章节末尾附近提供了相关示例。
这些是定义自定义计划和计划预处理器的有用工具.
|----------------|-------------------------------|
| pchain | 像itertools.chain但使用yield from |
| msg_mutator | 一种可在计划中修改或删除单个消息的简单预处理器。 |
| plan_mutator | 通过更改或插入消息来实现动态修改计划内容, |
| single_gen | 把单个消息变成计划 |
| make_decorator | 把一个生成器实例包装器变成一个生成器函数装饰器 |