Hi, I am using variant CU3. I have two Zotinos in my crate, and I wish to use it (26 channels spread across both Zotinos) to give a coordinated waveform across all 26 channels. These waveforms are used for axial confinement of our multiplexed ion trap.

I discretized the waveform (a sinusoid with well defined initial phase [arbitrary] and frequency [25 Hz]) into 10 points per period. I have a table of these values self.Vs to write and the delays self.delays between each write. I have an execute function that sets DAC values:

    def execute(self,handle):
        """ Run DAC sequences on ARTIQ """

        for i in range(len(self.chs)):

            chs0,Vs0 = self.chs[i][0], self.Vs[i][0]
            chs1,Vs1 = self.chs[i][1], self.Vs[i][1]
            if bool(Vs0):
                handle.zotino0.set_dac(Vs0,channels=chs0)
            if bool(Vs1):
                handle.zotino1.set_dac(Vs1,channels=chs1)
            try:
                delay(self.delays[i])
                # at_mu(now_mu()+handle.core.seconds_to_mu(self.delays[i]))
            except IndexError:
                pass

Looking at the waveform generated from the DACs on a scope, I see that not all values are written (the first few points of the waveform are fine, but it would stop updated after some number of points). The point at which it stops updating varies from shot to shot. I am guessing that this is due to the system not writing fast enough, but the manual seems to suggest a 1.5 µs dead time between writes, which is much faster than time between writes (4 ms).

I also tried using at_mu instead of delay.

Is there a much more efficient way to write to the Zotinos? Or am I doing this wrong? Please help, thank you!

Are there any errors logged (including in the core device output)?

What happens with simpler code e.g. with constant delay, no try/finally (note that catching an exception is slow and may well be the source of the problem), no conditional updates based on the value of Vsx?

6 days later

Thanks for the reply! We managed to mitigate the problem somehow. We are not exactly sure why the mitigation works, but at least it works, for now.

Here's some additional information in case there are people who are interested: the execute method above belongs to a "DAC helper" that we wrote. The DAC helper has methods that takes in values for voltages of waveforms that we wish to generate that correspond to various parts of the experiment, e.g. <DAC_helper>.translate_ions(N=1, t=1*ms) would generate a table of discretized values that modulate the voltages of all the relevant channels in our DACs in a sine for N=1 cycle in a time t=1*ms. We also have a "TTL helper" that takes in all the TTL related sequences and converts them into a table of when each TTL channel turns on or off.

It turns out that if a "TTL helper" that runs before a DAC helper (i.e, <TTL_helper>.execute() is >=1 line above <DAC_helper>.execute()) and the "TTL helper" has a sequence that occurs after some of the DAC sequences, some of the remaining DAC sequences will not execute. This also happens even if the exact time at which the TTL event happens is different from when the DACs are supposed to update their values. We used to believe that as long as we do not have more than 8 events happening at the exact same time (for an 8 laned SED) this would not happen, and we certainly do not have 8 ttl.on() or ttl.off() or zotino.set_dac(...) happening at the exact same at_mu(), but it seems like we do not fully understand how the system works.

We mitigate this problem by splitting the "TTL helper" into multiple "TTL helpers", such that the time at which they are active do not overlap with that of the "DAC helper".

For a more concrete example, here's one where we had to split self.ttl into self.ttl0 and self.ttl1:

# Start TTL sequence
at_mu(time_TO)
self.ttl0.execute(self)

# Translation DAC sequence
if self.translate:
    at_mu(time_translate_in)
    self.dac_translate_in.execute(self)

    at_mu(time_translate_out)
    self.dac_translate_out.execute(self)

# Start post-translation TTL sequence
at_mu(time_T1)
self.ttl1.execute(self)

# Kick out DAC sequence
at_mu(time_T2)
self.dac_kick.execute(self)

self.dac_kick is an "instantaneous" DAC set for all the channels that happens just once when called, but self.dac_translate* are DAC sets that propagate for some time to generate the sine waveform to translate our ions.

We tend to write our code (and pseudo code) in a way that is highly influenced by our predecessor experiment that uses LabVIEW and Matlab. If people have suggestions on how we should be using ARTIQ, we would gladly welcome them!