Hide keyboard shortcuts

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. 

8 

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. 

12 

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. 

16 

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. 

21 

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. 

27 

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 

38 

39from modypy.model.ports import PortNotConnectedError 

40 

41 

42class MultipleEventSourcesError(RuntimeError): 

43 """An exception raised when two ports connected to different clocks 

44 shall be connected to each other.""" 

45 

46 

47class EventPort: 

48 """An event port is a port that can be connected to other event ports.""" 

49 

50 def __init__(self, owner): 

51 self.owner = owner 

52 self._reference = self 

53 self._listeners = set() 

54 

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 

62 

63 @reference.setter 

64 def reference(self, new_reference): 

65 self._reference = new_reference 

66 

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""" 

71 

72 if self.reference == self: 

73 return None 

74 return self.reference.source 

75 

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 

82 

83 def connect(self, other): 

84 """Connect an event port to another event port. 

85 

86 Args: 

87 other: The other event port 

88 

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 """ 

94 

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 

116 

117 def register_listener(self, listener): 

118 """Register a listener for this event port. 

119 

120 Args: 

121 listener: The listener to register 

122 """ 

123 self.listeners.add(listener) 

124 

125 def __call__(self, provider): 

126 if self.source is None: 

127 raise PortNotConnectedError() 

128 return self.source(provider) 

129 

130 

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. 

134 

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. 

138 

139 ``AbstractEventSource`` is the abstract base class for all event sources.""" 

140 

141 @property 

142 def source(self): 

143 """An event source always has itself as source""" 

144 return self 

145 

146 

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 """ 

152 

153 def __init__(self, owner, event_function, direction=0, tolerance=1E-12): 

154 """ 

155 Create a new zero-crossing event-source. 

156 

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: 

163 

164 ``1`` 

165 Consider only changes from negative to positive 

166 

167 ``-1`` 

168 Consider only changes from positive to negative 

169 

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 """ 

176 

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 

182 

183 def __call__(self, system_state): 

184 return self.event_function(system_state) 

185 

186 

187class Clock(AbstractEventSource): 

188 """A clock is an event source that generates a periodic event.""" 

189 

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. 

198 

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``. 

205 

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 """ 

214 

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 

220 

221 self.owner.system.register_clock(self) 

222 

223 def tick_generator(self, not_before): 

224 """Return a generate that will yield the times of the ticks of 

225 this clock. 

226 

227 Args: 

228 not_before: The ticks shown shall not be before the given time 

229 

230 Returns: 

231 A generator for ticks 

232 """ 

233 

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 

238 

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 

244 

245 

246class ClockQueue: 

247 """Queue of clock events""" 

248 def __init__(self, start_time, clocks): 

249 self.clock_queue = [] 

250 

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 

263 

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 

271 

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() 

277 

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 

282 

283 clocks_to_tick.append(clock) 

284 

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 

297 

298 return clocks_to_tick 

299 

300 

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 """ 

305 

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 

310 

311 def __lt__(self, other): 

312 return self.tick_time < other.tick_time