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_format
in pylsl, can be provided as a string or as a supportednumpy.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.