<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 SPI Rack lock-in setup
Example notebook to create a lock-in setup using the SPI Rack modules
Step1:  Open the SPI rack connection and unlock the controller. This is necessary after bootup of the controller module. If not unlocked, no communication with the modules can take place. The virtual COM port baud rate is irrelevant as it doesn't change the actual speed. Timeout can be changed, but 1 second is a good value.
Step2:  Read back the version of the microcontroller software. This should return 1.6 or higher to be able to use the D5b properly. Als read the temperature and the battery voltages through the C1b, this way we verify that the connection with the SPI Rack is working.
Step3:  Create a new D5b module object at the correct module address using the SPI object. By default the module resets the output voltages to 0 Volt. Before it does this, it will read back the current value. If this value is non-zero it will slowly ramp it to zero. If reset_voltages = False then the output will not be changed.
To see that the we have a connection, we read back the firmware version.
Step4:  Now we create two B2b module objects at the correct module address using the SPI object. If we set calibrate=True, the module will run a calibration routine at initialisation. This takes about 4 seconds per module, the python code will stall operation during this process.
To see that the we have a connection, we read back the firmware versions.
Step5:  System Analysis
To characterize the system we will use the D5b_module to create a step function and use the B2b modules to measure the response. We do this by setting the D5b_module to a very long toggling time and a toggle amount of only 2. The B2b modules we set to trigger on the D5b with a large amount of samples and a high sample rate.
Step6:  The holdoff time of the source module we set as short as possible
Step7:  We'll use DAC output 0 and 1 (output 1 and 2 on the module front) to generate the step. For this we set them both to toggle mode and to 2 Volt bipolar. It will keep the output voltage outside of toggling at 0V (set_DAC_voltage) and we set it to toggle between -0.02V and 0.02V.
Step8:  The ADC modules will listen on the backplane for the triggers of the D5b module, and have a set holoff time of zero seconds. As the D5b module generates two triggers, we will tell the B2b modules to expect the same amount.
Step9:  We will set the ADC modules to the fastest sampling rate possible (filter rate zero), with filter type sinc3 and 2000 samples per trigger. For a list of all the filter settings, see the filter table on the website.
Step10:  Now we use the D5b software trigger to start the measurement. We can use the is_running() function from one of the B2b modules to see if the measurement is finished. Once its finished, we can get the data. In this case we will only use the channel 1 data.
Step11:  Now we plot the step response. As the DAC toggles twice (from 0 -> 2V -> -2V) we get two step responses. The timing between these two steps is not directly obvious from the data and might lead to a wrong interpretation. Therefore we'll only look at the first step response.
We can get the sample time/data rate from the B2b module, given the filter type and filter rate.
Step22:  Lock-in measurement
First we create a simple lock_in class which contains the source module (the previously defined D5b object) and (multiple) measurement modules (the B2b modules). This class gives us a simple interface to use these modules as a lock-in.
Step23:  We now create the lock_in object using the previously created D5b and B2b modules.
Step24:  For the lock-in source we use DAC output 0 and 1 (1 and 2 on the frontpanel), at a frequency of 125 Hz and we'll measure for 10 periods.
Step25:  On the measurement side we'll set a measurement holdoff of 2 ms with filter setting 8. This gives a 200 &mu;s settling time with a resolution of 19.3 bits. At 125 Hz and a holdoff time of 2 ms, we have 2ms left for measurements. With filter setting 8 this should give us 10 samples per toggle.
Step26:  Amplitude sweep
The current measurement units we use are the M1e and the M1f, we have both of them set to an output gain of 10M V/A with a post gain of 10. We will sweep the voltage from -0.05V to 0.05V for different resistances and plot the IV-curves below.
Step27:  We can now calculate the resistance from the IV data.
Step28:  Derivative measurement
Here we set the amplitude fixed and shift the offset voltage over a defined range
<ASSISTANT_TASK:>
Python Code:
from spirack import SPI_rack, D5b_module, B2b_module
import logging
from time import sleep, time
from tqdm import tqdm_notebook
import numpy as np
from scipy import signal
from plotly.offline import init_notebook_mode, iplot, plot
import plotly.graph_objs as go
init_notebook_mode(connected=True)
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
Explanation: SPI Rack lock-in setup
Example notebook to create a lock-in setup using the SPI Rack modules: D5b and B2b/D4b. One D5b (source module) is required, and any number of measurement modules (B2b/D4b) can be used. Here we will use two B2b modules interfacing to IVVI rack measurement modules.
First we will use these units to get the step response of the system. This allows us to characterize the system and to determine the highest possible lock-in frequency that we can use.
For this test we have the following measurement setup:
<br><br>
<img src="Images/B2b_example_lockin.png" alt="Measurement Setup" title="Measurement Setup" width="450" />
Initialisation
To use the SPI Rack as a lock-in, we need to import the SPI_rack, D5b_module and the B2b_module\ D4b_module from the spirack library. All the communication with the SPI Rack runs through the SPI_rack object which communicates through a virtual COM port. This COM port can only be open on one instance on the PC. Make sure you close the connection here before you can use it somewhere else.
We also import the logging library to be able to display the logging messages; numpy for data manipulation; scipy for the FFT analysis and plotly for visualistation.
End of explanation
COM_port = 'COM4' # COM port of the SPI rack
COM_speed = 1e6   # Baud rate, not of much importance
timeout = 1       # Timeout value in seconds
spi_rack = SPI_rack(COM_port, COM_speed, timeout)
spi_rack.unlock() # Unlock the controller to be able to send data to the rack
Explanation: Open the SPI rack connection and unlock the controller. This is necessary after bootup of the controller module. If not unlocked, no communication with the modules can take place. The virtual COM port baud rate is irrelevant as it doesn't change the actual speed. Timeout can be changed, but 1 second is a good value.
End of explanation
print('Version: ' + spi_rack.get_firmware_version())
print('Temperature: {:.2f} C'.format(spi_rack.get_temperature()))
battery_v = spi_rack.get_battery()
print('Battery: {:.3f}V, {:.3f}V'.format(battery_v[0], battery_v[1]))
Explanation: Read back the version of the microcontroller software. This should return 1.6 or higher to be able to use the D5b properly. Als read the temperature and the battery voltages through the C1b, this way we verify that the connection with the SPI Rack is working.
End of explanation
D5b = D5b_module(spi_rack, module=2, reset_voltages=False)
print("Firmware version: {}".format(D5b.get_firmware_version()))
Explanation: Create a new D5b module object at the correct module address using the SPI object. By default the module resets the output voltages to 0 Volt. Before it does this, it will read back the current value. If this value is non-zero it will slowly ramp it to zero. If reset_voltages = False then the output will not be changed.
To see that the we have a connection, we read back the firmware version.
End of explanation
B2b_1 = B2b_module(spi_rack, module=3, calibrate=False)
print("Firmware version B2b_1: {}".format(B2b_1.get_firmware_version()))
B2b_2 = B2b_module(spi_rack, module=4, calibrate=False)
print("Firmware version B2b_2: {}".format(B2b_2.get_firmware_version()))
Explanation: Now we create two B2b module objects at the correct module address using the SPI object. If we set calibrate=True, the module will run a calibration routine at initialisation. This takes about 4 seconds per module, the python code will stall operation during this process.
To see that the we have a connection, we read back the firmware versions.
End of explanation
D5b.set_toggle_time(0.2)
toggle_value = D5b.get_toggle_time()
print('Toggle time: {} s'.format(toggle_value))
D5b.set_toggle_amount(2)
print('Toggle amount: {}'.format(D5b.get_toggle_amount()))
Explanation: System Analysis
To characterize the system we will use the D5b_module to create a step function and use the B2b modules to measure the response. We do this by setting the D5b_module to a very long toggling time and a toggle amount of only 2. The B2b modules we set to trigger on the D5b with a large amount of samples and a high sample rate.
End of explanation
D5b.set_trigger_holdoff_time(30e-6)
print('Holdoff time: {} s'.format(D5b.get_trigger_holdoff_time()))
Explanation: The holdoff time of the source module we set as short as possible: 30 &mu;s.
End of explanation
DACs = [0, 1, 2]
for DAC in DACs:
    D5b.set_DAC_span(DAC, '2V_bi')
    D5b.set_DAC_mode(DAC, 'toggle')
    D5b.set_DAC_voltage(DAC, 0)
    D5b.set_DAC_neg_toggle_voltage(DAC, -0.02)
    D5b.set_DAC_pos_toggle_voltage(DAC, 0.02)
Explanation: We'll use DAC output 0 and 1 (output 1 and 2 on the module front) to generate the step. For this we set them both to toggle mode and to 2 Volt bipolar. It will keep the output voltage outside of toggling at 0V (set_DAC_voltage) and we set it to toggle between -0.02V and 0.02V.
End of explanation
meas_modules = [B2b_1, B2b_2]
for B2b in meas_modules:
    B2b.set_trigger_input('D5b')
    B2b.set_trigger_holdoff_time(0)
    B2b.set_trigger_amount(2)
Explanation: The ADC modules will listen on the backplane for the triggers of the D5b module, and have a set holoff time of zero seconds. As the D5b module generates two triggers, we will tell the B2b modules to expect the same amount.
End of explanation
filter_type = 'sinc3'
filter_rate = 0
no_samples = 1000
for B2b in meas_modules:
    B2b.set_ADC_enable(0, True)
    B2b.set_sample_amount(0, no_samples)
    B2b.set_filter_type(0, filter_type)
    B2b.set_filter_rate(0, filter_rate)
Explanation: We will set the ADC modules to the fastest sampling rate possible (filter rate zero), with filter type sinc3 and 2000 samples per trigger. For a list of all the filter settings, see the filter table on the website.
End of explanation
D5b.software_trigger()
while D5b.is_running():
    sleep(0.1)
print('Done!')
B2b_1_Ch1, _ = B2b_1.get_data()
B2b_2_Ch1, _ = B2b_2.get_data()
Explanation: Now we use the D5b software trigger to start the measurement. We can use the is_running() function from one of the B2b modules to see if the measurement is finished. Once its finished, we can get the data. In this case we will only use the channel 1 data.
End of explanation
timestep = B2b_1.sample_time[filter_type][filter_rate]
fs = 1/timestep
timearray = np.arange(0, no_samples*timestep, timestep)
M1e_gain = 10e6 * 10
M1f_gain = 10e6 * 10
pldata_1 = go.Scattergl(x=timearray, y=B2b_1_Ch1[:no_samples]/M1e_gain, mode='lines+markers', name='B2b_1')
pldata_2 = go.Scattergl(x=timearray, y=B2b_2_Ch1[:no_samples]/M1f_gain, mode='lines+markers', name='B2b_2')
plot_data = [pldata_1, pldata_2]
layout = go.Layout(
    xaxis = dict(title='Time (s)'),
    yaxis = dict(title='Current (A)'),
)
fig = go.Figure(data=plot_data, layout=layout)
iplot(fig)
Explanation: Now we plot the step response. As the DAC toggles twice (from 0 -> 2V -> -2V) we get two step responses. The timing between these two steps is not directly obvious from the data and might lead to a wrong interpretation. Therefore we'll only look at the first step response.
We can get the sample time/data rate from the B2b module, given the filter type and filter rate.
End of explanation
class lock_in(object):
    def __init__(self, source_module, measure_modules, reset_source_output=True):
        
        Inits the modules so they can be used as a lock-in. Sets all the measurement
        modules to be triggered by the source module.
        
        Args:
            source_module: D5b object that acts as source module
            measure_modules: list of B2b/D4b objects that act as measurement modules
            measure_names: a list (of lists) with module and channel names. Should have entry for each module
        
        self.source_module = source_module
        self.measure_modules = measure_modules
        
        self.toggle_time = None
        self.toggle_amount = None
        self.meas_holdoff_time = None
        
        self.sample_amount = None
        
        for module in self.measure_modules:
            module.set_trigger_input('D5b')
            module.set_ADC_enable(0, True)
            module.set_ADC_enable(1, True)
            
        if reset_source_output:
            for DAC in range(8):
                self.source_module.set_DAC_mode(DAC, 'DC')
                self.source_module.set_DAC_span(DAC, '4V_bi', update=False)
                self.source_module.set_DAC_voltage(DAC, 0.0, update=True)
    
    def enable_output(self, DAC, enable):
        Set DAC output to lock-in mode
        
        Multiple DACs can be set to lock-in mode. They can all have different
        output amplitudes and voltages, but they will all run at the same frequency
        and the same number of periods.
        
        Args:
            DAC (int:0-7): DAC output to enable/disable for lock-in mode
            enable (bool): enable or disable lock-in mode
        
        if enable:
            self.source_module.set_DAC_mode(DAC, 'toggle')
        else:
            self.source_module.set_DAC_mode(DAC, 'DC')
    
    def set_frequency(self, frequency, no_periods):
        Sets the measurement frequency and number of periods
        
        Args:
            frequency: measurement frequency
            no_periods (int): number of periods to measure
        
        toggle_time = 1/(2*frequency)        
        
        self.source_module.set_toggle_time(toggle_time)
        self.source_module.set_toggle_amount(int(no_periods*2))
        
        for module in self.measure_modules:
            module.set_trigger_amount(int(no_periods*2))
        
        self.toggle_time = toggle_time
        self.toggle_amount = no_periods*2
    
    def set_output(self, DAC, offset, amplitude):
         Sets the DAC output voltages
        
        Args:
            DAC (int:0-7): DAC output to change 
            offset (float): offset voltage in Volts
            amplitude (float): peak to peak amplitude in Volt
        
        self.source_module.set_DAC_voltage(DAC, offset)
        self.source_module.set_DAC_neg_toggle_voltage(DAC, offset - (amplitude/2))
        self.source_module.set_DAC_pos_toggle_voltage(DAC, offset + (amplitude/2))
    
    def set_output_range(self, DAC, output_range, update=True):
        Set the software span of the selected DAC
        Changes the span of the selected DAC. If update is True the span gets updated
        immediately. If False, it will update with the next span or value setting.
        Args:
            DAC (int: 0-7): DAC inside the module of which to set the span
            span (string): the span to be set (4V_uni, 8V_uni, 4V_bi, 8V_bi, 2V_bi)
            update (bool): if True updates the span immediately, if False updates
                           with the next span/value update
        
        self.source_module.set_DAC_span(DAC, output_range, update)
        
    
    def set_trigger_holdoff(self, holdoff_time):
         Sets the DAC trigger holdoff time
        
        Sets the time the system waits after the trigger for outputting the toggling
        DACs. The mimimum time is 30 us, and the resolution is 100ns.
        
        Args:
            holdoff_time (float): holdoff time in seconds (min 30 us)
        
        self.source_module.set_trigger_holdoff_time(holdoff_time)
    
    def set_measure_holdoff(self, holdoff_time):
         Sets the ADC trigger holdoff time
        
        Sets the time the system waits after a D5b trigger before measuring. Resolution
        of 100 ns
        
        Args:
            holdoff_time (float): holdoff time in seconds
        
        for module in self.measure_modules:
            module.set_trigger_holdoff_time(holdoff_time)
            
        self.holdoff_time = holdoff_time
    
    def set_sample_amount(self, amount):
         Sets the ADC sample amount
        
        Sets the amount of samples that the ADC channel takes per trigger.
        
        Args:
            amount (int): sample amount per trigger
        
        for module in self.measure_modules:
            for i in range(2):
                module.set_sample_amount(i, amount)
            
        self.sample_amount = amount
    
    def set_filter(self, filter_type, filter_rate): 
         Sets the ADC filters
        
        The filter rate together with the filter type determines the cutoff frequency, 
        sample rate, the resolution and the 50 Hz rejection. See the filter table to 
        determine which setting to use.
        
        Args:
            filter_type (string): either sinc3 or sinc5
            filter_rate (int:0-20): filter setting
        
        for module in self.measure_modules:
            for i in range(2):
                module.set_filter_type(i, filter_type)
                module.set_filter_rate(i, filter_rate)
    
    def software_trigger(self):
         Triggers the source (D5b) module
        
        This allows the user to trigger the S5b via software, not using the trigger lines
        on the backplane of the SPI rack.
        
        self.source_module.software_trigger()
        
    def amplitude_sweep(self, DAC_list, offset, voltage_list):
        meas_res = np.zeros([len(voltage_list), len(self.measure_modules)*2])
        
        for i, voltage in enumerate(voltage_list):
            for DAC in DAC_list:
                self.set_output(DAC=DAC, offset=offset, amplitude=voltage)
            self.software_trigger()
            while self.source_module.is_running():
                sleep(0.01)
            meas_res[i] = self.get_measurement_result()            
        
        return meas_res            
    
    def offset_sweep(self, DAC_list, offset_list, amplitude):
        meas_res = np.zeros([len(offset_list), len(self.measure_modules)*2])
        
        for i, voltage in enumerate(offset_list):
            for DAC in DAC_list:
                self.set_output(DAC=DAC, offset=voltage, amplitude=amplitude)
            self.software_trigger()
            while self.source_module.is_running():
                sleep(0.01)
            meas_res[i] = self.get_measurement_result()
        return meas_res
    
    def get_measurement_result(self):
        result = []
        for module in self.measure_modules:
            while module.is_running():
                sleep(0.01)
                
            ADC0, ADC1 = module.get_data()
            
            ADC0 = ADC0.reshape(self.toggle_amount, -1)
            avg_values = np.sum(ADC0, axis=1)/self.sample_amount
            result.append(np.sum(avg_values[0::2] - avg_values[1::2])/(self.toggle_amount/2))
            
            ADC1 = ADC1.reshape(self.toggle_amount, -1)
            avg_values = np.sum(ADC1, axis=1)/self.sample_amount
            result.append(np.sum(avg_values[0::2] - avg_values[1::2])/(self.toggle_amount/2))
        return np.array(result)
Explanation: Lock-in measurement
First we create a simple lock_in class which contains the source module (the previously defined D5b object) and (multiple) measurement modules (the B2b modules). This class gives us a simple interface to use these modules as a lock-in.
End of explanation
li_dev = lock_in(D5b, [B2b_1, B2b_2], reset_source_output=False)
Explanation: We now create the lock_in object using the previously created D5b and B2b modules.
End of explanation
for DAC in range(2):
    li_dev.enable_output(DAC, enable=True)
    li_dev.set_output_range(DAC, '2V_bi')
li_dev.set_trigger_holdoff(30e-6)
li_dev.set_frequency(frequency=125, no_periods=10)
Explanation: For the lock-in source we use DAC output 0 and 1 (1 and 2 on the frontpanel), at a frequency of 125 Hz and we'll measure for 10 periods.
End of explanation
li_dev.set_measure_holdoff(0)
li_dev.set_sample_amount(10)
li_dev.set_filter('sinc5', 8)
Explanation: On the measurement side we'll set a measurement holdoff of 2 ms with filter setting 8. This gives a 200 &mu;s settling time with a resolution of 19.3 bits. At 125 Hz and a holdoff time of 2 ms, we have 2ms left for measurements. With filter setting 8 this should give us 10 samples per toggle.
End of explanation
v_sweep = np.linspace(-0.05, 0.05, num=30, endpoint=True)
DAC_list = [0, 1, 2]
meas_results = li_dev.amplitude_sweep(DAC_list, 0, v_sweep)
plot_data = []
M1e_gain = -10*10e6
M1f_gain = 10*10e6
M1e_meas = meas_results[:,0]/M1e_gain
M1f_meas = meas_results[:,2]/M1f_gain
plot_data.append(go.Scattergl(x=v_sweep, y=M1e_meas, mode='lines+markers', name='M1e'))
plot_data.append(go.Scattergl(x=v_sweep, y=M1f_meas, mode='lines+markers', name='M1f'))
layout = go.Layout(
    xaxis = dict(title='Amplitude Voltage (V)'),
    yaxis = dict(title='Current (A)'),
)
fig = go.Figure(data=plot_data, layout=layout)
iplot(fig)
Explanation: Amplitude sweep
The current measurement units we use are the M1e and the M1f, we have both of them set to an output gain of 10M V/A with a post gain of 10. We will sweep the voltage from -0.05V to 0.05V for different resistances and plot the IV-curves below.
End of explanation
plot_data = []
plot_data.append(go.Scattergl(x=v_sweep, y=v_sweep/M1e_meas, mode='lines+markers', name='M1e'))
plot_data.append(go.Scattergl(x=v_sweep, y=v_sweep/M1f_meas, mode='lines+markers', name='M1f'))
layout = go.Layout(
    xaxis = dict(title='Amplitude Voltage (V)'),
    yaxis = dict(title='Resistance (Ohm)', type='log'),
)
fig = go.Figure(data=plot_data, layout=layout)
iplot(fig)
Explanation: We can now calculate the resistance from the IV data.
End of explanation
offset_sweep = np.linspace(-0.05, 0.05, num=50, endpoint=True)
DAC_list = [0, 1, 2]
toggle_amplitude = 0.005
meas_results = li_dev.offset_sweep(DAC_list, offset_sweep, toggle_amplitude)
plot_data = []
M1e_gain = -10*10e6
M1f_gain = 10*10e6
M1e_meas = meas_results[:,0]/M1e_gain
M1f_meas = meas_results[:,2]/M1f_gain
plot_data.append(go.Scattergl(x=offset_sweep, y=M1e_meas, mode='lines+markers', name='M1e'))
plot_data.append(go.Scattergl(x=offset_sweep, y=M1f_meas, mode='lines+markers', name='M1f'))
layout = go.Layout(
    xaxis = dict(title='Offset Voltage (V)'),
    yaxis = dict(title='Current (A)'),
)
fig = go.Figure(data=plot_data, layout=layout)
iplot(fig)
plot_data = []
plot_data.append(go.Scattergl(x=offset_sweep, y=toggle_amplitude/M1e_meas, mode='lines+markers', name='M1e'))
plot_data.append(go.Scattergl(x=offset_sweep, y=toggle_amplitude/M1f_meas, mode='lines+markers', name='M1f'))
layout = go.Layout(
    xaxis = dict(title='Offset Voltage (V)'),
    yaxis = dict(title='Resistance (Ohm)', type='log'),
)
fig = go.Figure(data=plot_data, layout=layout)
iplot(fig)
Explanation: Derivative measurement
Here we set the amplitude fixed and shift the offset voltage over a defined range
End of explanation
<END_TASK>