3个版本 (破坏性更新)
0.3.0 | 2023年9月7日 |
---|---|
0.2.0 | 2023年9月4日 |
0.1.0 | 2023年8月28日 |
#9 in #ni
在 niexpctrl_backend 中使用
155KB
1.5K SLoC
National Instrument (NI) 与 nicompiler_backend
的集成
National Instrument (NI) 长期以来一直是构建实验控制系统的首选,这得益于其硬件的通用性、成本效益、可扩展性和稳健的文档。其大量的文档涵盖了从系统设计(NI-DAQmx 文档)到 ANSI C 和 Python 的API。
虽然NI提供了对其硬件的精细控制,但现有的驱动程序存在以下挑战
现有实现中的挑战
1. 流式传输不足
NI驱动程序虽然功能丰富,但要求输出信号必须预采样并转发到设备的输出缓冲区。考虑一个运行时间较长的实验(例如,10分钟)且需要高时间分辨率的场景(例如,10个模拟f64通道的1MHz)。预采样整个波形将变得既计算密集又内存密集(需要约~44.7Gb的存储空间)。更实际的方法是流式传输信号,其中只采样并转发信号的一部分,同时执行前面的块。这种方法可以减少内存和计算开销,同时保持信号完整性。
2. 以设备为中心的抽象
NI驱动程序通常在设备级别进行接口,软件“任务”实体对应于特定的设备通道。然而,现代实验通常需要超出单个NI卡的能力。使用由多个设备组成的NI实验控制系统需要同时管理多个设备任务,这是一个充满复杂性的问题。理想情况下,研究人员应该整体地与整个系统接口,而不是与单个设备和它们的并发任务纠缠。请参阅Device
以获取有关同步的更多详细信息。
3. 高级与低级实现的权衡
低级系统实现提供了灵活性和性能,但牺牲了开发便捷性。相反,基于Python的解决方案鼓励快速开发,但可能受到性能瓶颈的影响,尤其是在处理跨多个设备的并发流时。
介绍 nicompiler_backend
nicompiler_backend
被设计用来解决这些挑战。在其核心,它利用了 Rust 的性能和安全保证,以及其方便的 C 和 Python 接口。通过无缝与 NI-DAQmx C 驱动库接口,并通过 PyO3
提供 Python API,nicompiler_backend
提供了两者之长。配合可选的高级 Python 包装器,研究人员可以使用表达式丰富的语言设计实验,而将 Rust 后端处理流和并发。
目前,此crate支持模拟和数字输出任务,以及通过共享启动触发器、采样时钟或锁相参考时钟在 NI 设备之间进行同步。
示例用法
Rust
use nicompiler_backend::*;
let mut exp = Experiment::new();
// Define devices and associated channels
exp.add_ao_device("PXI1Slot3", 1e6);
exp.add_ao_channel("PXI1Slot3", 0);
exp.add_ao_device("PXI1Slot4", 1e6);
exp.add_ao_channel("PXI1Slot4", 0);
exp.add_do_device("PXI1Slot6", 1e7);
exp.add_do_channel("PXI1Slot6", 0, 0);
exp.add_do_channel("PXI1Slot6", 0, 4);
// Define synchronization behavior:
exp.device_cfg_trig("PXI1Slot3", "PXI1_Trig0", true);
exp.device_cfg_ref_clk("PXI1Slot3", "PXI1_Trig7", 1e7, true);
exp.device_cfg_trig("PXI1Slot4", "PXI1_Trig0", false);
exp.device_cfg_ref_clk("PXI1Slot4", "PXI1_Trig7", 1e7, false);
exp.device_cfg_samp_clk_src("PXI1Slot6", "PXI1_Trig7");
exp.device_cfg_trig("PXI1Slot6", "PXI1_Trig0", false);
// PXI1Slot3/ao0 starts with a 1s-long 7Hz sine wave with offset 1
// and unit amplitude, zero phase. Does not keep its value.
exp.sine("PXI1Slot3", "ao0", 0., 1., false, 7., None, None, Some(1.));
// Ends with a half-second long 1V constant signal which returns to zero
exp.constant("PXI1Slot3", "ao0", 9., 0.5, 1., false);
// We can also leave a defined channel empty: the device / channel will simply not be compiled
// Both lines of PXI1Slot6 start with a one-second "high" at t=0 and a half-second high at t=9
exp.high("PXI1Slot6", "port0/line0", 0., 1.);
exp.high("PXI1Slot6", "port0/line0", 9., 0.5);
// Alternatively, we can also define the same behavior via go_high/go_low
exp.go_high("PXI1Slot6", "port0/line4", 0.);
exp.go_low("PXI1Slot6", "port0/line4", 1.);
exp.go_high("PXI1Slot6", "port0/line4", 9.);
exp.go_low("PXI1Slot6", "port0/line4", 9.5);
// Compile the experiment: this will stop the experiment at the last edit-time plus one tick
exp.compile();
// We can compile again with a specific stop_time (and add instructions in between)
exp.compile_with_stoptime(10.); // Experiment signal will stop at t=10 now
assert_eq!(exp.compiled_stop_time(), 10.);
Python
功能上相同的代码,另外还采样并绘制了 PXI1Slot6/port0/line4
的信号。Experiment
对象的主要目标是公开一组快速实现的 Rust 方法,用于与 NI 实验接口。可以通过在另一个 Python 代码层包裹 nicompiler_backend
模块来轻松自定义语法糖和高级抽象,请参阅我们的 项目页面 了解此类示例。
# Instantiate experiment, define devices and channels
from nicompiler_backend import Experiment
import matplotlib.pyplot as plt
exp = Experiment()
exp.add_ao_device(name="PXI1Slot3", samp_rate=1e6)
exp.add_ao_channel(name="PXI1Slot3", channel_id=0)
exp.add_ao_device(name="PXI1Slot4", samp_rate=1e6)
exp.add_ao_channel(name="PXI1Slot4", channel_id=0)
exp.add_do_device(name="PXI1Slot6", samp_rate=1e7)
exp.add_do_channel(name="PXI1Slot6", port_id=0, line_id=0)
exp.add_do_channel("PXI1Slot6", port_id=0, line_id=4)
# Define synchronization behavior
exp.device_cfg_trig(name="PXI1Slot3", trig_line="PXI1_Trig0", export_trig=True)
exp.device_cfg_ref_clk(name="PXI1Slot3", ref_clk_line="PXI1_Trig7",
ref_clk_rate=1e7, export_ref_clk=True)
exp.device_cfg_trig(name="PXI1Slot4", trig_line="PXI1_Trig0", export_trig=False)
exp.device_cfg_ref_clk(name="PXI1Slot4", ref_clk_line="PXI1_Trig7",
ref_clk_rate=1e7, export_ref_clk=False)
exp.device_cfg_samp_clk_src(name="PXI1Slot6", src="PXI1_Trig7")
exp.device_cfg_trig(name="PXI1Slot6", trig_line="PXI1_Trig0", export_trig=False)
# Define signal
# Arguments of "option" type in rust is converted to optional arguments in python
exp.sine(dev_name="PXI1Slot3", chan_name="ao0", t=0., duration=1., keep_val=False,
freq=7., dc_offset=1.)
exp.constant(dev_name="PXI1Slot3", chan_name="ao0", t=9., duration=0.5, value=1., keep_val=False)
exp.high("PXI1Slot6", "port0/line0", t=0., duration=1.)
exp.high("PXI1Slot6", "port0/line0", t=9., duration=.5)
exp.go_high("PXI1Slot6", "port0/line4", t=0.)
exp.go_low("PXI1Slot6", "port0/line4", t=1.)
exp.go_high("PXI1Slot6", "port0/line4", t=9.)
exp.go_low("PXI1Slot6", "port0/line4", t=9.5)
exp.compile_with_stoptime(10.)
# Returns a 100-element vector of float
sig = exp.channel_calc_signal_nsamps("PXI1Slot6", "port0/line4", start_time=0., end_time=10., num_samps=100)
plt.plot(sig)
导航 Crate
nicompiler_backend
crate 组织为主要模块 - experiment
、device
、channel
和 instruction
。每个模块在 crate 中都有特定的功能。以下是一个快速指南,帮助您导航
experiment
模块:您的起点
如果您是典型用户,您可能的大部分时间都会在这里。
- 概述:
Experiment
被视为一系列设备,每个设备都由 NI 驱动器识别的名称标识。 - 用法:
Experiment
对象是暴露给 Python 的主要实体。它提供用于实验范围、设备范围和通道范围的操作方法。 - 关键特性和实现:请参阅
BaseExperiment
特性以获取 Rust 方法和用法示例。对于 Python 方法签名,请检查Experiment
的直接实现,它只是简单地包装了BaseExperiment
实现。
device
模块:深入了解设备
如果您想了解或自定义特定于设备的详细信息,则此模块适合您。
- 概述:每个
Device
都与控制系统中的独特硬件相关联。它包含基本元数据,如物理名称、采样率和触发行为。 - 关键特性与实现:请参阅
BaseDevice
特性以及整个device
模块以获取更多信息。设备还包含一组通道,每个通道都通过其物理名称进行引用。
channel
模块:通道指令与行为
适合那些想了解指令如何管理或需要设计新的 TaskType
以及针对 TaskType
定制的通道行为的开发者。
instruction
模块:深入指令解析
对于那些对指令的定义和执行细节感兴趣的人。
- 概述:每个
InstrBook
包含一个与编辑时元数据相关联的Instruction
,如start_pos
、end_pos
和keep_val
。一个Instruction
由指令类型 (InstrType
) 和一组键值对形式的参数组成。
我们鼓励用户探索每个模块,以全面理解 crate 的功能和结构。无论是快速设置还是贡献,nicompiler_backend
crate 都旨在满足这两种需求。
依赖
~8–14MB
~182K SLoC