#devices #channel #instructions #experiment #ni #back-end #instrument

bin+lib nicompiler_backend

为National Instrument (NI)集成提供的后端接口,提供具有Rust性能和安全保证的简化实验控制系统

3个版本 (破坏性更新)

0.3.0 2023年9月7日
0.2.0 2023年9月4日
0.1.0 2023年8月28日

#9 in #ni


niexpctrl_backend 中使用

MIT 许可证

155KB
1.5K SLoC

National Instrument (NI) 与 nicompiler_backend 的集成

National Instrument (NI) 长期以来一直是构建实验控制系统的首选,这得益于其硬件的通用性、成本效益、可扩展性和稳健的文档。其大量的文档涵盖了从系统设计(NI-DAQmx 文档)到 ANSI CPython 的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 组织为主要模块 - experimentdevicechannelinstruction。每个模块在 crate 中都有特定的功能。以下是一个快速指南,帮助您导航

experiment 模块:您的起点

如果您是典型用户,您可能的大部分时间都会在这里。

  • 概述Experiment 被视为一系列设备,每个设备都由 NI 驱动器识别的名称标识。
  • 用法Experiment 对象是暴露给 Python 的主要实体。它提供用于实验范围、设备范围和通道范围的操作方法。
  • 关键特性和实现:请参阅 BaseExperiment 特性以获取 Rust 方法和用法示例。对于 Python 方法签名,请检查 Experiment 的直接实现,它只是简单地包装了 BaseExperiment 实现。

device 模块:深入了解设备

如果您想了解或自定义特定于设备的详细信息,则此模块适合您。

  • 概述:每个 Device 都与控制系统中的独特硬件相关联。它包含基本元数据,如物理名称、采样率和触发行为。
  • 关键特性与实现:请参阅 BaseDevice 特性以及整个 device 模块以获取更多信息。设备还包含一组通道,每个通道都通过其物理名称进行引用。

channel 模块:通道指令与行为

适合那些想了解指令如何管理或需要设计新的 TaskType 以及针对 TaskType 定制的通道行为的开发者。

  • 概述:一个 Channel 表示 NI 设备上的一个特定物理通道。它管理一系列非重叠的 InstrBook,在编译后可以采样以生成浮点信号。

instruction 模块:深入指令解析

对于那些对指令的定义和执行细节感兴趣的人。

  • 概述:每个 InstrBook 包含一个与编辑时元数据相关联的 Instruction,如 start_posend_poskeep_val。一个 Instruction 由指令类型 (InstrType) 和一组键值对形式的参数组成。

我们鼓励用户探索每个模块,以全面理解 crate 的功能和结构。无论是快速设置还是贡献,nicompiler_backend crate 都旨在满足这两种需求。

依赖

~8–14MB
~182K SLoC