timestamp_estimator.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. #
  2. # Copyright (C) 2014-2016 UAVCAN Development Team <uavcan.org>
  3. #
  4. # This software is distributed under the terms of the MIT License.
  5. #
  6. # Author: Pavel Kirienko <pavel.kirienko@zubax.com>
  7. # Ben Dyer <ben_dyer@mac.com>
  8. #
  9. from __future__ import division, absolute_import, print_function, unicode_literals
  10. import decimal
  11. class SourceTimeResolver:
  12. """
  13. This class contains logic that recovers absolute value of a remote clock observable via small overflowing
  14. integer samples.
  15. For example, consider a remote system that reports timestamps as a 16-bit integer number of milliseconds that
  16. overflows every 60 seconds (this method is used in SLCAN for example). This class can recover the time difference
  17. in the remote clock domain between two arbitrary timestamps, even if the timestamp variable overflowed more than
  18. once between these events.
  19. """
  20. def __init__(self, source_clock_overflow_period=None):
  21. """
  22. Args:
  23. source_clock_overflow_period: Overflow period of the remote clock, in seconds.
  24. If not provided, the remote clock is considered to never
  25. overflow (i.e. absolute).
  26. """
  27. self.source_clock_overflow_period = \
  28. decimal.Decimal(source_clock_overflow_period) if source_clock_overflow_period else None
  29. if self.source_clock_overflow_period is not None and self.source_clock_overflow_period <= 0:
  30. raise ValueError('source_clock_overflow_period must be positive or None')
  31. # Internal states
  32. self._resolved_time = None
  33. self._prev_source_sample = None
  34. self._prev_target_sample = None
  35. def reset(self):
  36. """
  37. Resets the internal logic; resolved time will start over.
  38. """
  39. self._resolved_time = None
  40. self._prev_source_sample = None
  41. self._prev_target_sample = None
  42. def update(self, source_clock_sample, target_clock_sample):
  43. """
  44. Args:
  45. source_clock_sample: Sample of the source clock, in seconds
  46. target_clock_sample: Sample of the target clock, in seconds
  47. Returns: Resolved absolute source clock value
  48. """
  49. if self._resolved_time is None or self.source_clock_overflow_period is None:
  50. self._resolved_time = decimal.Decimal(source_clock_sample)
  51. self._prev_source_sample = source_clock_sample
  52. self._prev_target_sample = target_clock_sample
  53. else:
  54. # Time between updates in the target clock domain
  55. tgt_delta = target_clock_sample - self._prev_target_sample
  56. self._prev_target_sample = target_clock_sample
  57. assert tgt_delta >= 0
  58. # Time between updates in the source clock domain
  59. src_delta = source_clock_sample - self._prev_source_sample
  60. self._prev_source_sample = source_clock_sample
  61. # Using the target clock we can resolve the integer ambiguity (number of overflows)
  62. full_cycles = int(round((tgt_delta - src_delta) / float(self.source_clock_overflow_period), 0))
  63. # Updating the source clock now; in two steps, in order to avoid error accumulation in floats
  64. self._resolved_time += decimal.Decimal(full_cycles * self.source_clock_overflow_period)
  65. self._resolved_time += decimal.Decimal(src_delta)
  66. return self._resolved_time
  67. class TimestampEstimator:
  68. """
  69. Based on "A Passive Solution to the Sensor Synchronization Problem" [Edwin Olson 2010]
  70. https://april.eecs.umich.edu/pdfs/olson2010.pdf
  71. """
  72. DEFAULT_MAX_DRIFT_PPM = 200
  73. DEFAULT_MAX_PHASE_ERROR_TO_RESYNC = 1.
  74. def __init__(self,
  75. max_rate_error=None,
  76. source_clock_overflow_period=None,
  77. fixed_delay=None,
  78. max_phase_error_to_resync=None):
  79. """
  80. Args:
  81. max_rate_error: The max drift parameter must be not lower than maximum relative clock
  82. drift in PPM. If the max relative drift is guaranteed to be lower,
  83. reducing this value will improve estimation. The default covers vast
  84. majority of low-cost (and up) crystal oscillators.
  85. source_clock_overflow_period: How often the source clocks wraps over, in seconds.
  86. For example, for SLCAN this value is 60 seconds.
  87. If not provided, the source clock is considered to never wrap over.
  88. fixed_delay: This value will be unconditionally added to the delay estimations.
  89. Represented in seconds. Default is zero.
  90. For USB-interfaced sources it should be safe to use as much as 100 usec.
  91. max_phase_error_to_resync: When this value is exceeded, the estimator will start over.
  92. Defaults to a large value.
  93. """
  94. self.max_rate_error = float(max_rate_error or (self.DEFAULT_MAX_DRIFT_PPM / 1e6))
  95. self.fixed_delay = fixed_delay or 0
  96. self.max_phase_error_to_resync = max_phase_error_to_resync or self.DEFAULT_MAX_PHASE_ERROR_TO_RESYNC
  97. if self.max_rate_error < 0:
  98. raise ValueError('max_rate_error must be non-negative')
  99. if self.fixed_delay < 0:
  100. raise ValueError('fixed_delay must be non-negative')
  101. if self.max_phase_error_to_resync <= 0:
  102. raise ValueError('max_phase_error_to_resync must be positive')
  103. # This is used to recover absolute source time
  104. self._source_time_resolver = SourceTimeResolver(source_clock_overflow_period=source_clock_overflow_period)
  105. # Refer to the paper for explanations
  106. self._p = None
  107. self._q = None
  108. # Statistics
  109. self._estimated_delay = 0.0
  110. self._resync_count = 0
  111. def update(self, source_clock_sample, target_clock_sample):
  112. """
  113. Args:
  114. source_clock_sample: E.g. value received from the source system, in seconds
  115. target_clock_sample: E.g. target time sampled when the data arrived to the local system, in seconds
  116. Returns: Event timestamp converted to the target time domain.
  117. """
  118. pi = float(self._source_time_resolver.update(source_clock_sample, target_clock_sample))
  119. qi = target_clock_sample
  120. # Initialization
  121. if self._p is None:
  122. self._p = pi
  123. self._q = qi
  124. # Sync error - refer to the reference implementation of the algorithm
  125. self._estimated_delay = abs((pi - self._p) - (qi - self._q))
  126. # Resynchronization (discarding known state)
  127. if self._estimated_delay > self.max_phase_error_to_resync:
  128. self._source_time_resolver.reset()
  129. self._resync_count += 1
  130. self._p = pi = float(self._source_time_resolver.update(source_clock_sample, target_clock_sample))
  131. self._q = qi
  132. # Offset options
  133. assert pi >= self._p
  134. offset = self._p - self._q - self.max_rate_error * (pi - self._p) - self.fixed_delay
  135. new_offset = pi - qi - self.fixed_delay
  136. # Updating p/q if the new offset is lower by magnitude
  137. if new_offset >= offset:
  138. offset = new_offset
  139. self._p = pi
  140. self._q = qi
  141. ti = pi - offset
  142. return ti
  143. @property
  144. def estimated_delay(self):
  145. """Estimated delay, updated in the last call to update()"""
  146. return self._estimated_delay
  147. @property
  148. def resync_count(self):
  149. return self._resync_count
  150. if __name__ == '__main__':
  151. # noinspection PyPackageRequirements
  152. import matplotlib.pyplot as plt
  153. # noinspection PyPackageRequirements
  154. import numpy
  155. import time
  156. if 1:
  157. estimator = TimestampEstimator()
  158. print(estimator.update(.0, 1000.0))
  159. print(estimator.update(.1, 1000.1))
  160. print(estimator.update(.2, 1000.1)) # Repeat
  161. print(estimator.update(.3, 1000.1)) # Repeat
  162. print(estimator.update(.4, 1000.2))
  163. print(estimator.update(.5, 1000.3))
  164. if 1:
  165. # Conversion from Real to Monotonic
  166. estimator = TimestampEstimator(max_rate_error=1e-5,
  167. fixed_delay=1e-6,
  168. max_phase_error_to_resync=1e-2)
  169. print('Initial mono to real:', time.time() - time.monotonic())
  170. while True:
  171. mono = time.monotonic()
  172. real = time.time()
  173. est_real = estimator.update(mono, real)
  174. mono_to_real_offset = est_real - mono
  175. print(mono_to_real_offset)
  176. time.sleep(1)
  177. max_rate_error = None
  178. source_clock_range = 10
  179. delay_min = 0.0001
  180. delay_max = 0.02
  181. num_samples = 200
  182. x = range(num_samples)
  183. delays = numpy.random.uniform(delay_min, delay_max, size=num_samples)
  184. estimator = TimestampEstimator(max_rate_error=max_rate_error, fixed_delay=delay_min,
  185. source_clock_overflow_period=source_clock_range)
  186. source_clocks = []
  187. estimated_times = []
  188. offset_errors = []
  189. estimated_delays = []
  190. for i, delay in enumerate(delays):
  191. source_clock = i
  192. source_clocks.append(source_clock)
  193. target_clock = i + delay
  194. estimated_time = estimator.update(source_clock % source_clock_range, target_clock)
  195. estimated_times.append(estimated_time)
  196. offset_errors.append(estimated_time - source_clock)
  197. estimated_delays.append(estimator.estimated_delay)
  198. fig = plt.figure()
  199. ax1 = fig.add_subplot(211)
  200. ax1.plot(x, numpy.array(delays) * 1e3)
  201. ax1.plot(x, numpy.array(offset_errors) * 1e3)
  202. ax2 = fig.add_subplot(212)
  203. ax2.plot(x, (numpy.array(estimated_times) - numpy.array(source_clocks)) * 1e3)
  204. plt.show()