asimpy 0.1.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 ADDED
@@ -0,0 +1,8 @@
1
+ """Discrete event simulation using async/await."""
2
+
3
+ from .environment import Environment as Environment
4
+ from .gate import Gate as Gate
5
+ from .interrupt import Interrupt as Interrupt
6
+ from .process import Process as Process
7
+ from .queue import Queue as Queue
8
+ from .resource import Resource as Resource
asimpy/actions.py ADDED
@@ -0,0 +1,26 @@
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/environment.py ADDED
@@ -0,0 +1,121 @@
1
+ """Simulation environment."""
2
+
3
+ from dataclasses import dataclass
4
+ import heapq
5
+ from .actions import BaseAction
6
+ from .process import Process
7
+
8
+
9
+ class Environment:
10
+ """Simulation environment."""
11
+
12
+ def __init__(self, logging: bool = False):
13
+ """
14
+ Construct a new simulation environment.
15
+
16
+ Args:
17
+ logging: print log messages while executing.
18
+ """
19
+ self.now = 0
20
+ self._queue = []
21
+ self._logging = logging
22
+
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))
41
+
42
+ def sleep(self, delay: float | int) -> "_Sleep":
43
+ """
44
+ Suspend the caller for a specified length of time.
45
+
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)
65
+
66
+ if until is not None and pending.time > until:
67
+ break
68
+
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)
78
+
79
+ awaited.act(proc)
80
+
81
+ except StopIteration:
82
+ continue
83
+
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
97
+ class _Pending:
98
+ """Keep track of a waiting process."""
99
+
100
+ 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)
119
+
120
+ def __str__(self) -> str:
121
+ return f"_Sleep({self._delay})"
asimpy/gate.py ADDED
@@ -0,0 +1,57 @@
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/interrupt.py ADDED
@@ -0,0 +1,25 @@
1
+ """Interruption exceptions."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ class Interrupt(Exception):
7
+ """Custom exception class for interruptions."""
8
+
9
+ def __init__(self, cause: Any):
10
+ """
11
+ Construct a new interruption exception.
12
+
13
+ Args:
14
+ cause: reason for interruption.
15
+ """
16
+ super().__init__()
17
+ self.cause = cause
18
+
19
+ def __str__(self) -> str:
20
+ """
21
+ Format interruption as printable string.
22
+
23
+ Returns: string representation of interruption and cause.
24
+ """
25
+ return f"Interrupt({self.cause})"
asimpy/process.py ADDED
@@ -0,0 +1,56 @@
1
+ """Base class for processes."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, TYPE_CHECKING
5
+
6
+ from .interrupt import Interrupt
7
+
8
+ if TYPE_CHECKING:
9
+ from .environment import Environment
10
+
11
+
12
+ class Process(ABC):
13
+ """Base class for active processes."""
14
+
15
+ def __init__(self, env: "Environment", *args: Any):
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.
21
+
22
+ Args:
23
+ env: simulation environment.
24
+ *args: to be passed to `init()` for custom initialization.
25
+ """
26
+ self.env = env
27
+ self._interrupt = None
28
+
29
+ self.init(*args)
30
+
31
+ self._coro = self.run()
32
+ self.env.immediate(self)
33
+
34
+ def init(self, *args: Any, **kwargs: Any) -> None:
35
+ """
36
+ Default (do-nothing) post-initialization method.
37
+
38
+ To satisfy type-checking, derived classes must also declare `*args`
39
+ rather than listing specific parameters by name.
40
+ """
41
+ pass
42
+
43
+ def interrupt(self, cause: Any):
44
+ """
45
+ Interrupt this process by raising an `Interrupt` exception the
46
+ next time the process is scheduled to run.
47
+
48
+ Args:
49
+ cause: reason for interrupt (attacked to `Interrupt` exception).
50
+ """
51
+ self._interrupt = Interrupt(cause)
52
+
53
+ @abstractmethod
54
+ def run(self):
55
+ """Actions for this process."""
56
+ pass
asimpy/queue.py ADDED
@@ -0,0 +1,87 @@
1
+ """Queues."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from .actions import BaseAction
5
+
6
+
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
21
+
22
+ @abstractmethod
23
+ def _empty(self):
24
+ pass
25
+
26
+ @abstractmethod
27
+ def _enqueue(self, obj):
28
+ pass
29
+
30
+
31
+ class Queue(BaseQueue):
32
+ def __init__(self, env):
33
+ super().__init__(env)
34
+ self._items = []
35
+
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)
64
+ 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
+
72
+
73
+ class _Put(BaseAction):
74
+ def __init__(self, queue, obj):
75
+ super().__init__(queue._env)
76
+ self._queue = queue
77
+ self.obj = obj
78
+
79
+ def act(self, proc):
80
+ self._queue._enqueue(self.obj)
81
+
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)
86
+
87
+ self._env.schedule(self._env.now, proc)
asimpy/resource.py ADDED
@@ -0,0 +1,76 @@
1
+ """Shared resource with limited capacity."""
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 Resource:
12
+ """Shared resource with limited capacity."""
13
+
14
+ def __init__(self, env: "Environment", capacity: int = 1):
15
+ """
16
+ Create a new resource.
17
+
18
+ Args:
19
+ env: simulation environment.
20
+ capacity: maximum simultaneous users.
21
+ """
22
+ self._env = env
23
+ self._capacity = capacity
24
+ self._count = 0
25
+ self._waiting = []
26
+
27
+ async def acquire(self):
28
+ """Acquire one unit of the resource."""
29
+ await _Acquire(self)
30
+
31
+ 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
+
44
+
45
+ # ----------------------------------------------------------------------
46
+
47
+
48
+ class _Acquire(BaseAction):
49
+ """Acquire a resource."""
50
+
51
+ def __init__(self, resource: Resource):
52
+ super().__init__(resource._env)
53
+ self._resource = resource
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)
61
+
62
+
63
+ class _Release(BaseAction):
64
+ """Release a resource."""
65
+
66
+ def __init__(self, resource: Resource):
67
+ super().__init__(resource._env)
68
+ self._resource = resource
69
+
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)
@@ -0,0 +1,27 @@
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`.
@@ -0,0 +1,12 @@
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,24 @@
1
+ # MIT License
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ Copyright (c) Greg Wilson.