.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "generated/tutorials/00_introduction.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_generated_tutorials_00_introduction.py: Introduction to real-time LSL streams ===================================== .. include:: ./../../links.inc .. _SSP projectors: https://mne.tools/dev/documentation/implementation.html#signal-space-projection-ssp LSL is an open-source networked middleware ecosystem to stream, receive, synchronize, and record neural, physiological, and behavioral data streams acquired from diverse sensor hardware. It reduces complexity and barriers to entry for researchers, sensor manufacturers, and users through a simple, interoperable, standardized API to connect data consumers to data producers while abstracting obstacles such as platform differences, stream discovery, synchronization and fault-tolerance. Source: `LabStreamingLayer website `_. In real-time applications, a server emits a data stream, and one or more clients connect to the server to receive this data. In LSL terminology, the server is referred to as a :class:`~mne_lsl.lsl.StreamOutlet`, while the client is referred to as a :class:`~mne_lsl.lsl.StreamInlet`. The power of LSL resides in its ability to facilitate interoperability and synchronization among streams. Clients have the capability to connect to multiple servers, which may be running on the same or different computers (and therefore different platforms/operating systems), and synchronize the streams originating from these various servers. MNE-LSL enhances the LSL API by offering a high-level interface akin to the `MNE-Python `_ API. While this tutorial concentrates on the high-level API, detailed coverage of the low-level LSL API is provided in :ref:`this separate tutorial `. Concepts -------- In essence, a real-time LSL stream can be envisioned as a perpetual recording, akin to a :class:`mne.io.Raw` instance, characterized by an indeterminate length and providing access solely to current and preceding samples. In memory, it can be depicted as a ring buffer, also known as a circular buffer, a data structure employing a single, unchanging buffer size, seemingly interconnected end-to-end. .. image:: ../../_static/tutorials/circular-buffer-light.png :align: center :class: only-light .. image:: ../../_static/tutorials/circular-buffer-dark.png :align: center :class: only-dark Within a ring buffer, there are two pivotal pointers: * The "head" pointer, also referred to as "start" or "read," indicates the subsequent data block available for reading. * The "tail" pointer, known as "end" or "write," designates the forthcoming data block to be replaced with fresh data. In a ring buffer configuration, when the "tail" pointer aligns with the "head" pointer, data is overwritten before it can be accessed. Conversely, the "head" pointer cannot surpass the "tail" pointer; it will always lag at least one sample behind. In all cases, it falls upon the user to routinely inspect and fetch samples from the ring buffer, thereby advancing the "head" pointer. Within MNE-LSL, the :class:`~mne_lsl.stream.StreamLSL` object manages a ring buffer internally, which is continuously refreshed with new samples. Notably, the two pointers are concealed, with the head pointer being automatically adjusted to the latest received sample. Given the preference for accessing the most recent information in neural, physiological, and behavioral real-time applications, this operational approach streamlines interaction with LSL streams and mitigates the risk of users accessing outdated data. Mocking an LSL stream --------------------- To build real-time applications or showcase their functionalities, such as in this tutorial, it's essential to generate simulated LSL streams. This involves creating a :class:~mne_lsl.lsl.StreamOutlet and regularly sending data through it. Within MNE-LSL, the :class:`~mne_lsl.player.PlayerLSL` generates a simulated LSL stream utilizing data from a :class:`mne.io.Raw` file or object. This stream inherits its description and channel specifications from the associated :class:`~mne.Info`. This information encompasses channel properties, channel locations, filters, digitization, and `SSP projectors`_. The :class:~mne_lsl.player.PlayerLSL subsequently publishes data at regular intervals and seamlessly loops back to the starting point once the end of the file is reached. .. GENERATED FROM PYTHON SOURCE LINES 84-97 .. code-block:: Python import time import uuid from matplotlib import pyplot as plt from mne import set_log_level from mne_lsl.datasets import sample from mne_lsl.player import PlayerLSL as Player from mne_lsl.stream import StreamLSL as Stream set_log_level("WARNING") .. GENERATED FROM PYTHON SOURCE LINES 98-103 .. note:: The argument ``source_id`` can be omitted most of the time. But for reliability in our documentation build on CIs, we assign a random unique identifier to each mock stream created. .. GENERATED FROM PYTHON SOURCE LINES 103-109 .. code-block:: Python source_id = uuid.uuid4().hex fname = sample.data_path() / "sample-ant-raw.fif" player = Player(fname, chunk_size=200, source_id=source_id).start() player.info .. raw:: html
General
MNE object type Info
Measurement date Unknown
Participant Unknown
Experimenter mne_anonymize
Acquisition
Sampling frequency 1024.00 Hz
Channels
EEG
EOG
ECG
Stimulus
Galvanic skin response
Head & sensor digitization Not available
Filters
Highpass 0.00 Hz
Lowpass 512.00 Hz


.. GENERATED FROM PYTHON SOURCE LINES 110-121 Once the :meth:`~mne_lsl.player.PlayerLSL.start` is called, data is published at regular intervals. The interval duration depends on the sampling rate and on the number of samples pushed at once, defined by the ``chunk_size`` argument of the :class:`~mne_lsl.player.PlayerLSL` object. .. note:: The default setting for chunk_size is ``10``. In real-time applications, there may be advantages to employing smaller chunk sizes for data publication, but to build the documentation website or to run test successfully on CIs, a larger chunk size is used to reduce the number of push operations and improve stability. .. GENERATED FROM PYTHON SOURCE LINES 121-127 .. code-block:: Python sfreq = player.info["sfreq"] chunk_size = player.chunk_size interval = chunk_size / sfreq # in seconds print(f"Interval between 2 push operations: {interval} seconds.") .. rst-class:: sphx-glr-script-out .. code-block:: none Interval between 2 push operations: 0.1953125 seconds. .. GENERATED FROM PYTHON SOURCE LINES 128-151 A :class:`~mne_lsl.player.PlayerLSL` can also stream annotations attached to the :class:`mne.io.Raw` object. Annotations are streamed on a second irregularly sampled :class:`~mne_lsl.lsl.StreamOutlet`. See :ref:`this separate tutorial ` for additional information. Subscribing to an LSL stream ---------------------------- With the mock LSL stream operational in the background, we can proceed to subscribe to this stream and access both its description and the data stored within its buffer. The :class:`~mne_lsl.stream.StreamLSL` object operates both the underlying :class:`~mne_lsl.lsl.StreamInlet` and the ring buffer, which size must be explicitly set upon creation. .. note:: A :class:`~mne_lsl.stream.StreamLSL` can connect to a single LSL stream. Thus, if multiple LSL stream are present on the network, it's crucial to uniquely identify a specific LSL stream using the ``name``, ``stype``, and ``source_id`` arguments of the :class:`~mne_lsl.stream.StreamLSL` object. The stream description is automatically parsed into an :class:`mne.Info` upon connection with the method :meth:`mne_lsl.stream.StreamLSL.connect`. .. GENERATED FROM PYTHON SOURCE LINES 151-155 .. code-block:: Python stream = Stream(bufsize=2, source_id=source_id).connect() stream.info .. raw:: html
General
MNE object type Info
Measurement date Unknown
Participant Unknown
Experimenter Unknown
Acquisition
Sampling frequency 1024.00 Hz
Channels
EEG
EOG
ECG
Stimulus
Galvanic skin response
Head & sensor digitization Not available
Filters
Highpass 0.00 Hz
Lowpass 512.00 Hz


.. GENERATED FROM PYTHON SOURCE LINES 156-160 Interaction with a :class:`~mne_lsl.stream.StreamLSL` is similar to the interaction with a :class:`mne.io.Raw`. In this example, the stream is mocked from a 64 channels EEG recording with an ANT Neuro amplifier. It includes 63 EEG, 2 EOG, 1 ECG, 1 EDA, 1 STIM channel, and uses CPz as reference. .. GENERATED FROM PYTHON SOURCE LINES 160-164 .. code-block:: Python ch_types = stream.get_channel_types(unique=True) print(f"Channel types included: {', '.join(ch_types)}") .. rst-class:: sphx-glr-script-out .. code-block:: none Channel types included: eeg, eog, gsr, ecg, stim .. GENERATED FROM PYTHON SOURCE LINES 165-178 Operations such as channel selection, re-referencing, and filtering are performed directly on the ring buffer. For instance, we can select the EEG channels, add the missing reference channel and re-reference using a common average referencing scheme which will reduce the ring buffer to 64 channels. .. note:: By design, once a re-referencing operation is performed or if at least one filter is applied, it is not possible anymore to select a subset of channels with the methods :meth:`~mne_lsl.stream.StreamLSL.pick` or :meth:`~mne_lsl.stream.StreamLSL.drop_channels`. Note that the re-referencing is not reversible while filters can be removed with the method :meth:`~mne_lsl.stream.StreamLSL.del_filter`. .. GENERATED FROM PYTHON SOURCE LINES 178-185 .. code-block:: Python stream.pick("eeg") # channel selection assert "CPz" not in stream.ch_names # reference absent from the data stream stream.add_reference_channels("CPz") stream.set_eeg_reference("average") stream.info .. raw:: html
General
MNE object type Info
Measurement date Unknown
Participant Unknown
Experimenter Unknown
Acquisition
Sampling frequency 1024.00 Hz
Channels
EEG
Head & sensor digitization Not available
Filters
Highpass 0.00 Hz
Lowpass 512.00 Hz


.. GENERATED FROM PYTHON SOURCE LINES 186-197 .. note:: As for MNE-Python, methods can be chained, e.g. .. code-block:: python stream.pick("eeg").add_reference_channels("CPz") The ring buffer is accessed with the method :meth:`~mne_lsl.stream.StreamLSL.get_data` which returns both the samples and their associated timestamps. In LSL terminology, a sample is an array of shape (n_channels,). .. GENERATED FROM PYTHON SOURCE LINES 197-213 .. code-block:: Python picks = ("Fz", "Cz", "Oz") # channel selection f, ax = plt.subplots(3, 1, sharex=True, constrained_layout=True) for _ in range(3): # acquire 3 separate window # figure how many new samples are available, in seconds winsize = stream.n_new_samples / stream.info["sfreq"] # retrieve and plot data data, ts = stream.get_data(winsize, picks=picks) for k, data_channel in enumerate(data): ax[k].plot(ts, data_channel) time.sleep(0.5) for k, ch in enumerate(picks): ax[k].set_title(f"EEG {ch}") ax[-1].set_xlabel("Timestamp (LSL time)") plt.show() .. image-sg:: /generated/tutorials/images/sphx_glr_00_introduction_001.png :alt: EEG Fz, EEG Cz, EEG Oz :srcset: /generated/tutorials/images/sphx_glr_00_introduction_001.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 214-232 .. warning:: Note that the first of the 3 chunks plotted is longer. This is because execution of the channel selection and re-referencing operations took a finite amount of time to complete while in the background, the :class:`~mne_lsl.stream.StreamLSL` was still acquiring new samples. Note also that :py:attr:`~mne_lsl.stream.StreamLSL.n_new_samples` is reset to 0 after each call to :meth:`~mne_lsl.stream.StreamLSL.get_data`, but it is not reset if the "tail" pointer overtakes the "head" pointer, in other words, it is not reset if the number of new samples since the last :meth:`~mne_lsl.stream.StreamLSL.get_data` call exceeds the buffer size. Free resources -------------- When you are done with a :class:`~mne_lsl.player.PlayerLSL` or :class:`~mne_lsl.stream.StreamLSL`, don't forget to free the resources they both use to continuously mock an LSL stream or receive new data from an LSL stream. .. GENERATED FROM PYTHON SOURCE LINES 232-235 .. code-block:: Python stream.disconnect() .. rst-class:: sphx-glr-script-out .. code-block:: none .. GENERATED FROM PYTHON SOURCE LINES 236-238 .. code-block:: Python player.stop() .. rst-class:: sphx-glr-script-out .. code-block:: none .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 5.536 seconds) **Estimated memory usage:** 297 MB .. _sphx_glr_download_generated_tutorials_00_introduction.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: 00_introduction.ipynb <00_introduction.ipynb>` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: 00_introduction.py <00_introduction.py>` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: 00_introduction.zip <00_introduction.zip>` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_