Skip to content

Commit

Permalink
Update time synchronization documentation in faqs and no-longer-orpha…
Browse files Browse the repository at this point in the history
…ned time_synchronization.rst.
  • Loading branch information
cboulay committed Jul 23, 2023
1 parent d1800fb commit 6c42274
Show file tree
Hide file tree
Showing 3 changed files with 25 additions and 21 deletions.
1 change: 1 addition & 0 deletions docs/index.rst
Expand Up @@ -13,6 +13,7 @@ LabStreamingLayer's Documentation
info/network-connectivity
info/lslapicfg
info/eeglab
info/time_synchronization
info/faqs

.. toctree::
Expand Down
32 changes: 11 additions & 21 deletions docs/info/faqs.rst
Expand Up @@ -9,7 +9,7 @@ How do I do that?

Because the result of :cpp:func:`~lsl::stream_inlet::pull_sample()` is the next
sample in the order provided by the sender, you first need to pull out all
samples that have been buffered up in the inlet. You can do this by calling
samples that have been buffered up in the inlet. You can do this by repeatedly calling
:cpp:func:`~lsl::stream_inlet::pull_sample()` with a timeout of ``0.0`` -- once
it returns zero, there are no more samples.

Expand All @@ -26,26 +26,8 @@ lsl_local_clock()
What clock does LSL use? /
How do I relate LSL's :cpp:func:`lsl_local_clock()` to my wall clock?

LSL's :cpp:func:`lsl_local_clock()` function measures the number of seconds
from a starting point,
e.g. since the local machine was started or since 1970-01-01 00:00.

Clock jumps, e.g. when suspending the system or resetting the clock (e.g. by a
network clock sychronization service like NTP) shouldn't occur.

The correct way to map its output to the time measured by your preferred system
clock is to first determine the constant offset between the two clocks, by
reading them out at the same time, and then to add that offset to the result
of :cpp:func:`lsl_local_clock()` whenever it is needed.

Also keep in mind that the time-stamps that are returned by
:cpp:func:`~lsl::stream_inlet::pull_sample()`
will generally be local to the sender's machine unless you set the
:cpp:enumerator:`proc_clocksync` or
:cpp:enumerator:`proc_ALL` postprocessing flags at inlet creation time.
Otherwise, you have to add the time offset returned by
:cpp:func:`lsl::stream_inlet::time_correction()`
to the timestamps to have them in your local domain.
LSL's :cpp:func:`lsl_local_clock()` function uses `std::chrono::steady_clock <https://en.cppreference.com/w/cpp/chrono/steady_clock>`_::now().time_since_epoch(). This returns the number of seconds from an arbitrary starting point. The starting point is platform-dependent -- it may be close to UNIX time, or the last reboot -- and LSL timestamps cannot be transformed naively to wall clock time without special effort.
For more information, see the :doc:`../info/time_synchronization` under the "Manual Synchronization" section.

Latency
-------
Expand Down Expand Up @@ -150,6 +132,10 @@ enough for your needs, e.g., demanding physics experiments), and you know
exactly what you are doing.
If you have any doubt on how you would use your own clock to synchronize
multiple pieces of hardware after you've recorded the data, don't use them.
Note that it will be impossible to synchronize any of the streams using this
custom clock with other streams using the LSL clock, and the default settings
on the XDF importers will be incorrect. At that point, one has to wonder if LSL
is the best choice for this scenario.

High sampling rates
-------------------
Expand Down Expand Up @@ -183,6 +169,10 @@ pre-allocated buffer.
Make sure that you use a recent version of liblsl (1.10 or later offers a
faster network protocol) at both the sender and the receiver.

If you are writing an application that needs to push a lot of data, please show
your interest by commenting on the long-lingering `pull request to speed up pushes
<https://github.com/sccn/liblsl/pull/170>`_ by reducing the number of data copies.

Chunk sizes
-----------

Expand Down
13 changes: 13 additions & 0 deletions docs/info/time_synchronization.rst
Expand Up @@ -63,6 +63,19 @@ Online Smoothing
----------------
To smooth the time stamps online, multiple algorithms can be used. The simplest one is double exponential smoothing, which is relatively easy to implement in an online data processing system, although it should be noted that during the first few minutes of operation (up to 5) the resulting time stamps will still have a considerable error (above 1 ms). The forgetting factors of this algorithm need to be set depending on the sampling rate of the data and the amplitude of the jitter in the raw stamps, and can require some tuning until the desired accuracy is reached. A better alternative is Recursive Least Squares (RLS), which has essentially optimal convergence behavior, although it will still take a minute or two of warmup until the jitter after smoothing reaches an acceptable level (<1ms) in realistic settings. To achieve this precision, the forget factor should be set such that a sample that is 30 seconds old will have an effective weighting of 1/2 (this depends on the sampling rate); one can also use 60 or as much as 120 seconds to further increase the precision -- however, too large values can fail to track sufficiently fast non-linear clock drift due to room or computer temperature changes (empirically, a half time of x seconds will be able to track clock rate fluctuations that change on the order of 10*x seconds or more slowly). This algorithm will also be built-in for optional use in a future version of liblsl. Other algorithms, such as QR-RLS and the Kalman filter can also be used and will perform similarly to RLS (note: some formulations of RLS have numerical difficulties, depending on the order of operations, and should not be used as they can 'blow up' after a few minutes of use).

Manual Synchronization With Non-LSL Clocks
==========================================

In general, it is not possible to synchronize LSL streams with non-LSL clocks (e.g., wall clock, UNIX time, device without an LSL integration) unless there is a separate solution for this.

The clock offset UDP stream described above used by LSL to synchronize streams from different computers is one such solution. This could be reproduced for non-LSL streams but it is perhaps not the most straightforward solution.

The most straightforward solution, which seems to be the most common, is to use hardware. For example, a common signal such as a digital pulse can be fed to an LSL-integrated device and to the non-LSL system, then an offline analysis of the two files can align the data sources via cross-correlation. However, a hardware solution is not always possible, especially in mobile experiments.

The next most straightforward solution is to create a map between LSL time and the _other_ clock, and this is probably most easily done in software by creating a simple LSL integration for that other clock. In this application, the _other_ clock and the LSL clock are read at approximately the same time (i.e., one line of code after the other) and the time of the other clock is sent as the data payload and the coincident LSL clock is sent as the time stamp in the call to `push_sample`. Subsequent offline analysis can load these clock-LSL pairs and create a mapping between the two. The most common mapping would be via a linear regression and that is probably sufficient in most cases (i.e., where the clock drift is consistent). Piecewise or moving-window mappings will be more accurate but also slower to calculate.

Note that the manual synchronization solutions described above only describe how to save data that will allow for offline synchronization during offline data analysis. This should satisfy most use cases. Online synchronization between the LSL clock and a non-LSL clock is still possible using the above software solution with a moving-window mapping (or exponential decay mapping) and setting the :cpp:enumerator:`proc_clocksync` or :cpp:enumerator:`proc_ALL` postprocessing flags at inlet creation time. Otherwise, you have to add the time offset returned by :cpp:func:`lsl::stream_inlet::time_correction()` to the timestamps to have them in your local domain.

Stream Header Synchronization Parameters
========================================
It is recommended that all LSL stream generators attach the following block to the header of each stream. The offset\_mean parameter is used to subtract known constant time lags from each stream. can\_drop\_samples is used to label a stream as having a steady frame rate, except for dropped samples. This is what you expect for video players and video recorders. The other parameters are for informational purposes or error estimation only.
Expand Down

0 comments on commit 6c42274

Please sign in to comment.