Differences with pylsl๐
Safer chunk pull default๐
Arguably the most important difference, pulling a chunk of numerical data with
pull_chunk() is faster than with its
pylsl counterpart, except if using the argument dest_obj.
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:
3.91 ยฑ 0.12 ms with
pylsl(default behavior: list comprehension)419.08 ยฑ 38.10 ns with
mne_lsl.lsl(numpy.frombuffer())
Increasing the number of channels to 650, simulating an higher sample count yields:
46.56 ยฑ 1.06 ms per loop with
pylsl(default behavior: list comprehension)424.29 ยฑ 36.41 ns with
mne_lsl.lsl(numpy.frombuffer())
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 above is skipped, yielding better performance than mne_lsl.lsl at the cost
of code complexity as the user is now responsible for the memory management.
Note
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 thechannel_formatin pylsl, can be provided as a string or as a supportednumpy.dtype, e.g.np.int8.processing_flagscan 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_streamswhich resolves all streams on the network.resolve_bypropwhich resolves all streams with a specific value for a given property.resolve_bypredwhich resolves all streams with a given predicate.
mne_lsl.lsl.resolve_streams() simplifies stream resolution with a unique function
with similar functionalities.