asimpy 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. asimpy-0.3.0/PKG-INFO +241 -0
  2. asimpy-0.3.0/README.md +216 -0
  3. {asimpy-0.2.0 → asimpy-0.3.0}/docs/404.html +85 -4
  4. {asimpy-0.2.0 → asimpy-0.3.0}/docs/CODE_OF_CONDUCT/index.html +85 -4
  5. {asimpy-0.2.0 → asimpy-0.3.0}/docs/CONTRIBUTING/index.html +85 -4
  6. {asimpy-0.2.0 → asimpy-0.3.0}/docs/LICENSE/index.html +85 -4
  7. asimpy-0.3.0/docs/allof/index.html +1024 -0
  8. {asimpy-0.2.0/docs/gate → asimpy-0.3.0/docs/barrier}/index.html +189 -92
  9. {asimpy-0.2.0/docs → asimpy-0.3.0/docs/environment}/index.html +219 -46
  10. asimpy-0.3.0/docs/event.py +1 -0
  11. {asimpy-0.2.0 → asimpy-0.3.0}/docs/examples/index.html +296 -79
  12. asimpy-0.3.0/docs/firstof/index.html +1034 -0
  13. asimpy-0.3.0/docs/index.html +1134 -0
  14. {asimpy-0.2.0 → asimpy-0.3.0}/docs/interrupt/index.html +97 -84
  15. asimpy-0.3.0/docs/objects.inv +6 -0
  16. {asimpy-0.2.0 → asimpy-0.3.0}/docs/process/index.html +414 -90
  17. asimpy-0.3.0/docs/queue/index.html +1373 -0
  18. {asimpy-0.2.0 → asimpy-0.3.0}/docs/resource/index.html +195 -148
  19. {asimpy-0.2.0 → asimpy-0.3.0}/docs/sitemap.xml.gz +0 -0
  20. {asimpy-0.2.0/docs/actions → asimpy-0.3.0/docs/timeout}/index.html +208 -164
  21. asimpy-0.3.0/examples/allof.py +13 -0
  22. asimpy-0.3.0/examples/barrier.py +37 -0
  23. asimpy-0.3.0/examples/firstof_queue.py +32 -0
  24. asimpy-0.3.0/examples/firstof_timeout.py +18 -0
  25. {asimpy-0.2.0 → asimpy-0.3.0}/examples/interrupt.py +10 -10
  26. {asimpy-0.2.0 → asimpy-0.3.0}/examples/priqueue.py +2 -2
  27. {asimpy-0.2.0 → asimpy-0.3.0}/examples/queue.py +5 -5
  28. {asimpy-0.2.0 → asimpy-0.3.0}/examples/resource.py +4 -4
  29. asimpy-0.2.0/examples/sleep.py → asimpy-0.3.0/examples/timeout.py +4 -4
  30. asimpy-0.3.0/output/allof.txt +2 -0
  31. asimpy-0.3.0/output/firstof_queue.txt +1 -0
  32. asimpy-0.3.0/output/firstof_timeout.txt +3 -0
  33. asimpy-0.3.0/output/interrupt.txt +14 -0
  34. asimpy-0.3.0/pages/allof.md +1 -0
  35. asimpy-0.3.0/pages/barrier.md +1 -0
  36. asimpy-0.3.0/pages/event.py +1 -0
  37. {asimpy-0.2.0 → asimpy-0.3.0}/pages/examples.md +33 -6
  38. asimpy-0.3.0/pages/firstof.md +1 -0
  39. asimpy-0.3.0/pages/queue.md +1 -0
  40. asimpy-0.3.0/pages/timeout.md +1 -0
  41. {asimpy-0.2.0 → asimpy-0.3.0}/pyproject.toml +21 -1
  42. {asimpy-0.2.0 → asimpy-0.3.0}/src/asimpy/__init__.py +5 -1
  43. asimpy-0.3.0/src/asimpy/_adapt.py +25 -0
  44. asimpy-0.3.0/src/asimpy/allof.py +50 -0
  45. asimpy-0.3.0/src/asimpy/barrier.py +33 -0
  46. asimpy-0.3.0/src/asimpy/environment.py +51 -0
  47. asimpy-0.3.0/src/asimpy/event.py +56 -0
  48. asimpy-0.3.0/src/asimpy/firstof.py +56 -0
  49. asimpy-0.3.0/src/asimpy/interrupt.py +20 -0
  50. asimpy-0.3.0/src/asimpy/process.py +92 -0
  51. asimpy-0.3.0/src/asimpy/queue.py +64 -0
  52. asimpy-0.3.0/src/asimpy/resource.py +69 -0
  53. asimpy-0.3.0/src/asimpy/timeout.py +23 -0
  54. {asimpy-0.2.0 → asimpy-0.3.0}/uv.lock +79 -30
  55. asimpy-0.2.0/PKG-INFO +0 -41
  56. asimpy-0.2.0/README.md +0 -16
  57. asimpy-0.2.0/docs/environment/index.html +0 -1477
  58. asimpy-0.2.0/docs/objects.inv +0 -7
  59. asimpy-0.2.0/examples/gate.py +0 -37
  60. asimpy-0.2.0/output/interrupt.txt +0 -22
  61. asimpy-0.2.0/pages/actions.md +0 -1
  62. asimpy-0.2.0/pages/gate.md +0 -1
  63. asimpy-0.2.0/src/asimpy/actions.py +0 -26
  64. asimpy-0.2.0/src/asimpy/environment.py +0 -121
  65. asimpy-0.2.0/src/asimpy/gate.py +0 -57
  66. asimpy-0.2.0/src/asimpy/interrupt.py +0 -25
  67. asimpy-0.2.0/src/asimpy/process.py +0 -56
  68. asimpy-0.2.0/src/asimpy/queue.py +0 -147
  69. asimpy-0.2.0/src/asimpy/resource.py +0 -76
  70. {asimpy-0.2.0 → asimpy-0.3.0}/.gitignore +0 -0
  71. {asimpy-0.2.0 → asimpy-0.3.0}/CODE_OF_CONDUCT.md +0 -0
  72. {asimpy-0.2.0 → asimpy-0.3.0}/CONTRIBUTING.md +0 -0
  73. {asimpy-0.2.0 → asimpy-0.3.0}/LICENSE.md +0 -0
  74. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/_mkdocstrings.css +0 -0
  75. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/images/favicon.png +0 -0
  76. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/bundle.79ae519e.min.js +0 -0
  77. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/bundle.79ae519e.min.js.map +0 -0
  78. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.ar.min.js +0 -0
  79. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.da.min.js +0 -0
  80. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.de.min.js +0 -0
  81. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.du.min.js +0 -0
  82. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.el.min.js +0 -0
  83. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.es.min.js +0 -0
  84. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.fi.min.js +0 -0
  85. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.fr.min.js +0 -0
  86. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.he.min.js +0 -0
  87. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.hi.min.js +0 -0
  88. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.hu.min.js +0 -0
  89. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.hy.min.js +0 -0
  90. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.it.min.js +0 -0
  91. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.ja.min.js +0 -0
  92. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.jp.min.js +0 -0
  93. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.kn.min.js +0 -0
  94. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.ko.min.js +0 -0
  95. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.multi.min.js +0 -0
  96. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.nl.min.js +0 -0
  97. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.no.min.js +0 -0
  98. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.pt.min.js +0 -0
  99. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.ro.min.js +0 -0
  100. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.ru.min.js +0 -0
  101. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.sa.min.js +0 -0
  102. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -0
  103. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.sv.min.js +0 -0
  104. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.ta.min.js +0 -0
  105. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.te.min.js +0 -0
  106. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.th.min.js +0 -0
  107. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.tr.min.js +0 -0
  108. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.vi.min.js +0 -0
  109. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.zh.min.js +0 -0
  110. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/tinyseg.js +0 -0
  111. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/wordcut.js +0 -0
  112. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/workers/search.2c215733.min.js +0 -0
  113. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/workers/search.2c215733.min.js.map +0 -0
  114. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/stylesheets/main.484c7ddc.min.css +0 -0
  115. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/stylesheets/main.484c7ddc.min.css.map +0 -0
  116. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/stylesheets/palette.ab4e12ef.min.css +0 -0
  117. {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -0
  118. {asimpy-0.2.0 → asimpy-0.3.0}/docs/sitemap.xml +0 -0
  119. {asimpy-0.2.0 → asimpy-0.3.0}/mkdocs.yml +0 -0
  120. asimpy-0.2.0/output/gate.txt → asimpy-0.3.0/output/barrier.txt +3 -3
  121. {asimpy-0.2.0 → asimpy-0.3.0}/output/priqueue.txt +1 -1
  122. {asimpy-0.2.0 → asimpy-0.3.0}/output/queue.txt +3 -3
  123. {asimpy-0.2.0 → asimpy-0.3.0}/output/resource.txt +0 -0
  124. /asimpy-0.2.0/output/sleep.txt → /asimpy-0.3.0/output/timeout.txt +0 -0
  125. {asimpy-0.2.0 → asimpy-0.3.0}/pages/.pages +0 -0
  126. {asimpy-0.2.0 → asimpy-0.3.0}/pages/CODE_OF_CONDUCT.md +0 -0
  127. {asimpy-0.2.0 → asimpy-0.3.0}/pages/CONTRIBUTING.md +0 -0
  128. {asimpy-0.2.0 → asimpy-0.3.0}/pages/LICENSE.md +0 -0
  129. {asimpy-0.2.0 → asimpy-0.3.0}/pages/environment.md +0 -0
  130. {asimpy-0.2.0 → asimpy-0.3.0}/pages/index.md +0 -0
  131. {asimpy-0.2.0 → asimpy-0.3.0}/pages/interrupt.md +0 -0
  132. {asimpy-0.2.0 → asimpy-0.3.0}/pages/process.md +0 -0
  133. {asimpy-0.2.0 → asimpy-0.3.0}/pages/resource.md +0 -0
asimpy-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,241 @@
1
+ Metadata-Version: 2.4
2
+ Name: asimpy
3
+ Version: 0.3.0
4
+ Summary: A simple discrete event simulator using async/await
5
+ Author-email: Greg Wilson <gvwilson@third-bit.com>
6
+ Maintainer-email: Greg Wilson <gvwilson@third-bit.com>
7
+ License-File: LICENSE.md
8
+ Keywords: discrete event simulation,open source
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.13
13
+ Provides-Extra: dev
14
+ Requires-Dist: build>=1.4.0; extra == 'dev'
15
+ Requires-Dist: markdown-include>=0.8.1; extra == 'dev'
16
+ Requires-Dist: mkdocs-awesome-pages-plugin>=2.10.1; extra == 'dev'
17
+ Requires-Dist: mkdocs-material>=9.7.1; extra == 'dev'
18
+ Requires-Dist: mkdocs>=1.6.1; extra == 'dev'
19
+ Requires-Dist: mkdocstrings[python]>=1.0.0; extra == 'dev'
20
+ Requires-Dist: ruff>=0.14.10; extra == 'dev'
21
+ Requires-Dist: taskipy>=1.14.1; extra == 'dev'
22
+ Requires-Dist: twine>=6.2.0; extra == 'dev'
23
+ Requires-Dist: ty>=0.0.11; extra == 'dev'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # asimpy
27
+
28
+ A simple discrete event simulation framework in Python using `async`/`await`.
29
+
30
+ - [Documentation][docs]
31
+ - [Package][package]
32
+ - [Repository][repo]
33
+ - [Examples][examples]
34
+
35
+ *Thanks to the creators of [SimPy][simpy] for inspiration.*
36
+
37
+ ## What This Is
38
+
39
+ This discrete-event simulation framework uses Python's `async`/`await` without `asyncio`.
40
+ Key concepts include:
41
+
42
+ - Simulation time is virtual, not wall-clock time.
43
+ - Processes are active entities (customers, producers, actors).
44
+ - Events are things that happen at specific simulation times.
45
+ - `await` is used to pause a process until an event occurs.
46
+ - A single-threaded event loop (the `Environment` class) advances simulated time and resumes processes.
47
+
48
+ The result feels like writing synchronous code, but executes deterministically.
49
+
50
+ ## The Environment
51
+
52
+ The `Environment` class is the core of `asimpy`.
53
+ It store upcoming events in a heap that keeps entries ordered by (simulated) time.
54
+ `env.run()` repeatedly pops the earliest scheduled callback,
55
+ advances `env._now` to that time,
56
+ and runs the callback.
57
+
58
+ > The key idea is that *nothing runs until the environment schedules it*.
59
+
60
+ ## Events
61
+
62
+ An `Event` represents something that may happen later (similar to an `asyncio` `Future`).
63
+ When a process runs:
64
+
65
+ ```python
66
+ value = await some_event
67
+ ```
68
+
69
+ the following happens:
70
+
71
+ 1. `Event.__await__()` yields the event object.
72
+ 1. The process is suspended.
73
+ 1. The process is registered as a waiter on that event.
74
+
75
+ When `event.succeed(value)` is called:
76
+
77
+ 1. The event is marked as triggered.
78
+ 1. All waiting processes are resumed.
79
+ 1. Each of those processes receives `value` as the result of `await`.
80
+
81
+ One way to understand `Event` is to look at the purpose of its key attributes.
82
+
83
+ ### `_triggered`
84
+
85
+ The Boolean `_triggered` indicates whether this event has already successfully occurred.
86
+ It is initially `False`,
87
+ and is set to `True` when `evt.succeed(value)` is called.
88
+ If the event has been triggered before,
89
+ and some process wants to wait on it later,
90
+ that process is immediately resumed with the store `_value`.
91
+ This prevents lost or duplicated notifications.
92
+
93
+ ```python
94
+ def succeed(self, value=None):
95
+ if self._triggered or self._cancelled:
96
+ return
97
+ self._triggered = True
98
+ self._value = value
99
+ for proc in self._waiters:
100
+ proc._resume(value)
101
+ self._waiters.clear()
102
+ ```
103
+
104
+ ### `_cancelled` and `_on_cancel`
105
+
106
+ The Boolean `_cancelled` indicates whether the event was aborted before it could succeed.
107
+ It is also initially `False`,
108
+ and is used in both `succeed()` (shown above) and in `cancel()`:
109
+
110
+ ```python
111
+ def cancel(self):
112
+ if self._triggered or self._cancelled:
113
+ return
114
+ self._cancelled = True
115
+ self._waiters.clear()
116
+ if self._on_cancel:
117
+ self._on_cancel()
118
+ ```
119
+
120
+ If a cancellation callback has been registered with the event,
121
+ the callback is executed to do resource cleanup.
122
+ For example,
123
+ suppose a program is using `FirstOf` to wait until an item is available
124
+ on one or the other of two queues.
125
+ If both queues become ready at the same simulated time,
126
+ the program might take one item from both queues
127
+ when it should instead take an item from one or the other queue.
128
+ To clean this up,
129
+ `Queue.get()` registers a callback that puts an item back at the front of the queue
130
+ to prevent incorrect over-consumption of items:
131
+
132
+ ```python
133
+ evt._on_cancel = lambda: self._items.insert(0, item)
134
+ ```
135
+
136
+ ### `_value`
137
+
138
+ When `evt.succeed(value)` is called,
139
+ the value passed in is saved as `evt._value`.
140
+ Any process resuming from `await event` receives this value:
141
+
142
+ ```python
143
+ class Event:
144
+ def __await__(self):
145
+ value = yield self
146
+ return value
147
+ ```
148
+
149
+ This is how the framework passes results between processes:
150
+ for example,
151
+ the value of a `Queue.get()` event is the item retrieved from the queue.
152
+
153
+ ### `_waiters`
154
+
155
+ `_waiters` is a list of processes waiting for the event to complete.
156
+ When a process `await`s an event,
157
+ it is added to the list by the internal method `_add_waiter()`.
158
+ If the event has already been triggered,
159
+ `_add_waiter` immediately resumes the process:
160
+
161
+ ```python
162
+ def _add_waiter(self, proc):
163
+ if self._triggered:
164
+ proc._resume(self._value)
165
+ elif not self._cancelled:
166
+ self._waiters.append(proc)
167
+ ```
168
+
169
+ When `succeed()` is called,
170
+ every process in `_waiters` is resumed and `_waiters` is cleared.
171
+ When `cancel()` is called, `_waiters` is cleared without resuming any process.
172
+
173
+ ## Processes
174
+
175
+ The `Process` class wraps an `async def run()` coroutine.
176
+ When a `Process` is created,
177
+ its constructor called `self.run()` to create a coroutine
178
+ and then gives the `self._loop` callback to the `Enviroment`,
179
+ which schedules it to run.
180
+
181
+ `self._loop` drives the coroutine by executing:
182
+
183
+ ```python
184
+ yielded = self._coro.send(value)
185
+ yielded._add_waiter(self)
186
+ ```
187
+
188
+ 1. The coroutine runs until it hits `await`.
189
+ 1. The `await` yields an `Event`.
190
+ 1. The process registers itself as waiting on that event.
191
+ 1. Control returns to the environment.
192
+
193
+ When the event fires, it calls:
194
+
195
+ ```python
196
+ proc._resume(value)
197
+ ```
198
+
199
+ which schedules `_loop(value)` again.
200
+
201
+ ## Example: `Timeout`
202
+
203
+ The entire `Timeout` class is:
204
+
205
+ ```python
206
+ class Timeout(Event):
207
+ def __init__(self, env, delay):
208
+ super().__init__(env)
209
+ env.schedule(env.now + delay, lambda: self.succeed())
210
+ ```
211
+
212
+ which simply asks the `Environment` to schedule `Event.succeed()` in the simulated future.
213
+
214
+ ## Example: `FirstOf`
215
+
216
+ Suppose the queue `q1` contains `"A"` and the `q2` contains `"B"`,
217
+ and a process then waits for a value from either queue with:
218
+
219
+ ```python
220
+ item = await FirstOf(env, a=q1.get(), b=q2.get())
221
+ ```
222
+
223
+ The sequence of events is:
224
+
225
+ | Action / Event | Queue State | Event State | Process / Waiters | Notes |
226
+ | ----------------------------------------- | ------------------ | --------------------------------------- | ------------------------------------------------ | --------------------------------------------------------- |
227
+ | `Tester` starts | q1=["A"], q2=["B"] | evt_a & evt_b created | `_waiters` in evt_a and evt_b = [Tester] | `FirstOf` yields to environment |
228
+ | `q1.get()` removes "A" | q1=[], q2=["B"] | evt_a._value=None, _triggered=False | evt_a._waiters=[FirstOfWatcher] | _on_cancel set to re-insert "A" if cancelled |
229
+ | `q2.get()` removes "B" | q1=[], q2=[] | evt_b._value=None, _triggered=False | evt_b._waiters=[FirstOfWatcher] | _on_cancel set to re-insert "B" if cancelled |
230
+ | Environment immediately triggers evt_a | q1=[], q2=[] | evt_a._value="A", _triggered=True | _waiters processed: FirstOf._child_done() called | This is the winner |
231
+ | FirstOf `_child_done()` sets `_done=True` | q1=[], q2=[] | _done=True | _events = {a: evt_a, b: evt_b} | Notifies all losing events to cancel |
232
+ | evt_b.cancel() called (loser) | q1=[], q2=[] | evt_b._cancelled=True | _waiters cleared | _on_cancel triggers: inserts "B" back at front of q2 |
233
+ | `_on_cancel` restores item | q1=[], q2=["B"] | evt_b._triggered=False, _cancelled=True | _waiters cleared | Queue order preserved |
234
+ | FirstOf succeeds with winner | q1=[], q2=["B"] | _value=("a","A"), _triggered=True | Processes waiting on FirstOf resumed | `Tester` receives ("a","A") |
235
+ | `Tester` continues execution | q1=[], q2=["B"] | - | - | Remaining queue items untouched; correct order guaranteed |
236
+
237
+ [docs]: https://gvwilson.github.io/asimpy
238
+ [examples]: https://gvwilson.github.io/asimpy/examples/
239
+ [package]: https://pypi.org/project/asimpy/
240
+ [repo]: https://github.com/gvwilson/asimpy
241
+ [simpy]: https://simpy.readthedocs.io/
asimpy-0.3.0/README.md ADDED
@@ -0,0 +1,216 @@
1
+ # asimpy
2
+
3
+ A simple discrete event simulation framework in Python using `async`/`await`.
4
+
5
+ - [Documentation][docs]
6
+ - [Package][package]
7
+ - [Repository][repo]
8
+ - [Examples][examples]
9
+
10
+ *Thanks to the creators of [SimPy][simpy] for inspiration.*
11
+
12
+ ## What This Is
13
+
14
+ This discrete-event simulation framework uses Python's `async`/`await` without `asyncio`.
15
+ Key concepts include:
16
+
17
+ - Simulation time is virtual, not wall-clock time.
18
+ - Processes are active entities (customers, producers, actors).
19
+ - Events are things that happen at specific simulation times.
20
+ - `await` is used to pause a process until an event occurs.
21
+ - A single-threaded event loop (the `Environment` class) advances simulated time and resumes processes.
22
+
23
+ The result feels like writing synchronous code, but executes deterministically.
24
+
25
+ ## The Environment
26
+
27
+ The `Environment` class is the core of `asimpy`.
28
+ It store upcoming events in a heap that keeps entries ordered by (simulated) time.
29
+ `env.run()` repeatedly pops the earliest scheduled callback,
30
+ advances `env._now` to that time,
31
+ and runs the callback.
32
+
33
+ > The key idea is that *nothing runs until the environment schedules it*.
34
+
35
+ ## Events
36
+
37
+ An `Event` represents something that may happen later (similar to an `asyncio` `Future`).
38
+ When a process runs:
39
+
40
+ ```python
41
+ value = await some_event
42
+ ```
43
+
44
+ the following happens:
45
+
46
+ 1. `Event.__await__()` yields the event object.
47
+ 1. The process is suspended.
48
+ 1. The process is registered as a waiter on that event.
49
+
50
+ When `event.succeed(value)` is called:
51
+
52
+ 1. The event is marked as triggered.
53
+ 1. All waiting processes are resumed.
54
+ 1. Each of those processes receives `value` as the result of `await`.
55
+
56
+ One way to understand `Event` is to look at the purpose of its key attributes.
57
+
58
+ ### `_triggered`
59
+
60
+ The Boolean `_triggered` indicates whether this event has already successfully occurred.
61
+ It is initially `False`,
62
+ and is set to `True` when `evt.succeed(value)` is called.
63
+ If the event has been triggered before,
64
+ and some process wants to wait on it later,
65
+ that process is immediately resumed with the store `_value`.
66
+ This prevents lost or duplicated notifications.
67
+
68
+ ```python
69
+ def succeed(self, value=None):
70
+ if self._triggered or self._cancelled:
71
+ return
72
+ self._triggered = True
73
+ self._value = value
74
+ for proc in self._waiters:
75
+ proc._resume(value)
76
+ self._waiters.clear()
77
+ ```
78
+
79
+ ### `_cancelled` and `_on_cancel`
80
+
81
+ The Boolean `_cancelled` indicates whether the event was aborted before it could succeed.
82
+ It is also initially `False`,
83
+ and is used in both `succeed()` (shown above) and in `cancel()`:
84
+
85
+ ```python
86
+ def cancel(self):
87
+ if self._triggered or self._cancelled:
88
+ return
89
+ self._cancelled = True
90
+ self._waiters.clear()
91
+ if self._on_cancel:
92
+ self._on_cancel()
93
+ ```
94
+
95
+ If a cancellation callback has been registered with the event,
96
+ the callback is executed to do resource cleanup.
97
+ For example,
98
+ suppose a program is using `FirstOf` to wait until an item is available
99
+ on one or the other of two queues.
100
+ If both queues become ready at the same simulated time,
101
+ the program might take one item from both queues
102
+ when it should instead take an item from one or the other queue.
103
+ To clean this up,
104
+ `Queue.get()` registers a callback that puts an item back at the front of the queue
105
+ to prevent incorrect over-consumption of items:
106
+
107
+ ```python
108
+ evt._on_cancel = lambda: self._items.insert(0, item)
109
+ ```
110
+
111
+ ### `_value`
112
+
113
+ When `evt.succeed(value)` is called,
114
+ the value passed in is saved as `evt._value`.
115
+ Any process resuming from `await event` receives this value:
116
+
117
+ ```python
118
+ class Event:
119
+ def __await__(self):
120
+ value = yield self
121
+ return value
122
+ ```
123
+
124
+ This is how the framework passes results between processes:
125
+ for example,
126
+ the value of a `Queue.get()` event is the item retrieved from the queue.
127
+
128
+ ### `_waiters`
129
+
130
+ `_waiters` is a list of processes waiting for the event to complete.
131
+ When a process `await`s an event,
132
+ it is added to the list by the internal method `_add_waiter()`.
133
+ If the event has already been triggered,
134
+ `_add_waiter` immediately resumes the process:
135
+
136
+ ```python
137
+ def _add_waiter(self, proc):
138
+ if self._triggered:
139
+ proc._resume(self._value)
140
+ elif not self._cancelled:
141
+ self._waiters.append(proc)
142
+ ```
143
+
144
+ When `succeed()` is called,
145
+ every process in `_waiters` is resumed and `_waiters` is cleared.
146
+ When `cancel()` is called, `_waiters` is cleared without resuming any process.
147
+
148
+ ## Processes
149
+
150
+ The `Process` class wraps an `async def run()` coroutine.
151
+ When a `Process` is created,
152
+ its constructor called `self.run()` to create a coroutine
153
+ and then gives the `self._loop` callback to the `Enviroment`,
154
+ which schedules it to run.
155
+
156
+ `self._loop` drives the coroutine by executing:
157
+
158
+ ```python
159
+ yielded = self._coro.send(value)
160
+ yielded._add_waiter(self)
161
+ ```
162
+
163
+ 1. The coroutine runs until it hits `await`.
164
+ 1. The `await` yields an `Event`.
165
+ 1. The process registers itself as waiting on that event.
166
+ 1. Control returns to the environment.
167
+
168
+ When the event fires, it calls:
169
+
170
+ ```python
171
+ proc._resume(value)
172
+ ```
173
+
174
+ which schedules `_loop(value)` again.
175
+
176
+ ## Example: `Timeout`
177
+
178
+ The entire `Timeout` class is:
179
+
180
+ ```python
181
+ class Timeout(Event):
182
+ def __init__(self, env, delay):
183
+ super().__init__(env)
184
+ env.schedule(env.now + delay, lambda: self.succeed())
185
+ ```
186
+
187
+ which simply asks the `Environment` to schedule `Event.succeed()` in the simulated future.
188
+
189
+ ## Example: `FirstOf`
190
+
191
+ Suppose the queue `q1` contains `"A"` and the `q2` contains `"B"`,
192
+ and a process then waits for a value from either queue with:
193
+
194
+ ```python
195
+ item = await FirstOf(env, a=q1.get(), b=q2.get())
196
+ ```
197
+
198
+ The sequence of events is:
199
+
200
+ | Action / Event | Queue State | Event State | Process / Waiters | Notes |
201
+ | ----------------------------------------- | ------------------ | --------------------------------------- | ------------------------------------------------ | --------------------------------------------------------- |
202
+ | `Tester` starts | q1=["A"], q2=["B"] | evt_a & evt_b created | `_waiters` in evt_a and evt_b = [Tester] | `FirstOf` yields to environment |
203
+ | `q1.get()` removes "A" | q1=[], q2=["B"] | evt_a._value=None, _triggered=False | evt_a._waiters=[FirstOfWatcher] | _on_cancel set to re-insert "A" if cancelled |
204
+ | `q2.get()` removes "B" | q1=[], q2=[] | evt_b._value=None, _triggered=False | evt_b._waiters=[FirstOfWatcher] | _on_cancel set to re-insert "B" if cancelled |
205
+ | Environment immediately triggers evt_a | q1=[], q2=[] | evt_a._value="A", _triggered=True | _waiters processed: FirstOf._child_done() called | This is the winner |
206
+ | FirstOf `_child_done()` sets `_done=True` | q1=[], q2=[] | _done=True | _events = {a: evt_a, b: evt_b} | Notifies all losing events to cancel |
207
+ | evt_b.cancel() called (loser) | q1=[], q2=[] | evt_b._cancelled=True | _waiters cleared | _on_cancel triggers: inserts "B" back at front of q2 |
208
+ | `_on_cancel` restores item | q1=[], q2=["B"] | evt_b._triggered=False, _cancelled=True | _waiters cleared | Queue order preserved |
209
+ | FirstOf succeeds with winner | q1=[], q2=["B"] | _value=("a","A"), _triggered=True | Processes waiting on FirstOf resumed | `Tester` receives ("a","A") |
210
+ | `Tester` continues execution | q1=[], q2=["B"] | - | - | Remaining queue items untouched; correct order guaranteed |
211
+
212
+ [docs]: https://gvwilson.github.io/asimpy
213
+ [examples]: https://gvwilson.github.io/asimpy/examples/
214
+ [package]: https://pypi.org/project/asimpy/
215
+ [repo]: https://github.com/gvwilson/asimpy
216
+ [simpy]: https://simpy.readthedocs.io/
@@ -284,14 +284,41 @@
284
284
 
285
285
 
286
286
  <li class="md-nav__item">
287
- <a href="/actions/" class="md-nav__link">
287
+ <a href="/allof/" class="md-nav__link">
288
288
 
289
289
 
290
290
 
291
291
  <span class="md-ellipsis">
292
292
 
293
293
 
294
- Actions
294
+ Allof
295
+
296
+
297
+
298
+ </span>
299
+
300
+
301
+
302
+ </a>
303
+ </li>
304
+
305
+
306
+
307
+
308
+
309
+
310
+
311
+
312
+
313
+ <li class="md-nav__item">
314
+ <a href="/barrier/" class="md-nav__link">
315
+
316
+
317
+
318
+ <span class="md-ellipsis">
319
+
320
+
321
+ Barrier
295
322
 
296
323
 
297
324
 
@@ -338,14 +365,14 @@
338
365
 
339
366
 
340
367
  <li class="md-nav__item">
341
- <a href="/gate/" class="md-nav__link">
368
+ <a href="/firstof/" class="md-nav__link">
342
369
 
343
370
 
344
371
 
345
372
  <span class="md-ellipsis">
346
373
 
347
374
 
348
- Gate
375
+ Firstof
349
376
 
350
377
 
351
378
 
@@ -418,6 +445,33 @@
418
445
 
419
446
 
420
447
 
448
+ <li class="md-nav__item">
449
+ <a href="/queue/" class="md-nav__link">
450
+
451
+
452
+
453
+ <span class="md-ellipsis">
454
+
455
+
456
+ Queue
457
+
458
+
459
+
460
+ </span>
461
+
462
+
463
+
464
+ </a>
465
+ </li>
466
+
467
+
468
+
469
+
470
+
471
+
472
+
473
+
474
+
421
475
  <li class="md-nav__item">
422
476
  <a href="/resource/" class="md-nav__link">
423
477
 
@@ -439,6 +493,33 @@
439
493
 
440
494
 
441
495
 
496
+
497
+
498
+
499
+
500
+
501
+
502
+ <li class="md-nav__item">
503
+ <a href="/timeout/" class="md-nav__link">
504
+
505
+
506
+
507
+ <span class="md-ellipsis">
508
+
509
+
510
+ Timeout
511
+
512
+
513
+
514
+ </span>
515
+
516
+
517
+
518
+ </a>
519
+ </li>
520
+
521
+
522
+
442
523
  </ul>
443
524
  </nav>
444
525
  </div>
@@ -395,14 +395,41 @@
395
395
 
396
396
 
397
397
  <li class="md-nav__item">
398
- <a href="../actions/" class="md-nav__link">
398
+ <a href="../allof/" class="md-nav__link">
399
399
 
400
400
 
401
401
 
402
402
  <span class="md-ellipsis">
403
403
 
404
404
 
405
- Actions
405
+ Allof
406
+
407
+
408
+
409
+ </span>
410
+
411
+
412
+
413
+ </a>
414
+ </li>
415
+
416
+
417
+
418
+
419
+
420
+
421
+
422
+
423
+
424
+ <li class="md-nav__item">
425
+ <a href="../barrier/" class="md-nav__link">
426
+
427
+
428
+
429
+ <span class="md-ellipsis">
430
+
431
+
432
+ Barrier
406
433
 
407
434
 
408
435
 
@@ -449,14 +476,14 @@
449
476
 
450
477
 
451
478
  <li class="md-nav__item">
452
- <a href="../gate/" class="md-nav__link">
479
+ <a href="../firstof/" class="md-nav__link">
453
480
 
454
481
 
455
482
 
456
483
  <span class="md-ellipsis">
457
484
 
458
485
 
459
- Gate
486
+ Firstof
460
487
 
461
488
 
462
489
 
@@ -529,6 +556,33 @@
529
556
 
530
557
 
531
558
 
559
+ <li class="md-nav__item">
560
+ <a href="../queue/" class="md-nav__link">
561
+
562
+
563
+
564
+ <span class="md-ellipsis">
565
+
566
+
567
+ Queue
568
+
569
+
570
+
571
+ </span>
572
+
573
+
574
+
575
+ </a>
576
+ </li>
577
+
578
+
579
+
580
+
581
+
582
+
583
+
584
+
585
+
532
586
  <li class="md-nav__item">
533
587
  <a href="../resource/" class="md-nav__link">
534
588
 
@@ -550,6 +604,33 @@
550
604
 
551
605
 
552
606
 
607
+
608
+
609
+
610
+
611
+
612
+
613
+ <li class="md-nav__item">
614
+ <a href="../timeout/" class="md-nav__link">
615
+
616
+
617
+
618
+ <span class="md-ellipsis">
619
+
620
+
621
+ Timeout
622
+
623
+
624
+
625
+ </span>
626
+
627
+
628
+
629
+ </a>
630
+ </li>
631
+
632
+
633
+
553
634
  </ul>
554
635
  </nav>
555
636
  </div>