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.
- asimpy-0.3.0/PKG-INFO +241 -0
- asimpy-0.3.0/README.md +216 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/404.html +85 -4
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/CODE_OF_CONDUCT/index.html +85 -4
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/CONTRIBUTING/index.html +85 -4
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/LICENSE/index.html +85 -4
- asimpy-0.3.0/docs/allof/index.html +1024 -0
- {asimpy-0.2.0/docs/gate → asimpy-0.3.0/docs/barrier}/index.html +189 -92
- {asimpy-0.2.0/docs → asimpy-0.3.0/docs/environment}/index.html +219 -46
- asimpy-0.3.0/docs/event.py +1 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/examples/index.html +296 -79
- asimpy-0.3.0/docs/firstof/index.html +1034 -0
- asimpy-0.3.0/docs/index.html +1134 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/interrupt/index.html +97 -84
- asimpy-0.3.0/docs/objects.inv +6 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/process/index.html +414 -90
- asimpy-0.3.0/docs/queue/index.html +1373 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/resource/index.html +195 -148
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/sitemap.xml.gz +0 -0
- {asimpy-0.2.0/docs/actions → asimpy-0.3.0/docs/timeout}/index.html +208 -164
- asimpy-0.3.0/examples/allof.py +13 -0
- asimpy-0.3.0/examples/barrier.py +37 -0
- asimpy-0.3.0/examples/firstof_queue.py +32 -0
- asimpy-0.3.0/examples/firstof_timeout.py +18 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/examples/interrupt.py +10 -10
- {asimpy-0.2.0 → asimpy-0.3.0}/examples/priqueue.py +2 -2
- {asimpy-0.2.0 → asimpy-0.3.0}/examples/queue.py +5 -5
- {asimpy-0.2.0 → asimpy-0.3.0}/examples/resource.py +4 -4
- asimpy-0.2.0/examples/sleep.py → asimpy-0.3.0/examples/timeout.py +4 -4
- asimpy-0.3.0/output/allof.txt +2 -0
- asimpy-0.3.0/output/firstof_queue.txt +1 -0
- asimpy-0.3.0/output/firstof_timeout.txt +3 -0
- asimpy-0.3.0/output/interrupt.txt +14 -0
- asimpy-0.3.0/pages/allof.md +1 -0
- asimpy-0.3.0/pages/barrier.md +1 -0
- asimpy-0.3.0/pages/event.py +1 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/pages/examples.md +33 -6
- asimpy-0.3.0/pages/firstof.md +1 -0
- asimpy-0.3.0/pages/queue.md +1 -0
- asimpy-0.3.0/pages/timeout.md +1 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/pyproject.toml +21 -1
- {asimpy-0.2.0 → asimpy-0.3.0}/src/asimpy/__init__.py +5 -1
- asimpy-0.3.0/src/asimpy/_adapt.py +25 -0
- asimpy-0.3.0/src/asimpy/allof.py +50 -0
- asimpy-0.3.0/src/asimpy/barrier.py +33 -0
- asimpy-0.3.0/src/asimpy/environment.py +51 -0
- asimpy-0.3.0/src/asimpy/event.py +56 -0
- asimpy-0.3.0/src/asimpy/firstof.py +56 -0
- asimpy-0.3.0/src/asimpy/interrupt.py +20 -0
- asimpy-0.3.0/src/asimpy/process.py +92 -0
- asimpy-0.3.0/src/asimpy/queue.py +64 -0
- asimpy-0.3.0/src/asimpy/resource.py +69 -0
- asimpy-0.3.0/src/asimpy/timeout.py +23 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/uv.lock +79 -30
- asimpy-0.2.0/PKG-INFO +0 -41
- asimpy-0.2.0/README.md +0 -16
- asimpy-0.2.0/docs/environment/index.html +0 -1477
- asimpy-0.2.0/docs/objects.inv +0 -7
- asimpy-0.2.0/examples/gate.py +0 -37
- asimpy-0.2.0/output/interrupt.txt +0 -22
- asimpy-0.2.0/pages/actions.md +0 -1
- asimpy-0.2.0/pages/gate.md +0 -1
- asimpy-0.2.0/src/asimpy/actions.py +0 -26
- asimpy-0.2.0/src/asimpy/environment.py +0 -121
- asimpy-0.2.0/src/asimpy/gate.py +0 -57
- asimpy-0.2.0/src/asimpy/interrupt.py +0 -25
- asimpy-0.2.0/src/asimpy/process.py +0 -56
- asimpy-0.2.0/src/asimpy/queue.py +0 -147
- asimpy-0.2.0/src/asimpy/resource.py +0 -76
- {asimpy-0.2.0 → asimpy-0.3.0}/.gitignore +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/CODE_OF_CONDUCT.md +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/CONTRIBUTING.md +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/LICENSE.md +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/_mkdocstrings.css +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/images/favicon.png +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/bundle.79ae519e.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/bundle.79ae519e.min.js.map +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.ar.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.da.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.de.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.du.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.el.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.es.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.fi.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.fr.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.he.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.hi.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.hu.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.hy.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.it.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.ja.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.jp.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.kn.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.ko.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.multi.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.nl.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.no.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.pt.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.ro.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.ru.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.sa.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.sv.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.ta.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.te.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.th.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.tr.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.vi.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/min/lunr.zh.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/tinyseg.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/lunr/wordcut.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/workers/search.2c215733.min.js +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/javascripts/workers/search.2c215733.min.js.map +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/stylesheets/main.484c7ddc.min.css +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/stylesheets/main.484c7ddc.min.css.map +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/stylesheets/palette.ab4e12ef.min.css +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/docs/sitemap.xml +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/mkdocs.yml +0 -0
- asimpy-0.2.0/output/gate.txt → asimpy-0.3.0/output/barrier.txt +3 -3
- {asimpy-0.2.0 → asimpy-0.3.0}/output/priqueue.txt +1 -1
- {asimpy-0.2.0 → asimpy-0.3.0}/output/queue.txt +3 -3
- {asimpy-0.2.0 → asimpy-0.3.0}/output/resource.txt +0 -0
- /asimpy-0.2.0/output/sleep.txt → /asimpy-0.3.0/output/timeout.txt +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/pages/.pages +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/pages/CODE_OF_CONDUCT.md +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/pages/CONTRIBUTING.md +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/pages/LICENSE.md +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/pages/environment.md +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/pages/index.md +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/pages/interrupt.md +0 -0
- {asimpy-0.2.0 → asimpy-0.3.0}/pages/process.md +0 -0
- {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="/
|
|
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
|
-
|
|
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="/
|
|
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
|
-
|
|
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="../
|
|
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
|
-
|
|
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="../
|
|
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
|
-
|
|
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>
|