Counter/Timer

cancel
Showing results for 
Search instead for 
Did you mean: 

Synchronizing analog output and counter input in USB-6211(OEM) using Python

Solved!
Go to solution

Hi all,

 

I am developing a python code to scan a laser beam over a transparent material and measure the transmitted intensity using a photodetector, which sends the intensity as counts.

 

I am using two analog outputs to control two galvanometers (one for x-direction control and one for y-direction control of laser). I am able to successfully control the galvanometers using the analog output channels of the usb-6211, and the following code is the short description of how I am scanning the sample.

 

from nidaqmx import stream_writers, Task

from nidaqmx.constants import AcquisitionType

import numpy as np

 

taskxy = Task()

taskxy.ao_channels.add_ao_voltage_chan('Dev1/ao0:1')

scanrate = 100  # the maximum scan rate I would be using is about 250

 

xarr = np.linspace(-3,3,50)

yarr = np.linspace(-3,3,9)

# xarr contains array of points to scan along x-direction (fast scan direction)

# yarr contains array of points to scan along y-direction (slow scan direction)

# scanning takes place by going through all values of xarr for fixed y-position of yarr[0] (line 1)

# then again scan over all x of fixed y-position yarr[1] (line 2), and so on...

def scanxy(xarr,yarr): 
   t = 10 # default timeout = 10 seconds
   sample = getxyarray(xarr,yarr)  # this function returns the full 2xn array of points, to scan the whole X*Y area
   sam_pc = np.shape(sample)[1]
   total_time = sam_pc*len(sample)/scanrate
   if t < total_time + 1:
       t = total_time + 2  # make sure that wait_until_done() works even when scanning takes more than 10 seconds
   taskxy.timing.cfg_samp_clk_timing(rate = scanrate+1, sample_mode = AcquisitionType.FINITE, samps_per_chan = sam_pc)
   taskxy.timing.cfg_samp_clk_timing(rate = scanrate, sample_mode = AcquisitionType.FINITE, samps_per_chan = sam_pc) 

 

"""Note: I need to do the above step step twice due to some hardware glitch, which results in wait_until_done() function useless if the function is called second time with the same scan parameters. So here, I change the scanrate to scanrate+1, and then again change it back to scanrate, so that the system thinks that I have changed the scan parameters. With this hack, I am able to run the function any number of times with or without changing the scan parameters"""
   

   writer = stream_writers.AnalogMultiChannelWriter(taskxy.out_stream,auto_start=False)   
   writer.write_many_sample(sample,timeout=t)
   taskxy.start()
   taskxy.wait_until_done(timeout = t)
   taskxy.stop()

Now, I need to configure the counter input, to collect the intensity of the transmitted beam over each position of the material. I just started reading about using counters, and currently, I am only able to collect the counts from the photodetector as on-demand edge-counting.

However, I need to synchronize the scanning of the laser with collection of the intensity from each point on the material.

After reading many topics in this forum, I realize that I need to use a buffered counting method. I think the best approach would be to collect the intensities of all points in one line and then collect the intensities from next line, and so on.. 

However, I am not sure how to synchronize the analog output and the counter input. 

The whole deal about sample clock, gate, etc. is making me very confused. I my current code, the analog output is using internal timing, but I read that I cannot use the same internal timer for the counter input. Is this correct? If this is true, then I need to use an external sample clock for analog output and also use the same sample clock for the counter as well. But I am not sure how to do this. Can anyone please guide me how to do it correctly?

 

Thanks in advance!

Badari

0 Kudos
Message 1 of 12
(3,089 Views)
Solution
Accepted by topic author badarinrao

I don't really know Python and especially not its syntax for DAQmx functions, but I can generally follow along with much of what you're doing.

 

I don't really know why your "hack" helps, nor why it seems to be needed.  The main thing I notice that *might* be relevant is that you create a task at the beginning of your code but never *clear* it at the end.  (Clearing is distinct from stopping, much like creating is distinct from starting.)

 

Now then, on to the counters.  Yes you need an external sample clock, but you can "borrow" it from your AO task.  Just configure your counter task timing to use the signal "/Dev1/ao/SampleClock" as its sample clock source.   I don't personally know the syntax for doing that in Python.

 

I'll also recommend that you continue to do edge counting.  You'll get a sample of cumulative counts each time the AO task generates a new sample.  Then you'll just need to do a simple finite difference to figure out the # counts at each individual AO state.  (There *IS* a period measurement mode that will internally re-zero the count register after each sample, but it's a little funky to try to explain, especially when we don't speak the same code language.)

 

You should start the counter task before starting the AO task.  And then you'll need to think through the little detail that if you sample your counts at the same instant that AO changes, you're actually capturing the response to the *previous* AO state.   One of those little "off-by-1" issues that arise pretty commonly.

 

 

-Kevin P

CAUTION! New LabVIEW adopters -- it's too late for me, but you *can* save yourself. The new subscription policy for LabVIEW puts NI's hand in your wallet for the rest of your working life. Are you sure you're *that* dedicated to LabVIEW? (Summary of my reasons in this post, part of a voluminous thread of mostly complaints starting here).
0 Kudos
Message 2 of 12
(3,060 Views)

Dear Kevin,

 

Thank you very much! I was able to synchronize the counter and analog output successfully. Also, could you please elaborate more regarding your last sentence? I thought since the same sample clock is used by both the ao channel as well as the counter, the problem stated by you will not occur. Am I wrong? Since this is my first time dealing with counters and synchronization, I am not familiar with this problem.

One more thing I need to figure out is how to start the counter exactly when the ao channel starts the scan? Is it possible send a trigger signal to the gate of the counter when ao task is started? Please let me know.

 

ALso, the reason I chose not to close the ao task in the function is that I thought it would be more efficient. I don't know if it is true or not, but somehow creating and destroying a task each time seems like a "heavy" operation in the background, please correct me if I am wrong. Since I am able to repeat the scans without having to close the task each time, I opted for it, and chose to close the task only when quitting the program. If you suggest otherwise, I will implement it accordingly.

 

Also, I have shared my updated code, it may be useful for others with similar problem.

 

from nidaqmx import stream_writers, Task

from nidaqmx.constants import AcquisitionType

import numpy as np

 

taskxy = Task()

counter = Task()

taskxy.ao_channels.add_ao_voltage_chan('Dev1/ao0:1')

counter.ci_channels.add_ci_count_edges_chan('Dev1/ctr0',initial_count=0,edge=Edge.RISING,count_direction=CountDirection.COUNT_UP)

counter.channels.ci_count_edges_term = '/Dev1/PFI0'

 

scanrate = 100  # the maximum scan rate I would be using is about 250

 

xarr = np.linspace(-3,3,50)

yarr = np.linspace(-3,3,9)

# xarr contains array of points to scan along x-direction (fast scan direction)

# yarr contains array of points to scan along y-direction (slow scan direction)

# scanning takes place by going through all values of xarr for fixed y-position of yarr[0] (line 1)

# then again scan over all x of fixed y-position yarr[1] (line 2), and so on...

 

t = 10 # default timeout = 10 seconds
sample = getxyarray(xarr,yarr)  # this function returns the full 2xn array of points, to scan the whole X*Y area
sam_pc = np.shape(sample)[1]
total_time = sam_pc*len(sample)/scanrate
if t < total_time + 1:
    t = total_time + 2  # make sure that wait_until_done() works even when scanning takes more than 10 seconds
counter.timing.cfg_samp_clk_timing(scanrate, source="/Dev1/ao/SampleClock",sample_mode=AcquisitionType.FINITE,samps_per_chan=sam_pc)

taskxy.timing.cfg_samp_clk_timing(rate = scanrate, sample_mode = AcquisitionType.FINITE, samps_per_chan = sam_pc) 

writer = stream_writers.AnalogMultiChannelWriter(taskxy.out_stream,auto_start=False)   
writer.write_many_sample(sample,timeout=t)

counter.start()

taskxy.start()
taskxy.wait_until_done(timeout = t)
taskxy.stop()

data = counter.read(number_of_samples_per_channel=sam_pc)

counter.stop()

individual_data = np.append(data[0],np.diff(data))

print(individual_data)

taskxy.close()

counter.close()

0 Kudos
Message 3 of 12
(3,048 Views)

RE: the "off by 1" problem. 

   What happens at the instant of the 1st AO sample clock?  The 1st AO state (x and y) is generated.  And the 1st instantaneous count value is buffered.  And then the 2nd AO sample clock?  The 2nd AO state is generated and the 2nd instantaneous count value is buffered.

   At first glance, that sounds good.  HOWEVER, when you think about it just a little bit more, you realize that the 2nd count value is capturing the response to the 1st AO state.  And the 3rd count value captures the response to the 2nd AO state.   Etc.   *That's* the off-by-1 issue.

   It isn't hard to deal with once you realize you need to.  I'd generate 1 extra AO sample that duplicates the final x,y state.  The sample clock used to generate it will capture the count value you need to know the response to that final state.  If you have N states for your AO x,y values, you need to capture N+1 samples of counts so you can do a finite difference over the resulting N intervals.

   If you do that, you won't need to bother with trying to trigger the counter task.  Honest.  The finite difference helps zero you out just as effectively as a hardware trigger would have.

 

RE: closing / clearing the task

   It wasn't entirely clear to me what part of the code would get called repeatedly.  You're right that creating and clearing/closing tasks add overhead that you can avoid.   I would separate your code into 3 distinct functions: Configure, Run, and Close.   The Configure function would end at "writer.write...", you'd call it once and hang onto the task variables that you just configured.  The Run function would span from "counter.start()" to "print()".   That's the only chunk you'd need to call repeatedly in your loop.

 

 

-Kevin P

CAUTION! New LabVIEW adopters -- it's too late for me, but you *can* save yourself. The new subscription policy for LabVIEW puts NI's hand in your wallet for the rest of your working life. Are you sure you're *that* dedicated to LabVIEW? (Summary of my reasons in this post, part of a voluminous thread of mostly complaints starting here).
0 Kudos
Message 4 of 12
(3,010 Views)

Dear Kevin,

 

Thank you for the clear explanation, I understand now.

 

RE: closing / clearing the task:

I was actually trying to include even a part of configuration into the repeating run function. Typically, we do a fast scan of a large area, and then select many smaller areas of interest from that image to do a slow, high-resolution scan.

I found that even when I include only the "counter.start()" to "print()" section in the repeating run function,  taskxy.wait_until_done() does not work after the first run. Even the taskxy.is_done() function is showing True when run() is called the second time. So I guess, there is some inherent bug that needs to be resolved.

Only when I slightly change the configuration (like the hack I mentioned before), I can successfully make the taskxy.wait_until_done() work even without freshly creating/closing the task.

 

-Badari

0 Kudos
Message 5 of 12
(2,948 Views)

Dear Kevin,

 

I am trying to implement the counter function using the "Period" measurement for my experiment as you had stated before. I think when I carry out long-time scans, this will be more useful.

The manual says that this measurement gives the number of ticks between two active edges of a gate signal. So this means that I need to connect the sample clock of the analog output as the terminal for period measurement, and the counter timebase source will be the actual input signal from the photodetector that I need to count. Is this correct?

 

Based on this approach, I tried the following code.

 

from nidaqmx import stream_writers, Task

from nidaqmx.constants import AcquisitionType, TimeUnits

import numpy as np

 

taskxy = Task()

counter = Task()

taskxy.ao_channels.add_ao_voltage_chan('Dev1/ao0:1')

counter.ci_channels.add_ci_period_chan('Dev1/ctr0',units=TimeUnits.TICKS)

counter.channels.ci_period_term = '/Dev1/ao/SampleClock'

counter.channels.ci_ctr_timebase_src='/Dev1/PFI0'

 

scanrate = 100  

 

xarr = np.linspace(-3,3,50)

yarr = np.linspace(-3,3,9)

t = 10 # default timeout = 10 seconds
sample = getxyarray(xarr,yarr)  # this function returns the full 2xn array of points, to scan the whole X*Y area
sam_pc = np.shape(sample)[1]
total_time = sam_pc*len(sample)/scanrate
if t < total_time + 1:
    t = total_time + 2  # make sure that wait_until_done() works even when scanning takes more than 10 seconds
counter.timing.cfg_samp_clk_timing(scanrate, source="/Dev1/ao/SampleClock",sample_mode=AcquisitionType.FINITE,samps_per_chan=sam_pc)

taskxy.timing.cfg_samp_clk_timing(rate = scanrate, sample_mode = AcquisitionType.FINITE, samps_per_chan = sam_pc) 

writer = stream_writers.AnalogMultiChannelWriter(taskxy.out_stream,auto_start=False)   
writer.write_many_sample(sample,timeout=t)

counter.start()

taskxy.start()
taskxy.wait_until_done(timeout = t)
taskxy.stop()

data = counter.read(number_of_samples_per_channel=sam_pc)

counter.stop()

print(data)

taskxy.close()

counter.close()

 

Though this code gives me the correct value of counts for each point, it only gives 205 out of the required 450 samples. And I also get a warning that finite acquisition has stopped before acquiring the requested number of samples.

I also tried to use the semi-period measurement as well as pulse-width measurement methods in a similar manner. While semi-period returns 450 samples, the problem here is that every alternate buffer value is 0. With pulse-width, I see that I obtain 410 samples out of the required 450 samples.

I guess this problem has to do with the nature of ao/sampleclock, but not able to figure out the exact cause.

 

Can you please help me out here?

 

Regards,

Badari

0 Kudos
Message 6 of 12
(2,775 Views)

You've got the right idea for period measurement.  Using the analog sample clock as the counter's terminal for period measurement is right -- that'll make you acquire both analog signals and photon counts at the same time.

 

The one thing I see that looks unexpected is this line:

 

counter.timing.cfg_samp_clk_timing(scanrate, source="/Dev1/ao/SampleClock",sample_mode=AcquisitionType.FINITE,samps_per_chan=sam_pc)

 

In LabVIEW, where I'm familiar with things, period measurement would declare "implicit timing" when setting up the timing config.   The idea is that sample timing isn't defined by a regular constant freq*clock*, it's implicit in the signal being measured.

   I'm not sure what you have there is wrong under Python, but it *does* look a little unexpected.

 

I don't see anything else that looks suspicious to me.

 

 

-Kevin P

CAUTION! New LabVIEW adopters -- it's too late for me, but you *can* save yourself. The new subscription policy for LabVIEW puts NI's hand in your wallet for the rest of your working life. Are you sure you're *that* dedicated to LabVIEW? (Summary of my reasons in this post, part of a voluminous thread of mostly complaints starting here).
0 Kudos
Message 7 of 12
(2,769 Views)

Dear Kevin,

 

Thank you for your comments. Yes, you were right regarding implicit timing for period measurement.

I found a function called cfg_implicit_timing which I can use instead of cfg_samp_clk_timing (see attached screenshot of the documentation).

 

However, the problem seems to have only partially improved. Now I get 410 samples out of the expected 450.

Another observation I made is that for the earlier code, when I used Falling Edges instead of Rising Edges, I got 410 samples, whereas Rising Edges had given only 205 samples. However, after specifying implicit timing, both Rising and Falling Edges are giving 410 samples. 

 

I feel that there could be some problem with the ao/SampleClock  I see in the documentation that the AO sample Clock in ni-6211 is output as an "active low signal" to any output PFI terminal. I don't know how this could affect the period measurement.

 

I will try some more tweaks to try and make it work, but in case you have any more ideas regarding what is causing this problem, please let me know.

 

Regards,

Badari

 

0 Kudos
Message 8 of 12
(2,746 Views)

I'm inclined to think there's something about your code that's leading to this discrepancy as I've *always* found the use of internal signal routing (such as the AO sample clock routed for use in a counter task) to be rock solid.

 

You appear to be explicitly requesting to read 'sam_pc' samples from the task.  Have you verified that sam_pc=450?   When you (apparently) only retrieve 410 samples, have you checked for errors or warnings from DAQmx?   Especially a timeout?   It isn't normal for DAQmx to give you less samples than you asked for unless it also gives you a warning or error.

 

Do some debugging on your variables and expressions.  It appears that 'sample' is a 2D array but you're calculating a timeout value with "len(sample)".  I have no clear idea what Python thinks is the len() of a 2D array.

 

Further, are you sure that your other timeout-related variables and calcs are using consistent units?  The code appears to assume seconds -- have you verified this?

 

You've reported a pretty consistent misbehavior so something is systematically wrong.  I'd start with extreme scrutiny of the code.   Like I said before, I've had a *ton* of perfect experience with routing internal signals, no weird anomalies ever.

 

 

-Kevin P

CAUTION! New LabVIEW adopters -- it's too late for me, but you *can* save yourself. The new subscription policy for LabVIEW puts NI's hand in your wallet for the rest of your working life. Are you sure you're *that* dedicated to LabVIEW? (Summary of my reasons in this post, part of a voluminous thread of mostly complaints starting here).
0 Kudos
Message 9 of 12
(2,735 Views)

Dear Kevin,

 

Yes I have verified that sam_pc is 450, and as I had mentioned before, I indeed get a warning saying finite acquisition has stopped before requested number of samples were acquired, and the warning code is 200010.

 

Also, the scanning time for 450 points at scan-rate of 100 is about 4.5 seconds, which is less than the default value of 10 seconds. The value of t (timeout) changes only if the total_time is greater than 10 seconds. Thank you for pointing out the mistake in calculation of total_time, the correct time should be "total_time = sam_pc/scanrate", which still doesn't affect the present problem.

 

Also, you may note that the same code works well when I use edge counting method, so I don't think there is any problem with the scanning and counting code. Since you confirm that there can be no anomalies with the sample clock, I guess I have not implemented period measurement correctly. I will check the manual once again and try to figure it out.

 

The consistent misbehavior is what is bugging me the most. During debugging, instead of sending signals from the photodetector, I sent constant frequency pulses from a frequency generator. So if I send pulses with a frequency of 10 kHz and scanrate is 100, then each point will see 100 pulses. While using edge counting method, I can correctly get the 100 counts for each point, and all the 451 samples are generated. Even for the period measurement, each point correctly counts 100 pulses, but the number of samples obtained is less than expected. 

 

After reading your comments, even I feel the counter could be timing out before all the samples have been collected, as any other problem should have given incorrect number of counts for each point. There could be some problem with the timeout handling in the nidaqmx python module. I will check and keep you posted.

 

Thanks!

0 Kudos
Message 10 of 12
(2,731 Views)