asimpy 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
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/__init__.py +5 -1
- asimpy/_adapt.py +25 -0
- asimpy/allof.py +50 -0
- asimpy/barrier.py +33 -0
- asimpy/environment.py +30 -100
- asimpy/event.py +56 -0
- asimpy/firstof.py +56 -0
- asimpy/interrupt.py +5 -10
- asimpy/process.py +62 -26
- asimpy/queue.py +37 -120
- asimpy/resource.py +39 -46
- asimpy/timeout.py +23 -0
- asimpy-0.3.0.dist-info/METADATA +241 -0
- asimpy-0.3.0.dist-info/RECORD +16 -0
- asimpy/actions.py +0 -26
- asimpy/gate.py +0 -57
- asimpy-0.2.0.dist-info/METADATA +0 -41
- asimpy-0.2.0.dist-info/RECORD +0 -12
- {asimpy-0.2.0.dist-info → asimpy-0.3.0.dist-info}/WHEEL +0 -0
- {asimpy-0.2.0.dist-info → asimpy-0.3.0.dist-info}/licenses/LICENSE.md +0 -0
asimpy/__init__.py
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"""Discrete event simulation using async/await."""
|
|
2
2
|
|
|
3
|
+
from .allof import AllOf as AllOf
|
|
4
|
+
from .barrier import Barrier as Barrier
|
|
3
5
|
from .environment import Environment as Environment
|
|
4
|
-
from .
|
|
6
|
+
from .event import Event as Event
|
|
7
|
+
from .firstof import FirstOf as FirstOf
|
|
5
8
|
from .interrupt import Interrupt as Interrupt
|
|
6
9
|
from .process import Process as Process
|
|
7
10
|
from .queue import Queue as Queue, PriorityQueue as PriorityQueue
|
|
8
11
|
from .resource import Resource as Resource
|
|
12
|
+
from .timeout import Timeout as Timeout
|
asimpy/_adapt.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from .event import Event
|
|
3
|
+
from .process import Process
|
|
4
|
+
|
|
5
|
+
def ensure_event(env, obj):
|
|
6
|
+
if isinstance(obj, Event):
|
|
7
|
+
return obj
|
|
8
|
+
|
|
9
|
+
if inspect.iscoroutine(obj):
|
|
10
|
+
evt = Event(env)
|
|
11
|
+
_Runner(env, evt, obj)
|
|
12
|
+
return evt
|
|
13
|
+
|
|
14
|
+
raise TypeError(f"Expected Event or coroutine, got {type(obj)}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _Runner(Process):
|
|
19
|
+
def init(self, evt, obj):
|
|
20
|
+
self.evt = evt
|
|
21
|
+
self.obj = obj
|
|
22
|
+
|
|
23
|
+
async def run(self):
|
|
24
|
+
result = await self.obj
|
|
25
|
+
self.evt.succeed(result)
|
asimpy/allof.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Wait for all events in a set to complete."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from .environment import Environment
|
|
5
|
+
from .event import Event
|
|
6
|
+
from ._adapt import ensure_event
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AllOf(Event):
|
|
10
|
+
"""Wait for all of a set of events."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, env: Environment, **events: Any):
|
|
13
|
+
"""
|
|
14
|
+
Construct new collective wait.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
env: simulation environment.
|
|
18
|
+
events: name=thing items to wait for.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
name, value = await AllOf(env, a=q1.get(), b=q2.get())
|
|
24
|
+
```
|
|
25
|
+
"""
|
|
26
|
+
assert len(events) > 0
|
|
27
|
+
super().__init__(env)
|
|
28
|
+
|
|
29
|
+
self._events = {}
|
|
30
|
+
self._results = {}
|
|
31
|
+
|
|
32
|
+
for key, obj in events.items():
|
|
33
|
+
evt = ensure_event(env, obj)
|
|
34
|
+
self._events[key] = evt
|
|
35
|
+
evt._add_waiter(_AllOfWatcher(self, key))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _child_done(self, key, value):
|
|
39
|
+
self._results[key] = value
|
|
40
|
+
if len(self._results) == len(self._events):
|
|
41
|
+
self.succeed(self._results)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _AllOfWatcher:
|
|
45
|
+
def __init__(self, parent, key):
|
|
46
|
+
self.parent = parent
|
|
47
|
+
self.key = key
|
|
48
|
+
|
|
49
|
+
def _resume(self, value):
|
|
50
|
+
self.parent._child_done(self.key, value)
|
asimpy/barrier.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Barrier that holds multiple processes until released."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from .event import Event
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .environment import Environment
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Barrier:
|
|
11
|
+
"""Barrier to hold multiple processes."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, env: "Environment"):
|
|
14
|
+
"""
|
|
15
|
+
Construct barrier.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
env: simulation environment.
|
|
19
|
+
"""
|
|
20
|
+
self._env = env
|
|
21
|
+
self._waiters = []
|
|
22
|
+
|
|
23
|
+
async def wait(self):
|
|
24
|
+
"""Wait until barrier released."""
|
|
25
|
+
evt = Event(self._env)
|
|
26
|
+
self._waiters.append(evt)
|
|
27
|
+
await evt
|
|
28
|
+
|
|
29
|
+
async def release(self):
|
|
30
|
+
"""Release processes waiting at barrier."""
|
|
31
|
+
for evt in self._waiters:
|
|
32
|
+
evt.succeed()
|
|
33
|
+
self._waiters.clear()
|
asimpy/environment.py
CHANGED
|
@@ -1,121 +1,51 @@
|
|
|
1
1
|
"""Simulation environment."""
|
|
2
2
|
|
|
3
|
-
from dataclasses import dataclass
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
4
|
import heapq
|
|
5
|
-
|
|
6
|
-
from
|
|
5
|
+
import itertools
|
|
6
|
+
from typing import Callable
|
|
7
7
|
|
|
8
|
+
from .timeout import Timeout
|
|
8
9
|
|
|
9
|
-
class Environment:
|
|
10
|
-
"""Simulation environment."""
|
|
11
|
-
|
|
12
|
-
def __init__(self, logging: bool = False):
|
|
13
|
-
"""
|
|
14
|
-
Construct a new simulation environment.
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
self.now = 0
|
|
20
|
-
self._queue = []
|
|
11
|
+
class Environment:
|
|
12
|
+
def __init__(self, logging=False):
|
|
13
|
+
self._now = 0
|
|
21
14
|
self._logging = logging
|
|
15
|
+
self._pending = []
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
Args:
|
|
28
|
-
proc: `Process`-derived object to schedule and run.
|
|
29
|
-
"""
|
|
30
|
-
self.schedule(self.now, proc)
|
|
31
|
-
|
|
32
|
-
def schedule(self, time: float | int, proc: Process):
|
|
33
|
-
"""
|
|
34
|
-
Schedule a process to run at a specified time (*not* after a delay).
|
|
35
|
-
|
|
36
|
-
Args:
|
|
37
|
-
time: when the process should be scheduled to run.
|
|
38
|
-
proc: `Process`-derived object to run.
|
|
39
|
-
"""
|
|
40
|
-
heapq.heappush(self._queue, _Pending(time, proc))
|
|
17
|
+
@property
|
|
18
|
+
def now(self):
|
|
19
|
+
return self._now
|
|
41
20
|
|
|
42
|
-
def
|
|
43
|
-
|
|
44
|
-
Suspend the caller for a specified length of time.
|
|
21
|
+
def schedule(self, time, callback):
|
|
22
|
+
heapq.heappush(self._pending, _Pending(time, callback))
|
|
45
23
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Returns: awaitable representing delay.
|
|
50
|
-
"""
|
|
51
|
-
return _Sleep(self, delay)
|
|
52
|
-
|
|
53
|
-
def run(self, until: float | int | None = None):
|
|
54
|
-
"""
|
|
55
|
-
Run the whole simulation.
|
|
56
|
-
|
|
57
|
-
Args:
|
|
58
|
-
until: when to stop (run forever if not provided).
|
|
59
|
-
"""
|
|
60
|
-
while self._queue:
|
|
61
|
-
if self._logging:
|
|
62
|
-
print(self)
|
|
63
|
-
|
|
64
|
-
pending = heapq.heappop(self._queue)
|
|
24
|
+
def timeout(self, delay):
|
|
25
|
+
return Timeout(self, delay)
|
|
65
26
|
|
|
27
|
+
def run(self, until=None):
|
|
28
|
+
while self._pending:
|
|
29
|
+
pending = heapq.heappop(self._pending)
|
|
66
30
|
if until is not None and pending.time > until:
|
|
67
31
|
break
|
|
32
|
+
self._now = pending.time
|
|
33
|
+
pending.callback()
|
|
68
34
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
try:
|
|
73
|
-
if proc._interrupt is None:
|
|
74
|
-
awaited = proc._coro.send(None)
|
|
75
|
-
else:
|
|
76
|
-
exc, proc._interrupt = proc._interrupt, None
|
|
77
|
-
awaited = proc._coro.throw(exc)
|
|
35
|
+
def _immediate(self, callback):
|
|
36
|
+
self.schedule(self._now, callback)
|
|
78
37
|
|
|
79
|
-
|
|
38
|
+
def __str__(self):
|
|
39
|
+
return f"Env(t={self._now})"
|
|
80
40
|
|
|
81
|
-
except StopIteration:
|
|
82
|
-
continue
|
|
83
41
|
|
|
84
|
-
|
|
85
|
-
"""
|
|
86
|
-
Format environment as printable string.
|
|
87
|
-
|
|
88
|
-
Returns: string representation of environment time and queue.
|
|
89
|
-
"""
|
|
90
|
-
return f"Env(t={self.now}, {' | '.join(str(p) for p in self._queue)})"
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
# ----------------------------------------------------------------------
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
@dataclass
|
|
42
|
+
@dataclass(order=True)
|
|
97
43
|
class _Pending:
|
|
98
|
-
|
|
44
|
+
_counter = itertools.count()
|
|
99
45
|
|
|
100
46
|
time: float
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def __lt__(self, other: "_Pending") -> bool:
|
|
104
|
-
return self.time < other.time
|
|
105
|
-
|
|
106
|
-
def __str__(self) -> str:
|
|
107
|
-
return f"Pend({self.time}, {self.proc})"
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
class _Sleep(BaseAction):
|
|
111
|
-
"""Wait for a specified simulated time."""
|
|
112
|
-
|
|
113
|
-
def __init__(self, env: Environment, delay: float | int):
|
|
114
|
-
super().__init__(env)
|
|
115
|
-
self._delay = delay
|
|
116
|
-
|
|
117
|
-
def act(self, proc: Process):
|
|
118
|
-
self._env.schedule(self._env.now + self._delay, proc)
|
|
47
|
+
serial: int = field(init=False, repr=False, compare=True)
|
|
48
|
+
callback: Callable = field(compare=False)
|
|
119
49
|
|
|
120
|
-
def
|
|
121
|
-
|
|
50
|
+
def __post_init__(self):
|
|
51
|
+
self.serial = next(self._counter)
|
asimpy/event.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Events."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from .environment import Environment
|
|
6
|
+
|
|
7
|
+
class Event:
|
|
8
|
+
"""Manage an event."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, env: "Environment"):
|
|
11
|
+
"""
|
|
12
|
+
Construct a new event.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
env: simulation environment.
|
|
16
|
+
"""
|
|
17
|
+
self._env = env
|
|
18
|
+
self._triggered = False
|
|
19
|
+
self._cancelled = False
|
|
20
|
+
self._value = None
|
|
21
|
+
self._waiters = []
|
|
22
|
+
self._on_cancel = None
|
|
23
|
+
|
|
24
|
+
def succeed(self, value: Any = None):
|
|
25
|
+
"""
|
|
26
|
+
Handle case of event succeeding.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
value: value associated with event.
|
|
30
|
+
"""
|
|
31
|
+
if self._triggered or self._cancelled:
|
|
32
|
+
return
|
|
33
|
+
self._triggered = True
|
|
34
|
+
self._value = value
|
|
35
|
+
for proc in self._waiters:
|
|
36
|
+
proc._resume(value)
|
|
37
|
+
self._waiters.clear()
|
|
38
|
+
|
|
39
|
+
def cancel(self):
|
|
40
|
+
"""Cancel event."""
|
|
41
|
+
if self._triggered or self._cancelled:
|
|
42
|
+
return
|
|
43
|
+
self._cancelled = True
|
|
44
|
+
self._waiters.clear()
|
|
45
|
+
if self._on_cancel:
|
|
46
|
+
self._on_cancel()
|
|
47
|
+
|
|
48
|
+
def _add_waiter(self, proc):
|
|
49
|
+
if self._triggered:
|
|
50
|
+
proc._resume(self._value)
|
|
51
|
+
elif not self._cancelled:
|
|
52
|
+
self._waiters.append(proc)
|
|
53
|
+
|
|
54
|
+
def __await__(self):
|
|
55
|
+
value = yield self
|
|
56
|
+
return value
|
asimpy/firstof.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Wait for the first of a set of events."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from .environment import Environment
|
|
5
|
+
from .event import Event
|
|
6
|
+
from ._adapt import ensure_event
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FirstOf(Event):
|
|
10
|
+
"""Wait for the first of a set of events."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, env: Environment, **events: Any):
|
|
13
|
+
"""
|
|
14
|
+
Construct new collective wait.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
env: simulation environment.
|
|
18
|
+
events: name=thing items to wait for.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
name, value = await FirstOf(env, a=q1.get(), b=q2.get())
|
|
24
|
+
```
|
|
25
|
+
"""
|
|
26
|
+
assert len(events) > 0
|
|
27
|
+
super().__init__(env)
|
|
28
|
+
|
|
29
|
+
self._done = False
|
|
30
|
+
self._events = {}
|
|
31
|
+
|
|
32
|
+
for key, obj in events.items():
|
|
33
|
+
evt = ensure_event(env, obj)
|
|
34
|
+
self._events[key] = evt
|
|
35
|
+
evt._add_waiter(_FirstOfWatcher(self, key, evt))
|
|
36
|
+
|
|
37
|
+
def _child_done(self, key, value, winner):
|
|
38
|
+
if self._done:
|
|
39
|
+
return
|
|
40
|
+
self._done = True
|
|
41
|
+
|
|
42
|
+
for evt in self._events.values():
|
|
43
|
+
if evt is not winner:
|
|
44
|
+
evt.cancel()
|
|
45
|
+
|
|
46
|
+
self.succeed((key, value))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class _FirstOfWatcher:
|
|
50
|
+
def __init__(self, parent, key, evt):
|
|
51
|
+
self.parent = parent
|
|
52
|
+
self.key = key
|
|
53
|
+
self.evt = evt
|
|
54
|
+
|
|
55
|
+
def _resume(self, value):
|
|
56
|
+
self.parent._child_done(self.key, value, self.evt)
|
asimpy/interrupt.py
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Interrupt exceptions."""
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class Interrupt(Exception):
|
|
7
|
-
"""
|
|
7
|
+
"""Interrupt raised inside a process."""
|
|
8
8
|
|
|
9
9
|
def __init__(self, cause: Any):
|
|
10
10
|
"""
|
|
11
|
-
Construct
|
|
11
|
+
Construct interruption exception.
|
|
12
12
|
|
|
13
13
|
Args:
|
|
14
|
-
cause: reason for
|
|
14
|
+
cause: reason for interrupt.
|
|
15
15
|
"""
|
|
16
16
|
super().__init__()
|
|
17
17
|
self.cause = cause
|
|
18
18
|
|
|
19
|
-
def __str__(self)
|
|
20
|
-
"""
|
|
21
|
-
Format interruption as printable string.
|
|
22
|
-
|
|
23
|
-
Returns: string representation of interruption and cause.
|
|
24
|
-
"""
|
|
19
|
+
def __str__(self):
|
|
25
20
|
return f"Interrupt({self.cause})"
|
asimpy/process.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
"""Base class for
|
|
1
|
+
"""Base class for active process."""
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
6
|
from .interrupt import Interrupt
|
|
7
7
|
|
|
@@ -10,47 +10,83 @@ if TYPE_CHECKING:
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class Process(ABC):
|
|
13
|
-
"""
|
|
13
|
+
"""Abstract base class for active process."""
|
|
14
14
|
|
|
15
|
-
def __init__(self, env: "Environment", *args: Any):
|
|
15
|
+
def __init__(self, env: "Environment", *args: Any, **kwargs: Any):
|
|
16
16
|
"""
|
|
17
|
-
Construct
|
|
18
|
-
calling the user-defined `init()` method (no underscores),
|
|
19
|
-
and registering the coroutine created by the `run()` method
|
|
20
|
-
with the environment.
|
|
17
|
+
Construct new process.
|
|
21
18
|
|
|
22
19
|
Args:
|
|
23
20
|
env: simulation environment.
|
|
24
|
-
|
|
21
|
+
args: extra constructor arguments passed to `init()`.
|
|
25
22
|
"""
|
|
26
|
-
self.
|
|
23
|
+
self._env = env
|
|
24
|
+
self._done = False
|
|
27
25
|
self._interrupt = None
|
|
28
|
-
|
|
29
|
-
self.init(*args)
|
|
30
|
-
|
|
26
|
+
self.init(*args, **kwargs)
|
|
31
27
|
self._coro = self.run()
|
|
32
|
-
self.
|
|
28
|
+
self._env._immediate(self._loop)
|
|
33
29
|
|
|
34
|
-
def init(self, *args: Any, **kwargs: Any)
|
|
30
|
+
def init(self, *args: Any, **kwargs: Any):
|
|
35
31
|
"""
|
|
36
|
-
|
|
32
|
+
Extra construction after generic setup but before coroutine created.
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
Args:
|
|
35
|
+
args: extra constructor arguments passed to `init()`.
|
|
36
|
+
kwargs: extra construct arguments passed to `init()`.
|
|
40
37
|
"""
|
|
41
38
|
pass
|
|
42
39
|
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def run(self):
|
|
42
|
+
"""Implementation of process behavior."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def now(self):
|
|
47
|
+
"""Shortcut to access simulation time."""
|
|
48
|
+
return self._env.now
|
|
49
|
+
|
|
50
|
+
def timeout(self, delay: int | float):
|
|
51
|
+
"""
|
|
52
|
+
Delay this process for a specified time.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
delay: how long to wait.
|
|
56
|
+
"""
|
|
57
|
+
return self._env.timeout(delay)
|
|
58
|
+
|
|
43
59
|
def interrupt(self, cause: Any):
|
|
44
60
|
"""
|
|
45
|
-
Interrupt this process
|
|
46
|
-
next time the process is scheduled to run.
|
|
61
|
+
Interrupt this process
|
|
47
62
|
|
|
48
63
|
Args:
|
|
49
|
-
cause: reason for interrupt
|
|
64
|
+
cause: reason for interrupt.
|
|
50
65
|
"""
|
|
51
|
-
self.
|
|
66
|
+
if not self._done:
|
|
67
|
+
self._interrupt = Interrupt(cause)
|
|
68
|
+
self._env._immediate(self._loop)
|
|
52
69
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
70
|
+
def _loop(self, value=None):
|
|
71
|
+
if self._done:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
if self._interrupt is None:
|
|
76
|
+
yielded = self._coro.send(value)
|
|
77
|
+
else:
|
|
78
|
+
exc = self._interrupt
|
|
79
|
+
self._interrupt = None
|
|
80
|
+
yielded = self._coro.throw(exc)
|
|
81
|
+
yielded._add_waiter(self)
|
|
82
|
+
|
|
83
|
+
except StopIteration:
|
|
84
|
+
self._done = True
|
|
85
|
+
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
self._done = True
|
|
88
|
+
raise exc
|
|
89
|
+
|
|
90
|
+
def _resume(self, value=None):
|
|
91
|
+
if not self._done:
|
|
92
|
+
self._env._immediate(lambda: self._loop(value))
|
asimpy/queue.py
CHANGED
|
@@ -1,147 +1,64 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""FIFO and priority queues."""
|
|
2
2
|
|
|
3
|
-
from abc import ABC, abstractmethod
|
|
4
3
|
import heapq
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
6
|
-
from .
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
from .event import Event
|
|
7
6
|
|
|
8
7
|
if TYPE_CHECKING:
|
|
9
8
|
from .environment import Environment
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"""Base class for all queues."""
|
|
10
|
+
class Queue:
|
|
11
|
+
"""FIFO queue."""
|
|
14
12
|
|
|
15
13
|
def __init__(self, env: "Environment"):
|
|
16
14
|
"""
|
|
17
|
-
|
|
15
|
+
Construct queue.
|
|
18
16
|
|
|
19
17
|
Args:
|
|
20
18
|
env: simulation environment.
|
|
21
19
|
"""
|
|
22
20
|
self._env = env
|
|
23
|
-
self.
|
|
21
|
+
self._items = []
|
|
22
|
+
self._getters = []
|
|
24
23
|
|
|
25
24
|
async def get(self):
|
|
26
|
-
"""Get
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def
|
|
39
|
-
pass
|
|
40
|
-
|
|
41
|
-
@abstractmethod
|
|
42
|
-
def _enqueue(self, obj):
|
|
43
|
-
pass
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# ----------------------------------------------------------------------
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class Queue(BaseQueue):
|
|
50
|
-
"""FIFO queue."""
|
|
51
|
-
|
|
52
|
-
def __init__(self, env):
|
|
25
|
+
"""Get one item from the queue."""
|
|
26
|
+
if self._items:
|
|
27
|
+
item = self._items.pop(0)
|
|
28
|
+
evt = Event(self._env)
|
|
29
|
+
self._env._immediate(lambda: evt.succeed(item))
|
|
30
|
+
evt._on_cancel = lambda: self._items.insert(0, item)
|
|
31
|
+
return await evt
|
|
32
|
+
|
|
33
|
+
evt = Event(self._env)
|
|
34
|
+
self._getters.append(evt)
|
|
35
|
+
return await evt
|
|
36
|
+
|
|
37
|
+
async def put(self, item: Any):
|
|
53
38
|
"""
|
|
54
|
-
|
|
39
|
+
Add one item to the queue.
|
|
55
40
|
|
|
56
41
|
Args:
|
|
57
|
-
|
|
42
|
+
item: to add to the queue.
|
|
58
43
|
"""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return self._items.pop(0)
|
|
65
|
-
|
|
66
|
-
def _enqueue(self, obj):
|
|
67
|
-
self._items.append(obj)
|
|
68
|
-
|
|
69
|
-
def _empty(self):
|
|
70
|
-
return len(self._items) == 0
|
|
71
|
-
|
|
72
|
-
def __str__(self):
|
|
73
|
-
return f"Queue({', '.join(str(i) for i in self._items)})"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
# ----------------------------------------------------------------------
|
|
44
|
+
if self._getters:
|
|
45
|
+
evt = self._getters.pop(0)
|
|
46
|
+
evt.succeed(item)
|
|
47
|
+
else:
|
|
48
|
+
self._items.append(item)
|
|
77
49
|
|
|
78
50
|
|
|
79
|
-
class PriorityQueue(
|
|
80
|
-
"""
|
|
51
|
+
class PriorityQueue(Queue):
|
|
52
|
+
"""Ordered queue."""
|
|
81
53
|
|
|
82
|
-
def
|
|
54
|
+
async def put(self, item: Any):
|
|
83
55
|
"""
|
|
84
|
-
|
|
56
|
+
Add one item to the queue.
|
|
85
57
|
|
|
86
58
|
Args:
|
|
87
|
-
|
|
59
|
+
item: comparable item to add to queue.
|
|
88
60
|
"""
|
|
89
|
-
|
|
90
|
-
self.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
assert len(self._items) > 0
|
|
94
|
-
return heapq.heappop(self._items)
|
|
95
|
-
|
|
96
|
-
def _enqueue(self, obj):
|
|
97
|
-
heapq.heappush(self._items, obj)
|
|
98
|
-
|
|
99
|
-
def _empty(self):
|
|
100
|
-
return len(self._items) == 0
|
|
101
|
-
|
|
102
|
-
def __str__(self):
|
|
103
|
-
return f"PriorityQueue({', '.join(str(i) for i in self._items)})"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# ----------------------------------------------------------------------
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
class _Get(BaseAction):
|
|
110
|
-
"""Get an item from the queue."""
|
|
111
|
-
|
|
112
|
-
def __init__(self, queue):
|
|
113
|
-
super().__init__(queue._env)
|
|
114
|
-
self._queue = queue
|
|
115
|
-
self._proc = None
|
|
116
|
-
self._item = None
|
|
117
|
-
|
|
118
|
-
def act(self, proc):
|
|
119
|
-
if self._queue._empty():
|
|
120
|
-
self._proc = proc
|
|
121
|
-
self._queue._gets.append(self)
|
|
122
|
-
else:
|
|
123
|
-
self._item = self._queue._dequeue()
|
|
124
|
-
self._env.schedule(self._env.now, proc)
|
|
125
|
-
|
|
126
|
-
def __await__(self):
|
|
127
|
-
yield self
|
|
128
|
-
return self._item
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
class _Put(BaseAction):
|
|
132
|
-
"""Put an item in a queue."""
|
|
133
|
-
|
|
134
|
-
def __init__(self, queue, obj):
|
|
135
|
-
super().__init__(queue._env)
|
|
136
|
-
self._queue = queue
|
|
137
|
-
self.obj = obj
|
|
138
|
-
|
|
139
|
-
def act(self, proc):
|
|
140
|
-
self._queue._enqueue(self.obj)
|
|
141
|
-
|
|
142
|
-
if len(self._queue._gets) > 0:
|
|
143
|
-
waiting_get = self._queue._gets.pop(0)
|
|
144
|
-
waiting_get._item = self._queue._dequeue()
|
|
145
|
-
self._env.schedule(self._env.now, waiting_get._proc)
|
|
146
|
-
|
|
147
|
-
self._env.schedule(self._env.now, proc)
|
|
61
|
+
heapq.heappush(self._items, item)
|
|
62
|
+
if self._getters:
|
|
63
|
+
evt = self._getters.pop(0)
|
|
64
|
+
evt.succeed(heapq.heappop(self._items))
|
asimpy/resource.py
CHANGED
|
@@ -1,76 +1,69 @@
|
|
|
1
1
|
"""Shared resource with limited capacity."""
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
|
-
from .
|
|
5
|
-
from .process import Process
|
|
4
|
+
from .event import Event
|
|
6
5
|
|
|
7
6
|
if TYPE_CHECKING:
|
|
8
7
|
from .environment import Environment
|
|
9
8
|
|
|
10
|
-
|
|
11
9
|
class Resource:
|
|
12
|
-
"""
|
|
10
|
+
"""A shared resource with limited capacity."""
|
|
13
11
|
|
|
14
12
|
def __init__(self, env: "Environment", capacity: int = 1):
|
|
15
13
|
"""
|
|
16
|
-
|
|
14
|
+
Construct resource.
|
|
17
15
|
|
|
18
16
|
Args:
|
|
19
17
|
env: simulation environment.
|
|
20
|
-
capacity: maximum
|
|
18
|
+
capacity: maximum capacity.
|
|
21
19
|
"""
|
|
20
|
+
assert capacity > 0
|
|
22
21
|
self._env = env
|
|
23
|
-
self.
|
|
22
|
+
self.capacity = capacity
|
|
24
23
|
self._count = 0
|
|
25
|
-
self.
|
|
24
|
+
self._waiters = []
|
|
26
25
|
|
|
27
26
|
async def acquire(self):
|
|
28
|
-
"""Acquire one unit of
|
|
29
|
-
|
|
27
|
+
"""Acquire one unit of resource."""
|
|
28
|
+
if self._count < self.capacity:
|
|
29
|
+
await self._acquire_available()
|
|
30
|
+
else:
|
|
31
|
+
await self._acquire_unavailable()
|
|
30
32
|
|
|
31
33
|
async def release(self):
|
|
32
|
-
"""Release one unit of
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
await self.acquire()
|
|
38
|
-
return self
|
|
39
|
-
|
|
40
|
-
async def __aexit__(self, exc_type, exc, tb):
|
|
41
|
-
"""Release one unit of the resource acquired with `async with`."""
|
|
42
|
-
await self.release()
|
|
43
|
-
|
|
34
|
+
"""Release one unit of resource."""
|
|
35
|
+
self._count -= 1
|
|
36
|
+
if self._waiters:
|
|
37
|
+
evt = self._waiters.pop(0)
|
|
38
|
+
evt.succeed()
|
|
44
39
|
|
|
45
|
-
|
|
40
|
+
async def _acquire_available(self):
|
|
41
|
+
self._count += 1
|
|
42
|
+
evt = Event(self._env)
|
|
46
43
|
|
|
44
|
+
def cancel():
|
|
45
|
+
self._count -= 1
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
evt._on_cancel = cancel
|
|
48
|
+
self._env._immediate(evt.succeed)
|
|
49
|
+
await evt
|
|
50
50
|
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
self.
|
|
51
|
+
async def _acquire_unavailable(self):
|
|
52
|
+
evt = Event(self._env)
|
|
53
|
+
self._waiters.append(evt)
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
self._env.schedule(self._env.now, proc)
|
|
59
|
-
else:
|
|
60
|
-
self._resource._waiting.append(proc)
|
|
55
|
+
def cancel():
|
|
56
|
+
if evt in self._waiters:
|
|
57
|
+
self._waiters.remove(evt)
|
|
61
58
|
|
|
59
|
+
evt._on_cancel = cancel
|
|
60
|
+
await evt
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
"""Release a resource."""
|
|
62
|
+
self._count += 1
|
|
65
63
|
|
|
66
|
-
def
|
|
67
|
-
|
|
68
|
-
self
|
|
64
|
+
async def __aenter__(self):
|
|
65
|
+
await self.acquire()
|
|
66
|
+
return self
|
|
69
67
|
|
|
70
|
-
def
|
|
71
|
-
self.
|
|
72
|
-
if self._resource._waiting:
|
|
73
|
-
next_proc = self._resource._waiting.pop(0)
|
|
74
|
-
self._resource._count += 1
|
|
75
|
-
self._env.schedule(self._env.now, next_proc)
|
|
76
|
-
self._env.schedule(self._env.now, proc)
|
|
68
|
+
async def __aexit__(self, exc_type, exc, tb):
|
|
69
|
+
await self.release()
|
asimpy/timeout.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Wait for a simulated time to pass."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from .event import Event
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .environment import Environment
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Timeout(Event):
|
|
11
|
+
"""Timeout event for sleeping."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, env: "Environment", delay: float | int):
|
|
14
|
+
"""
|
|
15
|
+
Construct timeout.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
env: simulation environment.
|
|
19
|
+
delay: how long to wait.
|
|
20
|
+
"""
|
|
21
|
+
assert delay >= 0
|
|
22
|
+
super().__init__(env)
|
|
23
|
+
env.schedule(env.now + delay, lambda: self.succeed())
|
|
@@ -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/
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
asimpy/__init__.py,sha256=zJx5CIZDInFYyjXT99QpYQvp1x5aQluob9amrnmPJkU,487
|
|
2
|
+
asimpy/_adapt.py,sha256=GHiDp8wmMrr3XWfGRDM1rrFJ2KR7_7F8L0xB4g6pe5U,521
|
|
3
|
+
asimpy/allof.py,sha256=8UpFe_-hmtm2brt-SHk1RgBv55agcef5AathH11Ax2E,1216
|
|
4
|
+
asimpy/barrier.py,sha256=evubm6H_93qi9vBogqrNLg1dZosY7YdnCnJc1jNGYW4,770
|
|
5
|
+
asimpy/environment.py,sha256=ndjPR7W-j7FPwV7ze5Icen-xRgHKYGLNl5M2m26d4Fw,1205
|
|
6
|
+
asimpy/event.py,sha256=ACYPPRGyaRgsSNB7KADgMu3PnQYLFLs7OOh70lHsEb4,1358
|
|
7
|
+
asimpy/firstof.py,sha256=htwskm_5lwBVD6yb7hD8jmAypeaJJk5iUqB7cKPYWyk,1356
|
|
8
|
+
asimpy/interrupt.py,sha256=tybPzsCeX7cpVL6psOUQf6egcAujV0vnJe1zDwkZWxo,406
|
|
9
|
+
asimpy/process.py,sha256=IQldzjqF5CuBUccdkM3ZV5mEtEIivZx8whGeNEr2jIc,2349
|
|
10
|
+
asimpy/queue.py,sha256=BFGDIle1olAbOSGc2umtl6DxVCl59twb4s658_RTChc,1514
|
|
11
|
+
asimpy/resource.py,sha256=ENDY37S2vQ_3iIISq9rPCdnVP-bLW7qCX9jJZ8RlX5Q,1679
|
|
12
|
+
asimpy/timeout.py,sha256=yRKJvKPITB75u25ZnE3-bk3yMC39w4bPnh9yZ_pNSes,549
|
|
13
|
+
asimpy-0.3.0.dist-info/METADATA,sha256=DB3RniluV4SAMgykUwUNh7TDeZRqSXLGpcdJBr3KZAs,9315
|
|
14
|
+
asimpy-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
15
|
+
asimpy-0.3.0.dist-info/licenses/LICENSE.md,sha256=IjTDUvBk8xdl_n50CG1Vtk4FYdrS-C3uEYrRWAoOQqQ,1066
|
|
16
|
+
asimpy-0.3.0.dist-info/RECORD,,
|
asimpy/actions.py
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
"""Awaitable actions."""
|
|
2
|
-
|
|
3
|
-
from typing import Any, TYPE_CHECKING
|
|
4
|
-
|
|
5
|
-
if TYPE_CHECKING:
|
|
6
|
-
from .environment import Environment
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class BaseAction:
|
|
10
|
-
"""
|
|
11
|
-
Base of all internal awaitable actions. Simulation authors should not use this directly.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
def __init__(self, env: "Environment"):
|
|
15
|
-
"""
|
|
16
|
-
Construct a new awaitable action.
|
|
17
|
-
|
|
18
|
-
Args:
|
|
19
|
-
env: simulation environment.
|
|
20
|
-
"""
|
|
21
|
-
self._env = env
|
|
22
|
-
|
|
23
|
-
def __await__(self) -> Any:
|
|
24
|
-
"""Handle `await`."""
|
|
25
|
-
yield self
|
|
26
|
-
return None
|
asimpy/gate.py
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
"""Gate that holds multiple processes until flagged."""
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
4
|
-
from .actions import BaseAction
|
|
5
|
-
from .process import Process
|
|
6
|
-
|
|
7
|
-
if TYPE_CHECKING:
|
|
8
|
-
from .environment import Environment
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class Gate:
|
|
12
|
-
"""Gate that multiple processes can wait on for simultaneous release."""
|
|
13
|
-
|
|
14
|
-
def __init__(self, env: "Environment"):
|
|
15
|
-
"""
|
|
16
|
-
Construct a new gate.
|
|
17
|
-
|
|
18
|
-
Args:
|
|
19
|
-
env: simulation environment.
|
|
20
|
-
"""
|
|
21
|
-
self._env = env
|
|
22
|
-
self._waiting = []
|
|
23
|
-
|
|
24
|
-
async def wait(self):
|
|
25
|
-
"""Wait until gate is next opened."""
|
|
26
|
-
await _Wait(self)
|
|
27
|
-
|
|
28
|
-
async def release(self):
|
|
29
|
-
"""Release all waiting processes."""
|
|
30
|
-
await _Release(self)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
# ----------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class _Wait(BaseAction):
|
|
37
|
-
"""Wait at the gate."""
|
|
38
|
-
|
|
39
|
-
def __init__(self, gate: Gate):
|
|
40
|
-
super().__init__(gate._env)
|
|
41
|
-
self._gate = gate
|
|
42
|
-
|
|
43
|
-
def act(self, proc: Process):
|
|
44
|
-
self._gate._waiting.append(proc)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class _Release(BaseAction):
|
|
48
|
-
"""Release processes waiting at gate."""
|
|
49
|
-
|
|
50
|
-
def __init__(self, gate: Gate):
|
|
51
|
-
super().__init__(gate._env)
|
|
52
|
-
self._gate = gate
|
|
53
|
-
|
|
54
|
-
def act(self, proc: Process):
|
|
55
|
-
while self._gate._waiting:
|
|
56
|
-
self._env.schedule(self._env.now, self._gate._waiting.pop())
|
|
57
|
-
self._env.schedule(self._env.now, proc)
|
asimpy-0.2.0.dist-info/METADATA
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: asimpy
|
|
3
|
-
Version: 0.2.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
|
-
[docs]: https://gvwilson.github.io/asimpy
|
|
38
|
-
[examples]: https://gvwilson.github.io/asimpy/examples/
|
|
39
|
-
[package]: https://pypi.org/project/asimpy/
|
|
40
|
-
[repo]: https://github.com/gvwilson/asimpy
|
|
41
|
-
[simpy]: https://simpy.readthedocs.io/
|
asimpy-0.2.0.dist-info/RECORD
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
asimpy/__init__.py,sha256=i_T5R48aoPnE3zhHaDIWmCOmvjOb32OfmSnhhUWL37g,330
|
|
2
|
-
asimpy/actions.py,sha256=G9f4Zt3Mgt1dvwOgXKbdZwmoy8Z-7pxVvre2C1J-WD0,546
|
|
3
|
-
asimpy/environment.py,sha256=9KooPy0v4gERqyX7H1O7cJQyYmTA04wWT8WExrqHAGI,3098
|
|
4
|
-
asimpy/gate.py,sha256=MFepFLcb8TNhzbaRAF5jJFFbprbRB-Q5phf1uMdl3LA,1381
|
|
5
|
-
asimpy/interrupt.py,sha256=G925y6jHB6b6y30sbq4e3jjutZDIW7Dwg9YY0mgf24Y,572
|
|
6
|
-
asimpy/process.py,sha256=6LRQRHzHI8PVt_AtckSQgsAPqv65BlUNJ8xOOVx9bXs,1550
|
|
7
|
-
asimpy/queue.py,sha256=MVCEsDU5a2774Y5xKD0HlTZAnZwpBDeThbjFEXpkd8s,3287
|
|
8
|
-
asimpy/resource.py,sha256=O18VJARLIZHbvntOxtTfYefGO5Zibz3EcsKWretIlxE,2107
|
|
9
|
-
asimpy-0.2.0.dist-info/METADATA,sha256=XxrAW1lY1xpFpn5xPF00--rFEe8pZQGcOC8WjjVaTWU,1495
|
|
10
|
-
asimpy-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
11
|
-
asimpy-0.2.0.dist-info/licenses/LICENSE.md,sha256=IjTDUvBk8xdl_n50CG1Vtk4FYdrS-C3uEYrRWAoOQqQ,1066
|
|
12
|
-
asimpy-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|