*** Wartungsfenster jeden ersten Mittwoch vormittag im Monat ***

Skip to content
Snippets Groups Projects
labscript_devices.py 15.3 KiB
Newer Older
#####################################################################
#                                                                   #
# ADwinProII/labscript_devices.py                                   #
#                                                                   #
# Copyright 2022, TU Vienna                                         #
#                                                                   #
# Implementation of the ADwin-Pro II for the labscript-suite,       #
# used in the Léonard lab for Experimental Quantum Information.     #
#                                                                   #
#####################################################################


from labscript import Device, Pseudoclock, PseudoclockDevice, IntermediateDevice, ClockLine, AnalogOut, DigitalOut, bitfield, config, LabscriptError
from labscript.functions import ramp, sine, sine_ramp, sine4_ramp, sine4_reverse_ramp, exp_ramp, exp_ramp_t
import numpy as np

default_cycle_time = 2500/1e9 # T12 Processor has 1GHz clock rate, TODO Cycle time in ADbasic

# Notes:
# The ADWin (T12) runs at 1 GHz. The cycle time should be specified in hardware programming in units of this clock speed.
# Subsequently, instruction timing must be specified in units of cycles.

# Voltages are specified with a 16 bit unsigned integer, mapping the range [-10,10) volts.
# There are 32 digital outputs on a card (DIO-32-TiCo)
# There are 8 analog outputs on a card (AOut-8/16)
# There are 8 analog inputs on a card (AIn-F-8/16)



class _ADwinProII(Pseudoclock):    
    def add_device(self, device):
        if isinstance(device, ClockLine):
            # only allow one child
            if self.child_devices:
                raise LabscriptError('The pseudoclock of the ADwin-Pro II %s only supports 1 clockline, which is automatically created. Please use the clockline located at %s.clockline'%(self.parent_device.name, self.parent_device.name))
            Pseudoclock.add_device(self, device)
        else:
            raise LabscriptError('You have connected %s to %s (the Pseudoclock of %s), but %s only supports children that are ClockLines. Please connect your device to %s.clockline instead.'%(device.name, self.name, self.parent_device.name, self.name, self.parent_device.name))



class ADwinAnalogOut(AnalogOut):
    def linear_ramp(self, t, duration, initial, final):
        #instruction = LinearRamp(t, duration, initial, final, self.parent_device.clock_limit)
        #self.add_instruction(t, instruction)
        #return instruction.duration
        pass
        
    def sin_ramp(self, t, duration, amplitude, offset, angular_period):
        #instruction = SinRamp(t, duration, amplitude, offset, angular_period, self.parent_device.clock_limit)
        #self.add_instruction(t, instruction)
        #return instruction.duration
        pass
        
    def cos_ramp(self, t, duration, amplitude, offset, angular_period):
        #instruction = CosRamp(t, duration, amplitude, offset, angular_period, self.parent_device.clock_limit)
        #self.add_instruction(t, instruction)
        #return instruction.duration
        pass
        
    def exp_ramp(self, t, duration, amplitude, offset, time_constant):
        #instruction = ExpRamp(t, duration, amplitude, offset, time_constant, self.parent_device.clock_limit)
        #self.add_instruction(t, instruction)
        #return instruction.duration
        pass
        
class ADwinDigitalOut(DigitalOut):
    pass
    
class ADwinCard(Pseudoclock):
    clock_type = 'fast clock'
    def __init__(self, name, parent_device, card_number):
        self.clock_limit = parent_device.clock_limit
        self.clock_resolution = parent_device.clock_resolution
        
        # Device must be accessed via the parent ADWin, so we must store
        # the parent's device_no as well as the card number:
        self.card_number = card_number
        self.BLACS_connection = parent_device.BLACS_connection, card_number
        # We won't call IntermediateDevice.__init__(), as we don't care
        # about the checks it does for clocking, we don't actually have
        # a clock:
        Device.__init__(self, name, parent_device, card_number)
        self.trigger_times = []
        self.wait_times = []
        self.initial_trigger_time = 0

    def trigger(self, t, *args):
        if t == 'initial':
            t = self.initial_trigger_time
            self.trigger_times.append(t)
        else:
            raise NotImplementedError("AdWins do not have waits implemented in labscript or the current firmware.")
        # Adwin cards are coordinated internally without the need for
        # triggering devices.  We split them up into pseudoclocks in
        # labscript because they are modular in nature, and it helps us
        # be compatible with AdWins that have different card setups. But
        # half of this pseudoclock stuff isn't relevant to this, so we
        # override some methods to do nothing.
        
#    def generate_code(self, hdf5_file):
#        # We don't actually need to expand out ramps and construct a pseudoclock or anything
#        # but we will anyway so that we have something to plot in runviewer
#        expanded_change_times
#        for output in self.get_all_outputs():
    
class ADwin_AO_Card(ADwinCard):
    description = 'ADWin analog output card'
    allowed_children = [AnalogOut]
    
    def generate_code(self, hdf5_file):
        Device.generate_code(self, hdf5_file)
        # This group must exist in order for BLACS to know that this
        # device is part of the experiment:
        group = hdf5_file.create_group('/devices/%s'%self.name)
        # OK, let's collect up all the analog instructions!
        self.formatted_instructions = []
        for output in self.get_all_outputs():
            for t, instruction in output.instructions.items():
                card_number = self.card_number
                channel_number = output.connection
                #if isinstance(instruction, RampInstruction):
                #    duration = instruction.duration
                #    if isinstance(instruction, LinearRamp):
                #        ramp_type = 0
                #    elif isinstance(instruction, SinRamp):
                #         ramp_type = 1
                #     elif isinstance(instruction, CosRamp):
                #         ramp_type = 2
                #     elif isinstance(instruction, ExpRamp):
                #         ramp_type = 3
                #     else:
                #         raise ValueError(instruction)
                #     A = instruction.A
                #     B = instruction.B
                #     C = instruction.C
                # else:
                #     # Let's construct a ramp out of the single value instruction:
                #     duration = self.clock_resolution
                #     ramp_type = 0
                #     A = instruction
                #     B = 0
                #     C = instruction
                formatted_instruction = {'t':t,
                                         'duration': duration,
                                         'card': card_number,
                                         'channel': channel_number,
                                         'ramp_type': ramp_type,
                                         'A': A, 'B': B, 'C': C}
                self.formatted_instructions.append(formatted_instruction)

                        
class ADwin_DO_Card(ADwinCard):
    description = 'ADWin digital output card'
    allowed_children = [DigitalOut]
    digital_dtype = np.uint32
    n_digitals = 32
    
    def generate_code(self, hdf5_file):
        Device.generate_code(self, hdf5_file)
        # This group must exist in order for BLACS to know that this
        # device is part of the experiment:
        group = hdf5_file.create_group('/devices/%s'%self.name)
        outputs = self.get_all_outputs() 
        change_times = self.collect_change_times(outputs)
        for output in outputs:
            output.make_timeseries(change_times)
        for time in change_times:
            outputarray = [0]*self.n_digitals
            for output in outputs:
                channel = output.connection
                # We have to subtract one from the channel number to get
                # the correct index, as ADWin is one-indexed, curse it.
                outputarray[channel - 1] = np.array(output.timeseries)
        bits = bitfield(outputarray, dtype=self.digital_dtype)
        self.formatted_instructions = []
        for t, value in zip(change_times, bits):
            formatted_instruction = {'t': t, 'card': self.card_number,'bitfield': value} 
            self.formatted_instructions.append(formatted_instruction)
            
class ADwinProII(PseudoclockDevice):
    description = 'ADWin-Pro II'
    clock_limit = 10e6
    clock_resolution = 25e-9
    trigger_delay = 350e-9
    wait_delay = 2.5e-6
    allowed_children = [_ADwinProII]
    max_instructions = 1e5
    #allowed_children = [ADwin_AO_Card, ADwin_DO_Card] # TODO where should this go?? To clockline class?
    def __init__(self, name="adwin", device_no=1, cycle_time = default_cycle_time, **kwargs):
        PseudoclockDevice.__init__(self, name, None, None, **kwargs)
        self.BLACS_connection = name + "_" + str(device_no)

        self._pseudoclock = _ADwinProII(
            name=f'{name}_pseudoclock',
            pseudoclock_device=self,
            connection='pseudoclock',
        )
        self._clock_line = ClockLine(
            name=f'{name}_clock_line',
            pseudoclock=self.pseudoclock,
            connection='internal',
        )

        # round cycle time to the nearest multiple of 3.3333ns TODO WHY??
        quantised_cycle_time = round(cycle_time/3.333333333333e-9)
        cycle_time = quantised_cycle_time*3.333333333333e-9
        self.clock_limit = 1./cycle_time
        self.clock_resolution = cycle_time

        self.trigger_times = []
        self.wait_times = []
        self.initial_trigger_time = 0

    @property
    def pseudoclock(self):
        return self._pseudoclock
    
    @property
    def clockline(self):
        return self._clock_line

    def add_device(self, device):
        if not self.child_devices and isinstance(device, Pseudoclock):
            PseudoclockDevice.add_device(self, device)            
        elif isinstance(device, Pseudoclock):
            raise LabscriptError('The %s automatically creates a Pseudoclock because it only supports one. '%(self.name) +
                                 'Instead of instantiating your own Pseudoclock object, please use the internal' +
                                 ' one stored in %s.pseudoclock'%self.name)
        else:
            raise LabscriptError('You have connected %s (class %s) to %s, but %s does not support children with that class.'%(device.name, device.__class__, self.name, self.name))

    def do_checks(self, outputs):
        if self.trigger_times != [0]:
            raise LabscriptError('ADWin does not support retriggering or waiting.')
        for output in outputs:
            output.do_checks(self.trigger_times)

    def collect_card_instructions(self, hdf5_file):
        group = hdf5_file.create_group('/devices/%s'%self.name)
        all_analog_instructions = []
        all_digital_instructions = []
        for device in self.child_devices:
            if isinstance(device, ADwin_AO_Card):
                all_analog_instructions.extend(device.formatted_instructions)
            elif isinstance(device, ADwin_DO_Card):
                all_digital_instructions.extend(device.formatted_instructions)
            else:
                raise AssertionError("Invalid child device, shouldn't be possible")
        # Make the analog output table:
        analog_dtypes = [('t',np.uint), ('duration',int), ('card',int), ('channel',int),
                         ('ramp_type',int), ('A',int), ('B',int), ('C',int)]
        # sort by time:
        all_analog_instructions.sort(key=lambda instruction: instruction['t'])
        analog_data = np.zeros(len(all_analog_instructions)+1, dtype=analog_dtypes)
        for i, instruction in enumerate(all_analog_instructions):
            analog_data[i]['t'] = round(instruction['t']/self.clock_resolution)
            analog_data[i]['duration'] = round(instruction['duration']/self.clock_resolution)
            analog_data[i]['card'] = instruction['card']
            analog_data[i]['channel'] = instruction['channel']
            analog_data[i]['ramp_type'] = instruction['ramp_type']
            if instruction['ramp_type'] in [0]:
                # If it's a linear ramp, map the voltages for parameter A from the range [-10,10] to a uint16:
                analog_data[i]['A'] = int((instruction['A']+10)/20.*(2**16-1))
            elif instruction['ramp_type'] in [1,2,3]:
                # For an exp,  sine or cos ramp, map A from [-10,10] to a signed int16:
                analog_data[i]['A'] = int(instruction['A']/10.*(2**15-1))
            else:
                raise RuntimeError('Sanity check failed: Invalid ramp type! Something has gone wrong.')
            analog_data[i]['B'] = round(instruction['B']/self.clock_resolution) # B has units of time
            analog_data[i]['C'] = int((instruction['C']+10)/20.*(2**16-1))
        # Add the 'end of data' instruction to the end:
        analog_data[-1]['t'] = 2**32-1
        # Save to the HDF5 file:
        group.create_dataset('ANALOG_OUTS', data=analog_data)
        
        # Make the digital output table:
        digital_dtypes = [('t',np.uint), ('card',int), ('bitfield',int)]
        # sort by time:
        all_digital_instructions.sort(key=lambda instruction: instruction['t'])
        digital_data = np.zeros(len(all_digital_instructions)+1, dtype=digital_dtypes)
        for i, instruction in enumerate(all_digital_instructions):
            digital_data[i]['t'] = round(instruction['t']/self.clock_resolution)
            digital_data[i]['card'] = instruction['card']
            digital_data[i]['bitfield'] = instruction['bitfield']
        # Add the 'end of data' instruction to the end:
        digital_data[-1]['t'] = 2**32-1
        # Save to the HDF5 file:
        group.create_dataset('DIGITAL_OUTS', data=digital_data)
        #group.attrs['stop_time'] = self.stop_time/self.clock_resolution
        group.attrs['cycle_time'] = self.clock_resolution
        
    def generate_code(self, hdf5_file):
        outputs = self.get_all_outputs()
        #outputs, outputs_by_clockline = self.get_outputs_by_clockline()
        # We call the following to do the error checking it includes,
        # but we're not actually interested in the set of change times.
        # Each card will handle its own timebase issues.
        #ignore = self.collect_change_times(outputs, outputs_by_clockline)
        #self.do_checks(outputs)
        # This causes the cards to have their generate_code() methods
        # called. They collect up the instructions of their outputs,
        # and then we will collate them together into one big instruction
        # table.
        #Device.generate_code(self, hdf5_file)
        #self.collect_card_instructions(hdf5_file)
        
        # We don't actually care about these other things that pseudoclock
        # classes normally do, but they still do some error checking
        # that we want:
        #change_times = self.collect_change_times(outputs, outputs_by_clockline)
        #for output in outputs:
        #    output.make_timeseries(change_times)
        #all_times, clock = self.expand_change_times(change_times, outputs)