02-25-2025 12:53 PM - edited 02-25-2025 01:50 PM
Hello!
I am working on code to write a sinusoidal signal with frequency 71 Hz and varying amplitude from an AO channel. I would like the amplitude of the signal to ramp up at a constant rate from 0 to A. Once the amplitude reaches A, the signal should continue indefinitely with regeneration until interrupted by user input. Upon user input, the signal should ramp back down to 0. An example is illustrated below.
I believe the ramp up/down outputs should utilize FINITE sample mode (it only lasts for a finite time until the signal reaches amplitude A or 0), whereas the steady-state output should utilize CONTINUOUS sample mode (it lasts for an indefinite amount of time until user keyboard input). Furthermore, the ramp up/down should transition seamlessly with the steady-state portion, without any discontinuities in voltage. I am not sure how to achieve this.
I have tried writing different variations of code with the nidaqmx Python API to achieve this. A snippet from one variation is shown below:
electrode_dict = {'electrode_x+':'PXI1Slot6/ao17', 'electrode_x-':'PXI1Slot6/ao21', 'electrode_y+':'PXI1Slot6/ao0', 'electrode_y-':'PXI1Slot6/ao8', 'electrode_z+':'PXI1Slot6/ao12', 'electrode_z-':'PXI1Slot6/ao4'}
electrode_x_dict = {'electrode_x+':'PXI1Slot6/ao17', 'electrode_x-':'PXI1Slot6/ao21'}
electrode_y_dict = {'electrode_y+':'PXI1Slot6/ao0', 'electrode_y-':'PXI1Slot6/ao8'}
electrode_z_dict = {'electrode_z+':'PXI1Slot6/ao12', 'electrode_z-':'PXI1Slot6/ao4'}
def XeFlashes_new(task_spin, sampling_rate_AO, f_e=6.23e3, amp_e=2, ramp_rate_e=0.05, axis='none'):
duration_ramp = amp_e/ramp_rate_e
waveforms_up = []
waveforms_down = []
waveforms_spin = []
if not axis=='none':
# choose which channel(s) to output waveform(s) on
if axis == 'X':
spin_electrodes = [item for pair in zip(electrode_y_dict.values(), electrode_z_dict.values()) for item in pair]
elif axis == 'Y':
spin_electrodes = [item for pair in zip(electrode_z_dict.values(), electrode_x_dict.values()) for item in pair]
elif axis == 'Z':
spin_electrodes = [item for pair in zip(electrode_x_dict.values(), electrode_y_dict.values()) for item in pair]
else:
raise Exception("'axis' parameter is not recognized. Should be 'X', 'Y', or 'Z'.")
for vv in enumerate(spin_electrodes):
# add analog output channels for electrodes
cc = task_spin.ao_channels.add_ao_voltage_chan(vv[1], min_val=-4, max_val=4)
cc.ao_term_cfg = TerminalConfiguration.RSE
# ramping time
t_ramp = np.linspace(0, duration_ramp, int(duration_ramp * sampling_rate_AO), endpoint=False)
# create waveforms for ramp up
waveform_ramp_up = (t_ramp * ramp_rate_e) * np.sin(2 * np.pi * f_e * t_ramp + vv[0] * np.pi/2)
waveforms_up.append(waveform_ramp_up)
# create waveforms for ramp down
waveform_ramp_down = (amp_e - t_ramp * ramp_rate_e) * np.sin(2 * np.pi * f_e * t_ramp + vv[0] * np.pi/2)
waveforms_down.append(waveform_ramp_down)
# create one period of waveforms for continuous section
t = np.linspace(0, 1/f_e, int(1/f_e * sampling_rate_AO), endpoint=False)
waveform = amp_e * np.sin(2 * np.pi * f_e * t + vv[0] * np.pi/2) # each adjacent electrode is phase-shifted by pi/2
waveforms_spin.append(waveform)
# allow regeneration
task_spin.regen_mode = RegenerationMode.ALLOW_REGENERATION
# configure timing
task_spin.timing.samp_clk_src='OnboardClock'
task_spin.timing.samp_clk_rate = sampling_rate_AO
task_spin.timing.cfg_samp_clk_timing(rate=sampling_rate_AO,
sample_mode=AcquisitionType.CONTINUOUS)
# ramp up for finite amount of time
task_spin.write(np.array(waveforms_up))
task_spin.start()
task_spin.wait_until_done(timeout=duration_ramp+1)
task_spin.stop()
# output sinusoids until user keyboard input
task_spin.write(np.array(waveforms_spin))
task_spin.start()
input('Generating voltage continously with regeneration. Press Enter to stop.\n')
task_spin.stop()
# ramp down for finite amount of time
task_spin.write(np.array(waveforms_down))
task_spin.start()
task_spin.wait_until_done(timeout=duration_ramp+1)
task_spin.stop()
return
Currently I encounter an issue where wait_until_done() cannot be used in conjunction with AcquisitionType.CONTINUOUS, as it is intended for finite acquisition. I have also tried controlling the same channel with two separate tasks (one task created with finite sample mode, the other with continuous sample mode), but this results in a resource error, as nidaqmx does not allow the same channel to be controlled by two tasks simultaneously.
A similar functionality has been successfully implemented in LabVIEW, so I believe it is achievable in Python as well. I'm open to any suggestions on how to fix/rewrite my code.
Thank you!
02-25-2025 04:03 PM
You want Continuous mode from the first sample to the last sample. A task is a grouping of channel and timing information - you should not change the timing mode while the task is running.
If you are tolerant of glitches, allow regeneration. The LabVIEW Sound and Vibration Toolkit supports output with ramp up and ramp down. SVT went with design decisions a long time ago to prefer DSA hardware and to never allow regeneration. It was the only way to guarantee smooth, deterministic test signals. While you are using the DAQmx Python API, the DAQmx concepts should translate.
Sample Mode = Continuous
Allow Regeneration = False
02-26-2025 08:04 AM
I second the motion -- you need to be in continuous sampling mode throughout. It's not possible to change the timing mode between continuous and finite without adding some kind of anomaly to the waveform (and it would be difficult to control or know exactly what kind of anomaly you'd be getting. It would depend on several things, some of them out of your control.)
Following are some tips and things to consider. I'll assume NON-regeneration throughout because it lets you service your task in a consistent way throughout the whole generation process.
1. There will always be *some* latency in a buffered output task between the time when you calculate and write new data to the task and when that data becomes a signal in the real world.
2. There are DAQmx properties and techniques that can help reduce that latency, but you need to be careful. At the extreme of reducing it *too* much, you risk a task-killing buffer underflow error that stops your waveform suddenly. You need to find an appropriate risk/reward tradeoff for your specific app.
Specifics of how to reduce latency can vary by device and are a bigger topic than I can address right now. Some searches here about latency may yield further help in the meantime.
3. There's a very general rule of thumb around here for servicing buffered tasks. It says that most tasks perform reliably for the long term if you interact with them at ~10 Hz, i.e., 100 msec of data at a time.
Note that in the case of output tasks, this does NOT limit your total latency to 100 msec. Some devices have large onboard FIFO buffers that could give you *seconds* worth of latency unless you explicitly configure them otherwise.
4. You'll likely make your life easier if each chunk of data represents an integer number of 71 Hz sine cycles. You can create this array one time only and use it throughout. You'd just need to know when you're ramping (and where you are within the ramp) so you can multiply the array elements by the appropriate portion of a 0->1 or 1->0 ramp.
5. 7 full cycles at 71 Hz would get you pretty close to the 10 Hz servicing rule of thumb target. 10 full cycles might be easier to think about and gets you in the same ballpark at 7.1 Hz.
6. You can afford to define the sine waves with pretty high resolution, say 100 samples per cycle or more. That would be a data acq rate of 7100 Hz, which is not generally difficult to achieve.
7. Here's a very picky detail that you can *probably* ignore. It's probably not possible to set up a sample rate that's *exactly* 7100 Hz. You can only do integer divisors of your device's internal timebase clock. My quick calc suggests you may get an actual sample rate that misses your 7100 Hz target by ~0.25 Hz. Again, my guess is that you can probably decide to just live with this.
Summary: there's kind of a lot to have to deal with, but you very likely *can* get there from here. The more latency you can accommodate from user keystroke to real world signal change, the easier your coding job gets.
-Kevin P
02-26-2025 06:08 PM - edited 02-26-2025 06:12 PM
Kevin's point about latency is real, especially for platforms such as ethernet cDAQ where the DAQmx driver doesn't limit the latency. SVT compares generated waveform time to system time (and waits if necessary) to avoid building a long latency. Here is a shipping example with the maximum latency property added:
Here is what the generated output looks like:
I know you are using Python, so I throw this example your way to let you know that your task is achievable. Since you posted in the LabVIEW forum, I'm hoping there might also be a chance you are considering LabVIEW 😉. All kidding aside, I think it is worth you considering the value of your time.
If you are outputting sine waves on one or two ao channels, use the free Dynamic Signal Generator (DSG) (https://www.ni.com/en/support/downloads/drivers/download.dsa-soft-front-panels.html#554445).