Differences with pylsl๐Ÿ”—

Faster chunk pull๐Ÿ”—

Arguably the most important difference, pulling a chunk of numerical data with pull_chunk() is much faster than with its pylsl counterpart. By default, pylsl loads the retrieved samples one by one in a list of list, here.

# return results (note: could offer a more efficient format in the
# future, e.g., a numpy array)
num_samples = num_elements / num_channels
if dest_obj is None:
    samples = [
        [data_buff[s * num_channels + c] for c in range(num_channels)]
        for s in range(int(num_samples))
    ]
    if self.channel_format == cf_string:
        samples = [[v.decode("utf-8") for v in s] for s in samples]
        free_char_p_array_memory(data_buff, max_values)

The creation of the variable samples is expensive and is performed in linear time O(n), scaling with the number of values. Instead, numpy can be used to pull the entire buffer at once with numpy.frombuffer().

samples = np.frombuffer(
    data_buffer, dtype=self._dtype)[:n_samples_data].reshape(-1, self._n_channels
)

Now, samples is created in constant time O(1). The performance gain varies depending on the number of values pulled, for instance retrieving 1024 samples with 65 channels in double precision (float64) takes:

  • 4.33 ms ยฑ 37.5 ยตs with pylsl (default behavior)

  • 268 ns ยฑ 0.357 ns with mne_lsl.lsl

Note that pylsl pulling function support a dest_obj argument described as:

A Python object that supports the buffer interface.
If this is provided then the dest_obj will be updated in place and the samples list
returned by this method will be empty. It is up to the caller to trim the buffer to
the appropriate number of samples. A numpy buffer must be order='C'.

If a ndarray is used as dest_obj, the memory re-allocation step described abvove is skipped, yielding similar performance to mne_lsl.lsl. For the same 1024 samples with 65 channels in double precision (float64), the pull operation takes:

  • 471 ns ยฑ 1.7 ns with pylsl (with dest_obj argument as ndarray)

Note that this performance improvement is absent for string based streams. Follow #225 for more information.

Convenience methods๐Ÿ”—

A StreamInfo has several convenience methods to retrieve and set channel attributes: names, types, units.

Those methods eliminate the need to interact with the XMLElement underlying tree, present in the mne_lsl.lsl.StreamInfo.desc property. The description can even be set or retrieved directly from a Info object with set_channel_info() and get_channel_info().

Improve arguments๐Ÿ”—

The arguments of a StreamInfo, StreamInlet, StreamOutlet support a wider variety of types. For instance:

  • dtype, which correspond to the channel_format in pylsl, can be provided as a string or as a supported numpy.dtype, e.g. np.int8.

  • processing_flags can be provided as strings instead of the underlying integer mapping.

Overall, the arguments are checked in mne_lsl.lsl. Any type or value mistake will raise an helpful error message.

Unique resolve function๐Ÿ”—

pylsl has several stream resolving functions:

  • resolve_streams which resolves all streams on the network.

  • resolve_byprop which resolves all streams with a specific value for a given property.

  • resolve_bypred which resolves all streams with a given predicate.

mne_lsl.lsl.resolve_streams() simplifies stream resolution with a unique function with similar functionalities.