Skip to main content
Version: current

Python

In SAFE you can integrate your own Python functions directly into SAFE. This can be used both in measurement steps, if you for example need to communicate with your own products API, and in processing if you want to use your own analysis.

Python 3.6 or above must be installed. The numpy package must also be installed.

Using a Python function in a measurement step

To use a Python function in a measurement step you must drag out the measurement track Python Script onto a step. The track shows you a console which shows you every print statement from your function when the step is started.

If more than one Python script is in a step they will run sequentially with a priority based on where the Python Script is placed in the step, but they will run simultaneously to any other tracks in the step.

Your Python function must have only three input variables and must return the same three variables.

When used in a measurement step, data will be sent to your function as a dictonary like the example below.

user_data = {'success': True}         # script success variable (True, False)

You can add key/value pairs to this dict and it will be passed between all the functions you run during a sequence.

Use the 'success' key to tell SAFE if the script ran successfully or not. Returning False will NOT stop the sequence, but it will be clear in the log if the script ran successfully or not.

Your Python function should be defined as the example function foo below. The file in which your function is, must have a if __name__ == __main__: clause with the content shown in the example below.

It is advised not to make additional function calls or change the code in the if __name__ == '__main__': clause. Doing so can cause unexpected behavior.

def foo(data, variables, metadata):
    """
    Example function to be used in a SAFE measurement sequence.
    
    :param data: A dictionary to be used throughout a measurement sequence.
    :param variables: A dictionary of all the variables your have defined in the current project
    :param metadata: A dictionary af all the metadata you have defined in the current project
    :return: data, variables, metadata: The data dictionary, the variables dictionary and the metadata dictionary
    """
    print('Hello world!')
    return data, variables, metadata


if __name__ == '__main__':
    import sys
    import json
    import numpy as np
    import time

    def serializeDict(d):
        return {k.replace('\'', '"'): v.tolist() if isinstance(v, np.ndarray) else serializeDict(v) if isinstance(v, dict) else v for k, v in d.items()}

    def deserializeDict(d):
        return {k.replace('\'', '"'): np.array(v) if isinstance(v, list) else deserializeDict(v) if isinstance(v, dict) else v for k, v in d.items()}

    info, variables, metadata = {}, {}, {}
    for i, arg in enumerate(sys.argv):
        if i == 2:
            variables = deserializeDict(json.loads(arg))  # decode data to numpy arrays
        elif i == 3:
            metadata = deserializeDict(json.loads(arg))  # decode data to numpy arrays
    data = deserializeDict(json.loads(sys.stdin.read()))  # read and decode data to numpy arrays
    # If you need to setup something before running the function do it here
    returnList = list(globals()[sys.argv[1]](data, variables, metadata))  # function call
    for i, ret in enumerate(returnList[:3]):
        if not isinstance(ret, dict):
            raise ValueError('Function must return two dictionaries.')
        if i == 0:
            print(f"***DATA***")
        elif i == 1:
            print(f"***VARIABLES***")
        elif i == 2:
            print(f"***METADATA***")
        print(json.dumps(serializeDict(ret)))  # encode and print output
    # NO PRINTS AFTER THIS LINE #

Using a Python function in processing

To use a Python function in processing you must drag out the processing block Python Script from the toolbox Scripts. Here you can choose filepath and function name in the attribute dock. When the block is connected to other blocks data will be sent to your function as a dictonary.

Below is an example of how the dictionary looks when one single-column dataset is routed into a script block with one input. The '0' key relates to the input the data is coming from. If more inputs are use they will be referred to respectively. If a dataset has more columns it is represented in all the arrays.

import numpy as np

data = {'0': {'msg': '',
              'fs': np.array([[48000]]),
              'xlabel': np.array([['Time']]),
              'ylabel': np.array([['Amplitude']]),
              'xunit': np.array([['s']]),
              'yunit': np.array([['SPL']]),
              'X': np.array([[1],
                             [2],
                             [3]]),
              'Y': np.array([[5],
                             [8],
                             [9]])
              }
        }

Your Python function should be defined as the example function bar below. If you have defined variables and/or metadata in your project they are also available to monitor or edit in scripts. You cannot add new variables or metadata in a script.

The file in which your function is, must have a if __name__ == __main__: clause with the content shown in the example below.

It is advised not to make additional function calls or change the code in the if __name__ == '__main__': clause. Doing so can cause unexpected behavior.

def bar(data, variables, metadata):
    """
    Example function to be used in a SAFE measurement sequence.
    
    :param data: A dictionary to be used throughout a measurement sequence.
    :param variables: A dictionary of all the variables your have defined in the current project
    :param metadata: A dictionary af all the metadata you have defined in the current project
    :return: data, variables, metadata: The data dictionary, the variables dictionary and the metadata dictionary
    """
    data['0']['Y'] = data['0']['Y'] * 2
    variables['my_var'] = 4
    return data, variables, metadata

if __name__ == '__main__':
    import sys
    import json
    import numpy as np
    import time

    def serializeDict(d):
        return {k.replace('\'', '"'): v.tolist() if isinstance(v, np.ndarray) else serializeDict(v) if isinstance(v, dict) else v for k, v in d.items()}

    def deserializeDict(d):
        return {k.replace('\'', '"'): np.array(v) if isinstance(v, list) else deserializeDict(v) if isinstance(v, dict) else v for k, v in d.items()}

    info, variables, metadata = {}, {}, {}
    for i, arg in enumerate(sys.argv):
        if i == 2:
            variables = deserializeDict(json.loads(arg))  # decode data to numpy arrays
        elif i == 3:
            metadata = deserializeDict(json.loads(arg))  # decode data to numpy arrays
    data = deserializeDict(json.loads(sys.stdin.read()))  # read and decode data to numpy arrays
    # If you need to setup something before running the function do it here
    returnList = list(globals()[sys.argv[1]](data, variables, metadata))  # function call
    for i, ret in enumerate(returnList[:3]):
        if not isinstance(ret, dict):
            raise ValueError('Function must return two dictionaries.')
        if i == 0:
            print(f"***DATA***")
        elif i == 1:
            print(f"***VARIABLES***")
        elif i == 2:
            print(f"***METADATA***")
        print(json.dumps(serializeDict(ret)))  # encode and print output
    # NO PRINTS AFTER THIS LINE #

Takeaway

If you had not noticed the if __name__ == '__main__': clause is the same for both examples. With this you only need to create one script to handle all your measurement sequence and processing needs.