asimpy 0.1.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 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 .gate import Gate as Gate
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
- from .queue import Queue as Queue
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
- from .actions import BaseAction
6
- from .process import Process
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
- Args:
17
- logging: print log messages while executing.
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
- def immediate(self, proc: Process):
24
- """
25
- Start a new process immediately.
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 sleep(self, delay: float | int) -> "_Sleep":
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
- Args:
47
- delay: how long to sleep.
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
- self.now = pending.time
70
- proc = pending.proc
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
- awaited.act(proc)
38
+ def __str__(self):
39
+ return f"Env(t={self._now})"
80
40
 
81
- except StopIteration:
82
- continue
83
41
 
84
- def __str__(self) -> str:
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
- """Keep track of a waiting process."""
44
+ _counter = itertools.count()
99
45
 
100
46
  time: float
101
- proc: Process
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 __str__(self) -> str:
121
- return f"_Sleep({self._delay})"
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
- """Interruption exceptions."""
1
+ """Interrupt exceptions."""
2
2
 
3
3
  from typing import Any
4
4
 
5
5
 
6
6
  class Interrupt(Exception):
7
- """Custom exception class for interruptions."""
7
+ """Interrupt raised inside a process."""
8
8
 
9
9
  def __init__(self, cause: Any):
10
10
  """
11
- Construct a new interruption exception.
11
+ Construct interruption exception.
12
12
 
13
13
  Args:
14
- cause: reason for interruption.
14
+ cause: reason for interrupt.
15
15
  """
16
16
  super().__init__()
17
17
  self.cause = cause
18
18
 
19
- def __str__(self) -> str:
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 processes."""
1
+ """Base class for active process."""
2
2
 
3
3
  from abc import ABC, abstractmethod
4
- from typing import Any, TYPE_CHECKING
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
- """Base class for active processes."""
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 a new process by performing common initialization,
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
- *args: to be passed to `init()` for custom initialization.
21
+ args: extra constructor arguments passed to `init()`.
25
22
  """
26
- self.env = env
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.env.immediate(self)
28
+ self._env._immediate(self._loop)
33
29
 
34
- def init(self, *args: Any, **kwargs: Any) -> None:
30
+ def init(self, *args: Any, **kwargs: Any):
35
31
  """
36
- Default (do-nothing) post-initialization method.
32
+ Extra construction after generic setup but before coroutine created.
37
33
 
38
- To satisfy type-checking, derived classes must also declare `*args`
39
- rather than listing specific parameters by name.
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 by raising an `Interrupt` exception the
46
- next time the process is scheduled to run.
61
+ Interrupt this process
47
62
 
48
63
  Args:
49
- cause: reason for interrupt (attacked to `Interrupt` exception).
64
+ cause: reason for interrupt.
50
65
  """
51
- self._interrupt = Interrupt(cause)
66
+ if not self._done:
67
+ self._interrupt = Interrupt(cause)
68
+ self._env._immediate(self._loop)
52
69
 
53
- @abstractmethod
54
- def run(self):
55
- """Actions for this process."""
56
- pass
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,87 +1,64 @@
1
- """Queues."""
1
+ """FIFO and priority queues."""
2
2
 
3
- from abc import ABC, abstractmethod
4
- from .actions import BaseAction
3
+ import heapq
4
+ from typing import TYPE_CHECKING, Any
5
+ from .event import Event
5
6
 
7
+ if TYPE_CHECKING:
8
+ from .environment import Environment
6
9
 
7
- class BaseQueue(ABC):
8
- def __init__(self, env):
9
- self._env = env
10
- self._gets = []
11
-
12
- async def get(self):
13
- return await _Get(self)
14
-
15
- async def put(self, obj):
16
- await _Put(self, obj)
17
-
18
- @abstractmethod
19
- def _dequeue(self):
20
- pass
10
+ class Queue:
11
+ """FIFO queue."""
21
12
 
22
- @abstractmethod
23
- def _empty(self):
24
- pass
13
+ def __init__(self, env: "Environment"):
14
+ """
15
+ Construct queue.
25
16
 
26
- @abstractmethod
27
- def _enqueue(self, obj):
28
- pass
29
-
30
-
31
- class Queue(BaseQueue):
32
- def __init__(self, env):
33
- super().__init__(env)
17
+ Args:
18
+ env: simulation environment.
19
+ """
20
+ self._env = env
34
21
  self._items = []
22
+ self._getters = []
35
23
 
36
- def _dequeue(self):
37
- assert len(self._items) > 0
38
- return self._items.pop(0)
39
-
40
- def _enqueue(self, obj):
41
- self._items.append(obj)
42
-
43
- def _empty(self):
44
- return len(self._items) == 0
45
-
46
- def __str__(self):
47
- return f"Queue({', '.join(str(i) for i in self._items)})"
48
-
49
-
50
- # ----------------------------------------------------------------------
51
-
52
-
53
- class _Get(BaseAction):
54
- def __init__(self, queue):
55
- super().__init__(queue._env)
56
- self._queue = queue
57
- self._proc = None
58
- self._item = None
59
-
60
- def act(self, proc):
61
- if self._queue._empty():
62
- self._proc = proc
63
- self._queue._gets.append(self)
24
+ async def get(self):
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):
38
+ """
39
+ Add one item to the queue.
40
+
41
+ Args:
42
+ item: to add to the queue.
43
+ """
44
+ if self._getters:
45
+ evt = self._getters.pop(0)
46
+ evt.succeed(item)
64
47
  else:
65
- self._item = self._queue._dequeue()
66
- self._env.schedule(self._env.now, proc)
67
-
68
- def __await__(self):
69
- yield self
70
- return self._item
71
-
48
+ self._items.append(item)
72
49
 
73
- class _Put(BaseAction):
74
- def __init__(self, queue, obj):
75
- super().__init__(queue._env)
76
- self._queue = queue
77
- self.obj = obj
78
50
 
79
- def act(self, proc):
80
- self._queue._enqueue(self.obj)
51
+ class PriorityQueue(Queue):
52
+ """Ordered queue."""
81
53
 
82
- if len(self._queue._gets) > 0:
83
- waiting_get = self._queue._gets.pop(0)
84
- waiting_get._item = self._queue._dequeue()
85
- self._env.schedule(self._env.now, waiting_get._proc)
54
+ async def put(self, item: Any):
55
+ """
56
+ Add one item to the queue.
86
57
 
87
- self._env.schedule(self._env.now, proc)
58
+ Args:
59
+ item: comparable item to add to queue.
60
+ """
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 .actions import BaseAction
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
- """Shared resource with limited capacity."""
10
+ """A shared resource with limited capacity."""
13
11
 
14
12
  def __init__(self, env: "Environment", capacity: int = 1):
15
13
  """
16
- Create a new resource.
14
+ Construct resource.
17
15
 
18
16
  Args:
19
17
  env: simulation environment.
20
- capacity: maximum simultaneous users.
18
+ capacity: maximum capacity.
21
19
  """
20
+ assert capacity > 0
22
21
  self._env = env
23
- self._capacity = capacity
22
+ self.capacity = capacity
24
23
  self._count = 0
25
- self._waiting = []
24
+ self._waiters = []
26
25
 
27
26
  async def acquire(self):
28
- """Acquire one unit of the resource."""
29
- await _Acquire(self)
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 the resource."""
33
- await _Release(self)
34
-
35
- async def __aenter__(self):
36
- """Acquire one unit of the resource using `async with`."""
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
- class _Acquire(BaseAction):
49
- """Acquire a resource."""
47
+ evt._on_cancel = cancel
48
+ self._env._immediate(evt.succeed)
49
+ await evt
50
50
 
51
- def __init__(self, resource: Resource):
52
- super().__init__(resource._env)
53
- self._resource = resource
51
+ async def _acquire_unavailable(self):
52
+ evt = Event(self._env)
53
+ self._waiters.append(evt)
54
54
 
55
- def act(self, proc: Process):
56
- if self._resource._count < self._resource._capacity:
57
- self._resource._count += 1
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
- class _Release(BaseAction):
64
- """Release a resource."""
62
+ self._count += 1
65
63
 
66
- def __init__(self, resource: Resource):
67
- super().__init__(resource._env)
68
- self._resource = resource
64
+ async def __aenter__(self):
65
+ await self.acquire()
66
+ return self
69
67
 
70
- def act(self, proc: Process):
71
- self._resource._count -= 1
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)
@@ -1,27 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: asimpy
3
- Version: 0.1.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-material>=9.7.1; extra == 'dev'
17
- Requires-Dist: mkdocs>=1.6.1; extra == 'dev'
18
- Requires-Dist: mkdocstrings[python]>=1.0.0; extra == 'dev'
19
- Requires-Dist: ruff>=0.14.10; extra == 'dev'
20
- Requires-Dist: taskipy>=1.14.1; extra == 'dev'
21
- Requires-Dist: twine>=6.2.0; extra == 'dev'
22
- Requires-Dist: ty>=0.0.11; extra == 'dev'
23
- Description-Content-Type: text/markdown
24
-
25
- # asimpy
26
-
27
- A simple discrete event simulation framework in Python using `async`/`await`.
@@ -1,12 +0,0 @@
1
- asimpy/__init__.py,sha256=BJoqSPhuG6zSHkEqYh8OBZPrqbAn52EYg9Hjf236xRk,298
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=qQOmQfuZ_MBS5aRax7pEuWrR6swlRmI-MU0iiWD6s-0,1938
8
- asimpy/resource.py,sha256=O18VJARLIZHbvntOxtTfYefGO5Zibz3EcsKWretIlxE,2107
9
- asimpy-0.1.0.dist-info/METADATA,sha256=oArpUAofvClqpvODhBOE_DyY2vyPWPZikHOTSQrA8MY,1044
10
- asimpy-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
11
- asimpy-0.1.0.dist-info/licenses/LICENSE.md,sha256=IjTDUvBk8xdl_n50CG1Vtk4FYdrS-C3uEYrRWAoOQqQ,1066
12
- asimpy-0.1.0.dist-info/RECORD,,
File without changes