ni / nidaqmx-python

A Python API for interacting with NI-DAQmx
Other
413 stars 155 forks source link

Instability of continuously acquired signals #590

Open muyichun opened 1 month ago

muyichun commented 1 month ago
image
import os
import time
import h5py
import nidaqmx
import numpy as np
from nidaqmx.constants import AcquisitionType, Edge
def main():
    tmp_list = []
    data = np.zeros(400)
    with nidaqmx.Task() as task:
        def callback(task_handle, every_n_samples_event_type, number_of_samples, callback_data):
            # reader.read_many_sample(data, number_of_samples_per_channel=number_of_samples,timeout=0)
            # print(data[0])
            arr = task.read(number_of_samples_per_channel=number_of_samples)
            tmp_list.extend(arr)
            return 0
        task.ai_channels.add_ai_voltage_chan("Dev1/ai0")
        task.timing.cfg_samp_clk_timing(2e6, "", Edge.RISING, sample_mode=AcquisitionType.CONTINUOUS)
        task.triggers.start_trigger.cfg_dig_edge_start_trig("/Dev1/PFI0", Edge.RISING)
        task.register_every_n_samples_acquired_into_buffer_event(400, callback)
        task.start()
        input("Running task. Press Enter to stop.\n")
        task.stop()
        path = "D:/HGY_DATA/test_daq/xj1.h5"
        os.remove(path)
        with h5py.File(path, 'a') as file:
            dataset = file.create_dataset(
                'Imaging data_' + time.strftime("%Y_%m_%d_%H_%M_%S", time.localtime())
                , shape=(0, 400)
                , maxshape=(None, 400)
                , dtype=np.float32
                , chunks=(1, 400))
            # 初始化创建字符串属性
            dataset.attrs['Display Time'] = 0
            # 追加写入新数据
            current_len = len(dataset)
            result_data = np.array(tmp_list).reshape((-1, 400))
            dataset.resize(current_len + result_data.shape[0], axis=0)
            dataset[current_len:] = result_data
    print("over")
if __name__ == "__main__":
    main()

Acquisition Card:NI PCIe-6361

Same environment, I use the Python API to continuously capture a standard sine wave from a signal generator. As a result, the captured data is not one cycle per line (the first amplitude value of each line is decreasing gradually), but it is normal if the data is captured using LabVIew. This question has been bothering me for days!!!

### Tasks
zhindes commented 1 month ago

Can you share a screenshot of your LV code and the values of any controls that are being passed into the DAQmx VIs? For what its worth, my hunch is that your channel config may be different. This code is hiding a lot of default parameter values, the one that might be at fault is the Terminal Configuration.

        task.ai_channels.add_ai_voltage_chan("Dev1/ai0")

The full declaration:

    def add_ai_voltage_chan(
            self, physical_channel, name_to_assign_to_channel="",
            terminal_config=TerminalConfiguration.DEFAULT, min_val=-5.0,
            max_val=5.0, units=VoltageUnits.VOLTS, custom_scale_name=""):
muyichun commented 1 month ago
image image image image

Thank you very much for your prompt reply!

Above is a screenshot of LabVIew, which is one that collects data and then saves it to disk. The sampling frequency is 2M/s and the signal generator 5000Hz. I did find the Python API manual, but still don't know what to do with it!

zhindes commented 1 month ago

The primary configuration difference I see is create channel, like I expected. Try this in your Python code:

        task.ai_channels.add_ai_voltage_chan("Dev1/ai0", terminal_config=TerminalConfiguration.DIFF, min_val=-10.0, max_val=10.0)
muyichun commented 1 month ago
image

It still doesn't seem to work.

zhindes commented 1 month ago

Oh, bah. I thought we were chasing an absolute accuracy issue. I see that those samples are the first point in a sine-wave, and you even said:

As a result, the captured data is not one cycle per line (the first amplitude value of each line is decreasing gradually), but it is normal if the data is captured using LabVIew.

My reading comprehension is poor today, I guess.

The other major thing that your VI is doing that Python is that it is a Finite Retriggered Acquisition. Try this:

        task.timing.cfg_samp_clk_timing(2e6, "", Edge.RISING, sample_mode=AcquisitionType.Finite, samps_per_chan=400)
        task.triggers.start_trigger.cfg_dig_edge_start_trig("/Dev1/PFI0", Edge.RISING)
        task.triggers.start_trigger.retriggerable = True
muyichun commented 1 month ago

Yes.It's too hard for me. Finite Retriggered Acquisition should be used, but how do you guarantee that he is infinitely acquiring? Right now it stops the task automatically after it has collected enough samples. I tried using callback function and writing in while loop respectively, it can't acquire infinitely.

zhindes commented 1 month ago

A finite retriggered acq shouldn't stop; it effectively behaves as a continuous acquisition. Can you share the most recent code that you have tried?

muyichun commented 1 month ago
image
import nidaqmx
from nidaqmx.constants import AcquisitionType, Edge, TerminalConfiguration, OverwriteMode
def main():
    with nidaqmx.Task() as task:
        def callback(task_handle, every_n_samples_event_type, number_of_samples, callback_data):
            arr = task.read(number_of_samples_per_channel=400)
            print(arr[0])
            return 0

        task.ai_channels.add_ai_voltage_chan("Dev1/ai0",terminal_config=TerminalConfiguration.DIFF, min_val=-10.0, max_val=10.0)
        task.timing.cfg_samp_clk_timing(2e6, "", Edge.RISING, sample_mode=AcquisitionType.FINITE, samps_per_chan=400*2000000)
        task.triggers.start_trigger.cfg_dig_edge_start_trig("/Dev1/PFI0", Edge.RISING)
        task.triggers.start_trigger.retriggerable = True
        task.register_every_n_samples_acquired_into_buffer_event(400, callback)
        task.start()
        input("Running task. Press Enter to stop.\n")
    print("over")

if __name__ == "__main__":
    main()

When I adjust the value of samps_per_chan, the first point of the sine function still changes when set very large.

My need is to keep getting the standard sine wave data point set over and over again, but the results are changing. It's too hard for me.

zhindes commented 1 month ago

samps_per_chan=400*2000000

That is acquiring a lot of data per trigger. The 400 samples callback will be invoked 2 million times per trigger. I am assuming 400 samples is a single cycle of the sine wave. If so, what you're seeing is time quantization errors from the clocks drifting between your acquisition device (the 6361) and whatever is generating the Sine Wave over a long period of time.

The clock of the DAQ device is a 100MHz oscillator that has up to 50ppm of error. So imagine your 6361 and your sinewave generation have 50ppm of error in opposite directions. The 6361 clock is actually 99.995MHz. In 400 seconds (400 2e6 samples acquired at 2e6 sample rate), we would expect 100M 400 clock ticks. At 99.995MHz that will take 400.02 seconds. If your sinewave generation is using a similar clock, but with the opposite error, 100.005MHz, then those same number of clock ticks will take 399.98 seconds. That is .04 seconds of skew between the two clocks. When you're sampling 2M times per second, that drift is significant - .04 seconds is 80,000 samples.

I don't know where the PFI0 signal is coming from, but if its somehow referenced to that sine wave, then you're good.

When I adjust the value of samps_per_chan, the first point of the sine function still changes when set very large.

Correct. You want that to be a single cycle of your sine wave because of the drift I describe above. 400 samples, maybe?