# Lesson 3. Creating a Butterworth User Operator Script

This Python example creates a Butterworth user operator in the Infiniium / Python environment. Although not required, it is a good practice to give the XML file and script files the same or similar base filename, but using different filename extensions of course! As the number of your measurements grow, you'll appreciate being able to quickly associate the different XML and script files at a glance.

Color has been added to the following example lines of the XML and script code to make them easier to read.

## Procedure

1. Complete Lesson 1, if you have not already done so.
2. Display an eye diagram.
3. Copy the following XML listing into a text editor. Save the XML file as Butterworth.xml in Infiniium's user functions folder (\Documents\Keysight\Infiniium\User Functions). Later, when this XML file is imported into the User Operator Setup dialog box, it identifies the Butterworth.py script, populates the dialog box, and creates the user operator as shown in these figures. Notice that the XML file defines three user-defined controls that are shown in the dialog box. You can make selections from these controls while your user operator is running.

Butterworth.xml
Drag the mouse over this listing, enter Ctrl-C to copy, and paste it into your text editor.
<?xml version="1.0" encoding="utf-8"?> <Function xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Name>Butterworth</Name> <Abbreviation>Butter</Abbreviation> <Comments>Creates a Butterworth filter</Comments> <Script>Butterworth.py</Script> <FunctionType>1 Source</FunctionType> <Control Name = "FilterType"> <Enumeration> <Default>lowpass</Default> <Selection>lowpass</Selection> <Selection>highpass</Selection> </Enumeration> </Control> <Control Name = "FilterOrder"> <Integer> <Min>1</Min> <Max>4</Max> <Units>None</Units> <Default>2</Default> </Integer> </Control> <Control Name = "FilterCutoff"> <Double> <Min>10e6</Min> <Max>1000e9</Max> <Resolution>1000</Resolution> <Units>Hertz</Units> <Default>7.5e9</Default> </Double> </Control> <Control Name = "IsPeriodic"> <Integer> <Min>0</Min> <Max>1</Max> <Units>None</Units> <Default>0</Default> </Integer> </Control> </Function> |

4. Copy the following script into a text editor. Save the script as Butterworth.py in Infiniium's user operator folder (\Documents\Keysight\Infiniium\User Functions).

The algorithm function name must be written in title case. Naming the function Algorithm.m or ALGORITHM, for example, will break the script.

Butterworth.py
Drag the mouse over this listing, enter Ctrl-C to copy, and paste it into your editor.
    import math  # Import Python's standard math library.
    import numpy as np
    from scipy.fftpack import fft, ifft
    from scipy.signal import butter, freqz, lfilter

    # Argument "variables" is a dictionary from Infiniium that
    # maps from variable name to value.
    def algorithm(variables):

        # Get input waveform and info from variables input dictionary.
        SrcData = variables['SrcData'] if 'SrcData' in variables else np.empty(0, dtype=np.float32)
        XOrg = variables['XOrg'] if 'XOrg' in variables else 0.0
        XInc = variables['XInc'] if 'XInc' in variables else 0.0
        XUnits = variables['XUnits'] if 'XUnits' in variables else 'Same'
        YUnits = variables['YUnits'] if 'YUnits' in variables else 'Same'

        # Other variables customized.
        Gain = 1.0
        ErrorMsg = ''
        FiltData = np.empty(0, dtype=np.float32)

        # Get custom controls from variables input dictionary.
        N = variables['FilterOrder'] if 'FilterOrder' in variables else 2
        Fc = variables['FilterCutoff'] if 'FilterCutoff' in variables else 7.5e9
        FType = variables['FilterType'] if 'FilterType' in variables else 'lowpass'
        IsPeriodic = variables['IsPeriodic'] if 'IsPeriodic' in variables else 0

        # Calculate FilterDelay and FilterWidth.
        FilterDelay = N * 0.112 / Fc
        FilterWidth = N / Fc

        # Create filter.
        Wn = 2 * XInc * Fc   # calculate passband (-3 dB point)
        b, a = butter(N, Wn, FType)   # design Butterworth filter

        num_points = len(SrcData)
        if num_points > 0:
            if IsPeriodic:
                # Perform filtering circularly using FFTs
                fft_src_data = fft(SrcData)
                _unused, H = freqz(b, a, num_points, True)
                FiltData = ifft(fft_src_data * H).real

                # Rotate to remove FilterDelay.
                shift = int(round(FilterDelay / XInc))
                FiltData = np.roll(FiltData, -shift)
            else:   # not periodic
                start_index = int(math.ceil(FilterWidth / XInc))
                if start_index >= num_points:
                    ErrorMsg = 'Too few points. Pipeline eats up entire waveform.'
                else:
                    # Perform filtering
                    FiltData = lfilter(b, a, SrcData)

                    # Remove filter delay and pipeline
                    FiltData = FiltData[start_index:]
                    leading_pipeline = FilterWidth - FilterDelay
                    XOrg = XOrg + leading_pipeline

        # Return dictionary of results to Infiniium.
        output_variables = {
            "FiltData" : FiltData,   # Output waveform
            "XOrg" : XOrg,   # X-axis origin
            "XInc" : XInc,   # X-axis data spacing
            "Gain" : Gain,   # Gain
            "FilterWidth" : FilterWidth,   # Width of the filter
            "FilterDelay" : FilterDelay,   # Delay of the filter
            "XUnits" : XUnits,   # x-axis units
            "YUnits" : YUnits,   # y-axis units
            "ErrorMsg" : ErrorMsg   # Error message string
        }
        return output_variables

    # When testing from the command line.
    if __name__ == "__main__":
        from scipy.signal import square

        # Parameters
        num_points = 100000  # Number of points
        frequency = 2.97e9   # Frequency of the square wave (Hz)
        duration = 100e-9    # Duration of the signal (seconds)
        dtype = np.float32   # Desired data type

        # Time array
        t = np.linspace(0, duration, num_points, endpoint=False)

        # Generate square wave
        square_wave = square(2 * np.pi * frequency * t).astype(dtype)

        print(algorithm({'SrcData': square_wave, 'XOrg': -1e-07, 'XInc': 2e-12}))

5. Click the button on the lower-right corner of the screen.
6. At the top of the Functions dialog box, select the User tab and drag a single-input User operator icon into the operator construction area as shown in the following picture.
7. Click on the user operator to open the User Operator Setup dialog box.
8. In the dialog box, click Browse and select your XML configuration file, Butterworth.xml.
9. In the Functions dialog box, drag a Function Color to the User Operator as shown in the following picture.
10. Click Auto Scale and the input and output waveforms should be displayed.