Data taking program: make it non-atomic

17 Oct 2021

Update in 2024-07

After reading qcodes examples, I found it would be more clean if the data fetching process from multiple instruments is realized parallelly using threads.

Motivation

Unlike InstrDAQ, data taking and output channel setting in qtlab and many other similar programs are usually atomic, i.e., operations are done by a single function, such as instrument_X.get_reading_Y() or instrument_X.set_output_Y(). On the other hand, non-atomic means that an operation is split into more than one steps, e.g., instrument_X.send_query_message() followed by instrument_X.get_response().

There are two reasons to make these operations non-atomic. First, an instrument may take tens of milliseconds to prepare the data after it receives a query message. It is more efficient to send query messages to all instruments, wait once, read all responses, than query-wait-read one instrument by one instrument. It also decreases the time misalignment of measurements from different instruments. Second, even if the hardware delay in the first case is zero, we still want to make the output setting non-atomic so that we can set all output channels, wait only once, for stability or avoiding gate voltages to change too fast, instead of set- instrument1-wait, set- instrument2-wait, set- instrument3-wait (wait multiple times)…

Method

The script can be found in https://github.com/cover-me/repository/tree/master/qt/qtlab%20scan%20scripts. It is named as Qscan.210729.draft10.py but may be renamed in the future. This is a script for qtlab (demo). With Qscan we can do any linear 1D, 2D or 3D scans with code scan(D1,D2,D3), where Di (i=1,2,3) has the form ([channel_i1, channel_i2, channel_i3, ...], [start_i1, start_i2, start_i3, ...], [stop_i1, stop_i2, stop_i3, ...], num_of_points_i, stabilization_time_after_set_i), e.g., a 2d scan: scan(['I1(e-2uA)','I2(e-2uA)'],['dac1','dac2'],[-100,-100],[100,100],100,['Bx(T)'],['magnetX'],[0],[1],100). It can also communicate with qtplot and MS Word to show realtime plot and log measurement automatically.

Non-atomic reading

self._query_list is the list of channels to read. A group of data is generated after going through self._query_list. To run non-atomic reading, the get function of a reading channel must have the flag argument, which may not always be in the case. See Sec. Notes.

def take_data_nonatomic(self):
    # flag 0 (default): write command and read respond, 1: write only, 2: read only
    val = []
    for instr, para_name, label in self._query_list:
        if instr is not None:
            instr.get(para_name, flag=1)

    for instr, para_name, label in self._query_list:
        if instr is not None:
            ans = instr.get(para_name, flag=2)
            if type(ans) == list:
                val += ans
            else:
                val.append(ans)
    val += self.get_prcss(val)#add processed data
    return val      

Non-atomic setting

Non-atomic setting is a little more complicated. There are two kinds of output channels. One is like the magnet, we set, it ramps gradually. The other one is a DAC or a voltage source, we set, it changes immediately. For the second type of outputs, qtlab has the feature that they can be assigned with maxstep and stepdelay. If the difference between the target value (SV) and the current value (PV) is larger than the maxstep, qtlab will sweep the channel step by step, each step by the change of maxstep and wait for stepdelay except the last step which may be smaller than the maxstep and no waiting. The code bellow only takes care of the second type of output channels (see Sec. Notes).

def do_set_nonatomic(self, setpoint_list):
    '''
    Set a list of channels
    input:
        setpoint_list: a list of setpoints. A setpoint is like [instr_name, para_name, sv]
    '''
    step = 0
    delay = 0
    pv_list = []
    delta_list = []

    # calculate step, delay, pv_list, delta_list, sign_list
    for i in setpoint_list:
        instr_name, para_name, sv = i
        instr = qt.instruments.get(instr_name)
        para = instr.get_parameters()[para_name]
        if 'maxstep' in para:# channels like DAC
            pv = para['value']
            if pv is None:
                pv = instr.get(para_name)
            d = pv - sv
            pv_list.append(pv)
            delta_list.append(d)
            if step==0 or step>para['maxstep']:
                step = para['maxstep']
            if delay<para['stepdelay']:
                delay = para['stepdelay']
        else:# channels like magnet
            pv_list.append(sv)
            delta_list.append(0)
            instr.set(para_name, sv)

    sign_list = [int(i<0)*2-1 for i in delta_list]

    # ramp channels
    while 1:
        for i in range(len(setpoint_list)):
            if delta_list[i] != 0:
                instr_name, para_name, sv = setpoint_list[i]
                instr = qt.instruments.get(instr_name)
                if abs(delta_list[i])> step:
                    pv_list[i] += sign_list[i] * step
                    delta_list[i] += sign_list[i] * step
                else:
                    pv_list[i] = sv
                    delta_list[i] = 0
                instr.set(para_name, pv_list[i])
        if all(d==0 for d in delta_list):# True if delta_list is empty
            break
        else:
            qt.msleep(delay/1000.)

Notes

Qscan.210729.draft10 still uses the atomic reading and setting strategy by default. To make it non-atomic, one should add

g = get_set()
g.take_data = g.take_data_nonatomic
g.do_set = g.do_set_nonatomic
e = easy_scan()

to the scan script.