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

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

## 110 statements

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

2 Events are instances in time when some specific condition about the system

3 changes. For example, clocks split the domain of the time variable into a

4 possibly infinite sequence of continuous and disjoint intervals, each as long

5 as a period of the clock. Whenever the continuous time variable leaves one of

6 these intervals - and thereby enters the succeeding interval - a clock tick

7 occurs.

8

9 Similarly, zero-crossing events occur when the sign of a specific event function

10 changes. That event function may depend on the time and on the value of signals

11 and states.

12

13 Listeners are special functions that may change the value of states in the

14 system. They may be bound to events, meaning that they are executed whenever the

15 respective event occurs. Note that any listener may be bound to multiple events.

16

17 As each event may have multiple listeners bound to it, each occurrence of an

18 event may lead to multiple listeners being executed. Similarly, multiple events

19 may occur at any point in time, also possibly leading to multiple listeners

20 being executed.

21

22 The order of execution of listeners in this situation is undefined. Thus,

23 model developers should make sure that listeners acting on the same parts of the

24 state are confluent, i.e., that the final states resulting from different orders

25 of execution of listeners are equivalent to each other. What is considered

26`equivalent` may depend on the application.

27

28 Further, listeners may change the state in such a way that the sign of the

29 event function of a zero-crossing event changes. Thus, one event may lead to the

30 occurrence of another, and it is possible that a single event results in an

31 endless event loop. Thus, event listeners need to be expressed carefully so that

32 they do not trigger any unwanted events.

33"""

34 import heapq

35 from abc import ABC

36 from math import ceil

37 from typing import Optional

38

39 from modypy.model.ports import PortNotConnectedError

40

41

42 class MultipleEventSourcesError(RuntimeError):

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

44 shall be connected to each other."""

45

46

47 class 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

131 class 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

147 class 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

187 class 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

246 class 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

301 class _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