Coverage for D:\Ralf Gerlich\git\modypy\modypy\model\events.py : 27%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Events are instances in time when some specific condition about the system
3changes. For example, clocks split the domain of the time variable into a
4possibly infinite sequence of continuous and disjoint intervals, each as long
5as a period of the clock. Whenever the continuous time variable leaves one of
6these intervals - and thereby enters the succeeding interval - a clock tick
7occurs.
9Similarly, zero-crossing events occur when the sign of a specific event function
10changes. That event function may depend on the time and on the value of signals
11and states.
13Listeners are special functions that may change the value of states in the
14system. They may be bound to events, meaning that they are executed whenever the
15respective event occurs. Note that any listener may be bound to multiple events.
17As each event may have multiple listeners bound to it, each occurrence of an
18event may lead to multiple listeners being executed. Similarly, multiple events
19may occur at any point in time, also possibly leading to multiple listeners
20being executed.
22The order of execution of listeners in this situation is undefined. Thus,
23model developers should make sure that listeners acting on the same parts of the
24state are confluent, i.e., that the final states resulting from different orders
25of execution of listeners are equivalent to each other. What is considered
26`equivalent` may depend on the application.
28Further, listeners may change the state in such a way that the sign of the
29event function of a zero-crossing event changes. Thus, one event may lead to the
30occurrence of another, and it is possible that a single event results in an
31endless event loop. Thus, event listeners need to be expressed carefully so that
32they do not trigger any unwanted events.
33"""
34import heapq
35from abc import ABC
36from math import ceil
37from typing import Optional
39from modypy.model.ports import PortNotConnectedError
42class MultipleEventSourcesError(RuntimeError):
43 """An exception raised when two ports connected to different clocks
44 shall be connected to each other."""
47class EventPort:
48 """An event port is a port that can be connected to other event ports."""
50 def __init__(self, owner):
51 self.owner = owner
52 self._reference = self
53 self._listeners = set()
55 @property
56 def reference(self):
57 """The event port that is referenced by connection to this event
58 port."""
59 if self._reference is not self:
60 self._reference = self._reference.reference
61 return self._reference
63 @reference.setter
64 def reference(self, new_reference):
65 self._reference = new_reference
67 @property
68 def source(self) -> Optional['EventPort']:
69 """The event source this port is connected to or ``None`` if it is
70 not connected to any event source"""
72 if self.reference == self:
73 return None
74 return self.reference.source
76 @property
77 def listeners(self):
78 """The listeners registered on this event port"""
79 if self.reference == self:
80 return self._listeners
81 return self.reference.listeners
83 def connect(self, other):
84 """Connect an event port to another event port.
86 Args:
87 other: The other event port
89 Raises:
90 MultipleEventSourcesError: raised when two ports that are already
91 connected to two different sources shall be connected to each
92 other
93 """
95 if self.source is not None and other.source is not None:
96 # Both ports are already connected to an event.
97 # It is an error if it's not the same event.
98 if self.source != other.source:
99 raise MultipleEventSourcesError()
100 else:
101 # At least one of the ports is not yet connected to an event.
102 # We select a common reference and join all the listeners connected
103 # to both ports in one set.
104 if other.source is not None:
105 # The other port is already connected to an event,
106 # so we choose the other port as reference and add
107 # our listeners to its listeners.
108 other.listeners.update(self.listeners)
109 self.reference.reference = other.reference
110 else:
111 # The other part is not yet connected to a event,
112 # so we make ourselves the reference and add
113 # its listeners to our listeners.
114 self.listeners.update(other.listeners)
115 other.reference.reference = self.reference
117 def register_listener(self, listener):
118 """Register a listener for this event port.
120 Args:
121 listener: The listener to register
122 """
123 self.listeners.add(listener)
125 def __call__(self, provider):
126 if self.source is None:
127 raise PortNotConnectedError()
128 return self.source(provider)
131class AbstractEventSource(EventPort, ABC):
132 """An event source defines the circumstances under which an event occurs.
133 Events are occurrences of special occurrence that may require a reaction.
135 Events can be reacted upon by updating the state of the system. For this
136 purpose, event listeners can be registered which are called upon occurrence
137 of the event.
139 ``AbstractEventSource`` is the abstract base class for all event sources."""
141 @property
142 def source(self):
143 """An event source always has itself as source"""
144 return self
147class ZeroCrossEventSource(AbstractEventSource):
148 """A ``ZeroCrossEventSource`` defines an event source by the change of sign
149 of a special event function. Such zero-cross events are specifically
150 monitored and the values of event functions are recorded by the simulator.
151 """
153 def __init__(self, owner, event_function, direction=0, tolerance=1E-12):
154 """
155 Create a new zero-crossing event-source.
157 Args:
158 owner: The system or block this event belongs to
159 event_function: The callable used to calculate the value of the
160 event function
161 direction: The direction of the sign change to consider
162 Possible values:
164 ``1``
165 Consider only changes from negative to positive
167 ``-1``
168 Consider only changes from positive to negative
170 ``0`` (default)
171 Consider all changes
172 tolerance: The tolerance around zero
173 Values with an absolute value less than or equal to
174 ``tolerance`` are considered to be zero
175 """
177 AbstractEventSource.__init__(self, owner)
178 self.event_function = event_function
179 self.direction = direction
180 self.event_index = self.owner.system.register_event(self)
181 self.tolerance = tolerance
183 def __call__(self, system_state):
184 return self.event_function(system_state)
187class Clock(AbstractEventSource):
188 """A clock is an event source that generates a periodic event."""
190 def __init__(self,
191 owner,
192 period,
193 start_time=0.0,
194 end_time=None,
195 run_before_start=False):
196 """
197 Construct a clock.
199 The clock generates a periodic tick occurring at multiples of
200 ``period`` offset by ``start_time``. If ``end_time`` is set to
201 a value other than ``None``, no ticks will be generated after
202 ``end_time``. If ``run_before_start`` is set to ``True``, the
203 clock will also generate ticks before the time defined by
204 ``start_time``.
206 Args:
207 owner: The owner object of this clock (a system or a block)
208 period: The period of the clock
209 start_time: The start time of the clock (default: 0)
210 end_time: The end time of the clock (default: ``None``)
211 run_before_start: Flag indicating whether the clock shall already
212 run before the start time (default: ``False``)
213 """
215 AbstractEventSource.__init__(self, owner)
216 self.period = period
217 self.start_time = start_time
218 self.end_time = end_time
219 self.run_before_start = run_before_start
221 self.owner.system.register_clock(self)
223 def tick_generator(self, not_before):
224 """Return a generate that will yield the times of the ticks of
225 this clock.
227 Args:
228 not_before: The ticks shown shall not be before the given time
230 Returns:
231 A generator for ticks
232 """
234 k = ceil((not_before - self.start_time) / self.period)
235 if k < 0 and not self.run_before_start:
236 # No ticks before the start
237 k = 0
239 tick_time = self.start_time + k * self.period
240 while self.end_time is None or tick_time <= self.end_time:
241 yield tick_time
242 k += 1
243 tick_time = self.start_time + k * self.period
246class ClockQueue:
247 """Queue of clock events"""
248 def __init__(self, start_time, clocks):
249 self.clock_queue = []
251 # Fill the queue
252 for clock in clocks:
253 # Get a tick generator, started at the current time
254 tick_generator = clock.tick_generator(start_time)
255 try:
256 first_tick = next(tick_generator)
257 entry = _TickEntry(first_tick, clock, tick_generator)
258 heapq.heappush(self.clock_queue, entry)
259 except StopIteration:
260 # The block did not produce any ticks at all,
261 # so we just ignore it
262 pass
264 @property
265 def next_clock_tick(self):
266 """The time at which the next clock tick will occur or `None` if there
267 are no further clock ticks"""
268 if len(self.clock_queue)>0:
269 return self.clock_queue[0].tick_time
270 return None
272 def tick(self, current_time):
273 """Advance all the clocks until the current time"""
274 # We collect the clocks to tick here and executed all their listeners
275 # later.
276 clocks_to_tick = list()
278 while (len(self.clock_queue) > 0 and
279 self.clock_queue[0].tick_time <= current_time):
280 tick_entry = heapq.heappop(self.clock_queue)
281 clock = tick_entry.clock
283 clocks_to_tick.append(clock)
285 try:
286 # Get the next tick for the clock
287 next_tick_time = next(tick_entry.tick_generator)
288 next_tick_entry = _TickEntry(next_tick_time,
289 clock,
290 tick_entry.tick_generator)
291 # Add the clock tick to the queue
292 heapq.heappush(self.clock_queue, next_tick_entry)
293 except StopIteration:
294 # This clock does not deliver any more ticks, so we simply
295 # ignore it from now on.
296 pass
298 return clocks_to_tick
301class _TickEntry:
302 """A ``_TickEntry`` holds information about the next tick of a given clock.
303 An order over ``_TickEntry`` instances is defined by their time.
304 """
306 def __init__(self, tick_time, clock, tick_generator):
307 self.tick_time = tick_time
308 self.clock = clock
309 self.tick_generator = tick_generator
311 def __lt__(self, other):
312 return self.tick_time < other.tick_time