PageRenderTime 24ms CodeModel.GetById 35ms RepoModel.GetById 1ms app.codeStats 0ms

/gillcup/clocks.py

https://github.com/encukou/gillcup
Python | 346 lines | 340 code | 0 blank | 6 comment | 0 complexity | 167d4bb3ee48e0a15204e0e744d0be5c MD5 | raw file
  1. """Asyncio-based discrete-time simulation infrastructure
  2. A clock keeps track of *time*. But, what is time?
  3. If you are familiar with the :mod:`asyncio` library,
  4. you might know the :func:`asyncio.sleep` coroutine.
  5. It "sleeps" roughly for a given number of seconds,
  6. usually based on the computer's system time.
  7. Since the Python process does not control this time, ``sleep()`` may sleep
  8. longer than requested if the event loop is busy.
  9. Also, the system time always changes:
  10. so it is not possible to do two actions at exactly the same time.
  11. For animations and simulations, such time is unusable.
  12. Thus, time in Gillcup is a very different beast.
  13. Gillcup time is a quantity that *increases in discrete intervals*.
  14. On other words, it can never go backwards,
  15. and it does not change while animation/simulation code is executing.
  16. You can schedule an function for any time in the future.
  17. When the clock advances to that time, the clock is frozen at the event's
  18. scheduled time, and the function is called.
  19. The passage of Gillcup time is entirely in control of the programmer.
  20. It can be tied to the system clock to produce real-time animations,
  21. it can be slowed down or sped up,
  22. or an entire simulation can be run at once to get simulation results quickly.
  23. The Clock runs inside an asyncio event loop,
  24. using the future, callback, and coroutine mechanisms familiar to asyncio users.
  25. Gillcup uses its own :class:`~gillcup.futures.Future` objects that are tied
  26. to a clock that handles them.
  27. Any callbacks on a Gillcup future are handled by that future's clock;
  28. the Gillcup time does not advance between the future's completion
  29. and the callback execution.
  30. A coroutine can be scheduled on a Gillcup clock using
  31. :meth:`~gillcup.clocks.Clock.task`; see the corresponding docs for details.
  32. Reference
  33. ---------
  34. .. autofunction:: gillcup.clocks.coroutine
  35. .. autoclass:: gillcup.clocks.Clock
  36. .. autoclass:: gillcup.clocks.Subclock
  37. """
  38. import collections
  39. import heapq
  40. import asyncio
  41. import gillcup.futures
  42. from gillcup.util.signature import fix_public_signature
  43. from gillcup import expressions
  44. def coroutine(func):
  45. """Mark a function as a Gillcup coroutine.
  46. Direct equivalent of :func:`asyncio.coroutine` -- also does nothing
  47. (unless asyncio debugging is enabled).
  48. """
  49. return asyncio.coroutine(func)
  50. _Event = collections.namedtuple('_Event',
  51. 'time category index callback args')
  52. _Event.__doc__ = """
  53. Heap entry
  54. Namedtuple elements:
  55. .. attribute:: time
  56. The time for which the event is scheduled
  57. .. attribute:: category
  58. Category for sorting.
  59. Normal events have category of 0;
  60. the event that advance() creates to wait for has a category of 1
  61. to ensure other events at the same time have completed.
  62. .. attribute:: index
  63. Index for sorting.
  64. Unique to each _Event, asigned from a global counter.
  65. Used to keep FIFO ordering for actions scheduled for the same time.
  66. .. attribute:: callback
  67. The action to perform.
  68. .. attribute:: args
  69. Arguments to call :token:`callback` with.
  70. """
  71. _next_index = 0
  72. class Clock:
  73. """Keeps track of discrete time, and schedules events.
  74. Attributes:
  75. .. attribute:: time
  76. The current time on the clock. A read-only expression.
  77. Use :meth:`advance` to increase this value.
  78. .. attribute:: speed
  79. Arguments to :meth:`advance` are multiplied by this value.
  80. Usefull mainly for :class:`Subclock`.
  81. Methods:
  82. .. automethod:: schedule
  83. .. automethod:: wait_for
  84. .. automethod:: sleep
  85. .. automethod:: advance
  86. .. automethod:: advance_sync
  87. .. automethod:: task
  88. """
  89. def __init__(self):
  90. # Time on the clock
  91. self._time_value = 0
  92. # Heap queue of scheduled actions
  93. self.events = []
  94. # Recursion guard flag for advance()
  95. self.advancing = False
  96. # Set of dependent clocks
  97. self._subclocks = set()
  98. speed = 1
  99. @property
  100. def time(self):
  101. try:
  102. return self._time_exp
  103. except AttributeError:
  104. prop = self._time_exp = expressions.Time(self)
  105. return prop
  106. def _get_next_event(self):
  107. try:
  108. event = self.events[0]
  109. except IndexError:
  110. events = []
  111. else:
  112. events = [(event.time - self._time_value, event.category,
  113. event.index, self, event)]
  114. for subclock in self._subclocks:
  115. event = subclock._get_next_event()
  116. if event:
  117. remain, category, index, clock, event = event
  118. try:
  119. remain /= subclock.speed
  120. except ZeroDivisionError:
  121. # zero speed events never happen
  122. pass
  123. else:
  124. events.append((remain, category, index, clock, event))
  125. try:
  126. return min(events)
  127. except ValueError:
  128. return None
  129. @asyncio.coroutine
  130. @fix_public_signature
  131. def advance(self, delay, *, _continuing=False):
  132. """Advance the clock's time
  133. Moves the clock's time forward, pausing at times when
  134. actions are scheduled, and running them.
  135. :param delay: If :token:`delay` is a real number, move that many time
  136. units into the future.
  137. Attempting to move to the past (negative delay) will raise
  138. an error.
  139. If :token:`delay` is None, the Clock will advance until no more
  140. actions are scheduled on it.
  141. Note that with recurring events, ``advance(None)`` may
  142. never finish.
  143. Otherwise :token:`delay` should be a Future; in this case Clock
  144. will advance until either that future is done, or no more actions
  145. are scheduled.
  146. """
  147. if not _continuing:
  148. if self.advancing:
  149. raise RuntimeError('Clock.advance called recursively')
  150. if delay is None:
  151. delay = asyncio.Future()
  152. try:
  153. float(delay)
  154. except TypeError:
  155. # We want to wait for a *Gillcup* future on *this* clock,
  156. # with category 1
  157. delay = gillcup.futures.Future(self, delay, _category=1)
  158. else:
  159. if delay < 0:
  160. raise ValueError('Moving backwards in time')
  161. delay = self.sleep(delay * self.speed, _category=1)
  162. self.advancing = True
  163. if delay.done():
  164. self.advancing = False
  165. return
  166. event = self._get_next_event()
  167. if event is None:
  168. self.advancing = False
  169. return
  170. event_dt, _cat, _index, clock, event = event
  171. if event_dt:
  172. self._advance(event_dt)
  173. _evt = heapq.heappop(clock.events)
  174. assert _evt is event and clock._time_value == event.time
  175. # jump to the event's time
  176. clock._time_value = event.time
  177. # Handle the event (synchronously!)
  178. event.callback(*event.args)
  179. # finish jumping
  180. yield from asyncio.Task(self.advance(delay, _continuing=True))
  181. def advance_sync(self, delay):
  182. """Call (and wait for) :meth:`advance` outside of an event loop
  183. Runs asyncio's main event loop until ``advance()`` is finished.
  184. Useful in testing or in some non-realtime applications.
  185. """
  186. loop = asyncio.get_event_loop()
  187. loop.run_until_complete(self.advance(delay))
  188. def _advance(self, dt):
  189. self._time_value += dt
  190. for subclock in self._subclocks:
  191. subclock._advance(dt * subclock.speed)
  192. @fix_public_signature
  193. def sleep(self, delay, *, _category=0):
  194. """Return a future that will complete after "delay" time units
  195. Scheduling for the past (delay<0) will raise an error.
  196. """
  197. future = asyncio.Future()
  198. self.schedule(delay, future.set_result, None, _category=_category)
  199. return gillcup.futures.Future(self, future)
  200. def wait_for(self, future):
  201. """Wrap a future so that its calbacks are scheduled on this Clock
  202. Return a future that is done when the original one is,
  203. but any callbacks registered on it will be scheduled on this Clock.
  204. If the given future is already scheduling on this Clock,
  205. it is returned unchanged.
  206. """
  207. if isinstance(future, gillcup.futures.Future) and future.clock is self:
  208. return future
  209. else:
  210. return gillcup.futures.Future(self, future)
  211. @fix_public_signature
  212. def schedule(self, delay, callback, *args, _category=0):
  213. """Schedule callback to be called after "delay" time units
  214. """
  215. global _next_index
  216. if delay < 0:
  217. raise ValueError('Scheduling an action in the past')
  218. _next_index += 1
  219. scheduled_time = self._time_value + delay
  220. event = _Event(scheduled_time, _category, _next_index, callback, args)
  221. heapq.heappush(self.events, event)
  222. def task(self, coro):
  223. """Run an asyncio-style coroutine on this clock
  224. Futures yielded by the coroutine will be handled by this Clock.
  225. In addition to futures, the coroutine may yield real numbers,
  226. which are translated to :meth:`sleep`.
  227. """
  228. @asyncio.coroutine
  229. def coro_wrapper():
  230. iterator = iter(coro)
  231. value = exception = None
  232. while True:
  233. try:
  234. if exception is None:
  235. value = iterator.send(value)
  236. else:
  237. value = iterator.throw(exception)
  238. except StopIteration as exc:
  239. return exc.value
  240. if value is None:
  241. value = 0
  242. try:
  243. try:
  244. value = float(value)
  245. except TypeError:
  246. yield from self.wait_for(value)
  247. else:
  248. yield from self.sleep(value)
  249. except Exception as exc:
  250. value = None
  251. exception = exc
  252. except BaseException as exc:
  253. iterator.throw(exc)
  254. raise
  255. return asyncio.Task(coro_wrapper())
  256. class Subclock(Clock):
  257. """A Clock that advances in sync with another Clock
  258. A Subclock advances whenever its :token:`parent` clock does.
  259. Its :token:`speed` attribute specifies the relative speed relative
  260. to the parent clock.
  261. For example, if ``speed==2``, the subclock will run twice as fast as its
  262. parent clock.
  263. The actions scheduled on a parent Clock and all subclocks are run in the
  264. correct sequence, with consistent times on the clocks.
  265. """
  266. def __init__(self, parent, speed=1):
  267. super(Subclock, self).__init__()
  268. self.speed = speed
  269. parent._subclocks.add(self)