<jupyter_start><jupyter_text>A (slightly less) Dummy BCI In this notebook, we learn about some of the other features of the `generic_BCI` class by building another dummy brain-computer interface.We will show the use of a calibrator and a transformer.<jupyter_code># importing the BCI module from neurol
from neurol import BCI<jupyter_output><empty_output><jupyter_text>Calibrators are given to a `generic_BCI` instance on initialization. They are run with its `calibrate` function. After running, the calibrator returns a `calibration_info` object which stored as an attribute of the `generic_BCI` instance (initialilze to None if no calibrator). This `calibration_info` is then passed on to the `transformer` and `classifier` as the second argument in order to adjust their behaviour.  Still-pretty-dummy BCI First, let's create a dummy calibrator to show how this works. The `calibrator` will ask for user input and return it. The `transformer` will ignore the data stream and just return `calibration_info` appending the string "_tfrm". The `classifier` again will just return its input appending the string "_clf". The `action` will be to simply print the classification.<jupyter_code>def dummy_clb(inlet):
    '''dummy calibrator'''
    print("What's up?")
    return input()

def dummy_tfrm(buffer, clb_info):
    '''dummy transformer'''
    return clb_info + '_tfrm'

def dummy_clf(clf_input, clb_info):
    '''dummy classifier'''
    return clf_input + '_clf' # note that dummy_clf happens to not use the calibration info<jupyter_output><empty_output><jupyter_text>Now, we define the BCI using the above functions<jupyter_code>clb_demo_bci = BCI.generic_BCI(dummy_clf, dummy_tfrm, action=print, calibrator=dummy_clb)<jupyter_output><empty_output><jupyter_text>Before we calibrate and run our BCI, we need to get our stream of data. This looks slightly different for different devices, but looks something like the following:<jupyter_code>from neurol.connect_device import get_lsl_EEG_inlets
from neurol import streams

inlet = get_lsl_EEG_inlets()[0] # gets first inlet, assuming only one EEG streaming device is connected

# we ask the stream object to manage a buffer of 1024 samples from the inlet
stream = streams.lsl_stream(inlet, buffer_length=1024)<jupyter_output><empty_output><jupyter_text>Now that we have a stream of data, we can calibrate our BCI. Obviously this particular BCI isn't using the stream of data (though it is still managing it as if it will)<jupyter_code>clb_demo_bci.calibrate(inlet)<jupyter_output>What's up?<jupyter_text>That's it. Now let's check its `calibration_info` attribute to see if it worked.<jupyter_code>clb_demo_bci.calibration_info<jupyter_output><empty_output><jupyter_text>Alright there we go! If you happen to know the `calibration_info` you want to give your BCI before hand without needing to run a calibrator, then you can set the property directly without needing to initialize your BCI with a calibrator.Now let's run the BCI and see it using the calibration<jupyter_code>try:
    clb_demo_bci.run(stream)
except KeyboardInterrupt:
    stream.close()
    
    print('\n')
    print('QUIT BCI')<jupyter_output>notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_clf
notmuch_tfrm_c[...]<jupyter_text>So, it does what we expect. But this isn't very useful behavior. Let's show a slightly less contrived usecase.  Not-as-dummy BCI We will be creating a BCI which classifies based on frequency band power. To do this, we'll be making use of the BCI_tools module<jupyter_code>from neurol import BCI_tools<jupyter_output><empty_output><jupyter_text>We went to make our classification based on the power of, say, alpha waves. If it's above a certain threshold, we take it to be a positive classification and perhaps treat it as 'relaxation'. This threhsold classification is, as you might expect, performed by the `classifier`. The `tranformer` computes the power in the alpha frequency band. Finally, the `classifer`'s threshold is computed by the calibrator as a certain percentile of the band power distribution First, we define the calibrator. Conveniently, `BCI_tools` has a `band_power_calibrator` function we can use. These utility functions in `BCI_tools` tend to have several parameters for you to specify exactly what you want them to do. The idea is that you can use lambda expressions to define your BCI's functionality.<jupyter_code>help(BCI_tools.band_power_calibrator)<jupyter_output>Help on function band_power_calibrator in module neurol.BCI_tools:

band_power_calibrator(stream, channels, device, bands, percentile=50, recording_length=10, epoch_len=1, inter_window_interval=0.2)
    Calibrator for `generic_BCI.BCI` which computes a given `percentile` for
    the power of each frequency band across epochs channel-wise. Useful for
    calibrating a concentration-based BCI.
    
    Arguments:
        stream(neurol.streams object): neurol stream for brain data.
        channels: array of strings with the names of channels to use.
        device(str): device name for use by `classification_tools`
        bands: the frequency bands to get power features for.
            'all': all of ['theta', 'alpha_low', 'alpha_high', 'beta', 'gamma']
            otherwise an array of strings of the desired bands.
        percentile: the percentile of power distribution across epochs to
            return for each band.
        recording_length(float): length of recording to use for c[...]<jupyter_text>We see the parameters that `band_power_calibrator` accepts. In the end, the `calibrator` we pass into our `generic_BCI` instance should accept an `inlet` as its only parameter, so we should specify everything else. After looking over the parameters, we might choose to do the following.<jupyter_code>my_clb = lambda stream : BCI_tools.band_power_calibrator(stream, ['AF7', 'AF8'], 'muse', bands=['alpha_low', 'alpha_high'], 
                                        percentile=65, recording_length=10, epoch_len=1, inter_window_interval=0.25)
# we defined a calibrator which returns the 65th percentile of alpha wave 
#power over the 'AF7' and 'AF8' channels of a muse headset after recording for 10 seconds 
# and using epochs of 1 second seperated by 0.25 seconds.<jupyter_output><empty_output><jupyter_text>Next we define the transformer. Again, `BCI_tools` has a `band_power_transformer` utility function we can use.<jupyter_code>help(BCI_tools.band_power_transformer)<jupyter_output>Help on function band_power_transformer in module neurol.BCI_tools:

band_power_transformer(buffer, clb_info, channels, device, bands, epoch_len=1)
    Transformer for `generic_BCI.BCI` which chooses channels, epochs, and
    gets power features on some choice of bands.
    
    Arguments:
        buffer: most recent stream data. shape: [n_samples, n_channels]
        clb_info: not used. included for compatibility with generic_BCI.BCI
        channels: list of strings of the channels to use.
        device:(str): device name for use by `classification_tools`.
        bands: the frequency bands to get power features for.
            'all': all of ['theta', 'alpha_low', 'alpha_high', 'beta', 'gamma']
            otherwise a list of strings of the desired bands.
        epoch_len(float): the duration of data to classify on in seconds.
    
    Returns:
        transformed_signal: array of shape [n_bands, n_channels] of the
        channel-wise power of each band over the epoch.<jupyter_text>Alright then. After looking at the parameters, we might choose to define the following transformer:<jupyter_code>my_tfrm = lambda buffer, clb_info: BCI_tools.band_power_transformer(buffer, clb_info, ['AF7', 'AF8'], 'muse',
                                                    bands=['alpha_low', 'alpha_high'], epoch_len=1)
# define a transformer that corresponds to the choices we made with the calibrator<jupyter_output><empty_output><jupyter_text>Finally, we need to define the classifier. Since we're looking for a threshold classifier, we can make use of the `classification_tools` module under the `models` package. It has a `threshold_clf` function that would be useful here.<jupyter_code>from neurol.models import classification_tools
help(classification_tools.threshold_clf)<jupyter_output>Help on function threshold_clf in module neurol.models.classification_tools:

threshold_clf(features, threshold, clf_consolidator='any')
    Classifies given features based on a given threshold.
    
    Arguments:
        features: an array of numerical features to classify
        threshold: threshold for classification. A single number, or an
          array corresponding to `features` for element-wise comparison.
        clf_consolidator: method of consolidating element-wise comparisons
          with threshold into a single classification.
            'any': positive class if any features passes the threshold
            'all': positive class only if all features pass threshold
            'sum': a count of the number of features which pass the threshold
            function: a custom function which takes in an array of booleans
              and returns a consolidated classification
    
    Returns:
        classification for the given features. Return type `clf_consolidator`.<jupyter_text>Okay, seems simple enough. The 'features' would be the output of the transformer, and the 'threshold' was computed by the calibrator.<jupyter_code># Again, we define a classifier that matches the choices we made
# we use a function definition instead of a lambda expression since we want to do slightly more with 
def my_clf(clf_input, clb_info):
    
    # use threshold_clf to get a binary classification
    binary_label = classification_tools.threshold_clf(clf_input, clb_info, clf_consolidator='all')
    
    # decode the binary_label into something more inteligible for printing
    label = classification_tools.decode_prediction(binary_label, {True: 'Relaxed', False: 'Concentrated'})
    
    return label<jupyter_output><empty_output><jupyter_text>Alright! Now we can define our BCI:<jupyter_code>my_bci = BCI.generic_BCI(my_clf, transformer=my_tfrm, action=print, calibrator=my_clb)<jupyter_output><empty_output><jupyter_text>Let's start by running the calibrator and checking that it worked. We'll be using the same inlet we used before.<jupyter_code>inlet = get_lsl_EEG_inlets()[0] # gets first inlet, assuming only one EEG streaming device is connected

# we ask the stream object to manage a buffer of 1024 samples from the inlet
stream = streams.lsl_stream(inlet, buffer_length=1024) 
my_bci.calibrate(stream)
my_bci.calibration_info<jupyter_output><empty_output><jupyter_text>Alright, cool! The calibrator computed the given percentile of the alpha band power for each of the 'AF7' and 'AF8' channels and stored it in `calibration_info`. Let's now run the BCI!<jupyter_code>try:
    my_bci.run(stream)
except KeyboardInterrupt:
    stream.close()
    
    print('\n')
    print('QUIT BCI')<jupyter_output>Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Relaxed
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Relaxed
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Relaxed
Relaxed
Relaxed
Relaxed
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Relaxed
Relaxed
Relaxed
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Concentrated
Conce[...]
{'fraction_non_alphanumeric': 0.05748549323017408, 'fraction_numerical': 0.003945841392649903, 'mean_word_length': 5.190613026819923, 'pattern_counts': {'":': 0, '<': 63, '<?xml version=': 0, '>': 63, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 3, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '17', 'dir': 'jupyter-structured-clean-dedup', 'id': '309032', 'license': 'MIT', 'n_tokens_mistral': 4161, 'n_tokens_neox': 3977, 'n_words': 1379, 'path': 'examples/(slightly less) dummy_BCI.ipynb', 'repo_name': 'Awni00/neurol', 'size': '12925'}