commit
This commit is contained in:
commit
8a24549cdb
21 changed files with 3478 additions and 0 deletions
67
StartingPoint/lib/realtime/event_loop.py
Normal file
67
StartingPoint/lib/realtime/event_loop.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
from lib.controller import Controller
|
||||
from lib.realtime.realtime import WallClock, AbstractRealTimeSimulation
|
||||
import time
|
||||
import abc
|
||||
|
||||
class AbstractEventLoop:
|
||||
# delay in nanoseconds
|
||||
# should be non-blocking
|
||||
# should return timer ID
|
||||
@abc.abstractmethod
|
||||
def schedule(self, delay, callback):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def cancel(self, timer_id):
|
||||
pass
|
||||
|
||||
# Runs virtual (simulated) time as close as possible to (scaled) wall-clock time.
|
||||
# Depending on how fast your computer is, simulated time will always run a tiny bit behind wall-clock time, but this error will NOT grow over time.
|
||||
class EventLoopRealTimeSimulation(AbstractRealTimeSimulation):
|
||||
|
||||
def __init__(self, controller: Controller, event_loop: AbstractEventLoop, wall_clock: WallClock, termination_condition=lambda: False, time_advance_callback=lambda simtime:None):
|
||||
self.controller = controller
|
||||
self.event_loop = event_loop
|
||||
|
||||
self.wall_clock = wall_clock
|
||||
|
||||
self.termination_condition = termination_condition
|
||||
|
||||
# Just a callback indicating that the current simulated time has changed.
|
||||
# Can be useful for displaying the simulated time in a GUI or something
|
||||
self.time_advance_callback = time_advance_callback
|
||||
|
||||
# At most one timer will be scheduled at the same time
|
||||
self.scheduled_id = None
|
||||
|
||||
def poke(self):
|
||||
if self.scheduled_id is not None:
|
||||
self.event_loop.cancel(self.scheduled_id)
|
||||
|
||||
self.controller.run_until(self.wall_clock.time_since_start()) # this call may actually consume some time
|
||||
|
||||
self.time_advance_callback(self.controller.simulated_time)
|
||||
|
||||
if self.termination_condition():
|
||||
print("Termination condition satisfied. Stop mainloop.")
|
||||
return
|
||||
|
||||
if self.controller.have_event():
|
||||
# schedule next wakeup
|
||||
sleep_duration = self.wall_clock.sleep_duration_until(self.controller.get_earliest())
|
||||
self.scheduled_id = self.event_loop.schedule(sleep_duration, self.poke)
|
||||
# print("sleeping for", pretty_time(sleep_duration))
|
||||
else:
|
||||
# print("sleeping until woken up")
|
||||
pass
|
||||
|
||||
# generate input event at the current wall clock time
|
||||
# this method should be used for generating events that represent e.g., button clicks, key presses
|
||||
def add_input_now(self, sc, event, value=None):
|
||||
self.controller.add_input(sc, event, timestamp=self.wall_clock.time_since_start(), value=value)
|
||||
self.poke()
|
||||
|
||||
# for events that need to happen immediately, at the current point in simulated time
|
||||
def add_input_sync(self, sc, event, value=None):
|
||||
self.controller.add_input_relative(sc, event, value=value)
|
||||
self.poke()
|
||||
35
StartingPoint/lib/realtime/realtime.py
Normal file
35
StartingPoint/lib/realtime/realtime.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import time
|
||||
import abc
|
||||
|
||||
# Use time_scale different from 1.0 for scaled real-time execution:
|
||||
# time_scale > 1 speeds up simulation
|
||||
# 0 < time_scale < 1 slows down simulation
|
||||
class WallClock:
|
||||
def __init__(self, time_scale=1.0):
|
||||
self.time_scale = time_scale
|
||||
self.purposefully_behind = 0
|
||||
|
||||
def record_start_time(self):
|
||||
self.start_time = time.perf_counter_ns()
|
||||
|
||||
def time_since_start(self):
|
||||
time_since_start = time.perf_counter_ns() - self.start_time
|
||||
return (time_since_start * self.time_scale) + self.purposefully_behind
|
||||
|
||||
def sleep_duration_until(self, earliest_event_time):
|
||||
now = self.time_since_start()
|
||||
sleep_duration = int((earliest_event_time - now) / self.time_scale)
|
||||
# sleep_duration can be negative, if the next event is in the past
|
||||
# This indicates that our computer is too slow, and cannot keep up with the simulation.
|
||||
# Like all things fate-related, we embrace this slowness, rather than fighting it:
|
||||
# We will temporarily run the simulation at a slower pace, which has the benefit of the simulation remaining responsive to user input.
|
||||
self.purposefully_behind = min(sleep_duration, 0) # see above comment
|
||||
actual_sleep_duration = max(sleep_duration, 0) # can never sleep less than 0
|
||||
return actual_sleep_duration
|
||||
|
||||
class AbstractRealTimeSimulation:
|
||||
# Generate input event at the current wall clock time (with time-scale applied, of course)
|
||||
# This method should be used for interactive simulation, for generating events that were caused by e.g., button clicks, key presses, ...
|
||||
@abc.abstractmethod
|
||||
def add_input_now(self, sc, event, value=None):
|
||||
pass
|
||||
43
StartingPoint/lib/realtime/threaded.py
Normal file
43
StartingPoint/lib/realtime/threaded.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import threading
|
||||
|
||||
from lib.realtime.realtime import WallClock, AbstractRealTimeSimulation
|
||||
from lib.controller import Controller, pretty_time
|
||||
|
||||
# Runs simulation, real-time, in its own thread
|
||||
#
|
||||
# Typical usage:
|
||||
# thread = threading.Thread(
|
||||
# target=ThreadedRealTimeSimulation(...).mainloop,
|
||||
# )
|
||||
# thread.start()
|
||||
class ThreadedRealTimeSimulation(AbstractRealTimeSimulation):
|
||||
def __init__(self, controller: Controller, wall_clock: WallClock, termination_condition = lambda: False):
|
||||
self.controller = controller
|
||||
self.wall_clock = wall_clock
|
||||
self.termination_condition = termination_condition
|
||||
self.condition = threading.Condition()
|
||||
|
||||
def mainloop(self):
|
||||
while True:
|
||||
self.controller.run_until(self.wall_clock.time_since_start())
|
||||
if self.termination_condition():
|
||||
print("Termination condition satisfied. Stop mainloop.")
|
||||
return
|
||||
if self.controller.have_event():
|
||||
earliest_event_time = self.controller.get_earliest()
|
||||
sleep_duration = self.wall_clock.sleep_duration_until(earliest_event_time)
|
||||
with self.condition:
|
||||
# print('thread sleeping for', pretty_time(sleep_duration), 'or until interrupted')
|
||||
self.condition.wait(sleep_duration / 1000000000)
|
||||
# print('thread woke up')
|
||||
else:
|
||||
with self.condition:
|
||||
# print('thread sleeping until interrupted')
|
||||
self.condition.wait()
|
||||
|
||||
def add_input_now(self, sc, event, value=None):
|
||||
with self.condition:
|
||||
self.controller.add_input(sc, event,
|
||||
timestamp=self.wall_clock.time_since_start(),
|
||||
value=value)
|
||||
self.condition.notify()
|
||||
13
StartingPoint/lib/realtime/tk_event_loop.py
Normal file
13
StartingPoint/lib/realtime/tk_event_loop.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
from lib.realtime.event_loop import AbstractEventLoop
|
||||
|
||||
# schedules calls in an existing tkinter eventloop
|
||||
class TkEventLoopAdapter(AbstractEventLoop):
|
||||
def __init__(self, tk):
|
||||
self.tk = tk
|
||||
|
||||
def schedule(self, delay, callback):
|
||||
return self.tk.after(int(delay / 1000000), # ns to ms
|
||||
callback)
|
||||
|
||||
def cancel(self, timer):
|
||||
self.tk.after_cancel(timer)
|
||||
Loading…
Add table
Add a link
Reference in a new issue