phyelds-mesa 8.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.1
2
+ Name: phyelds-mesa
3
+ Version: 8.1.0
4
+ Summary: Mesa integration for phyelds: aggregate programs on the agents of a Mesa model
5
+ Home-page: https://github.com/phyelds/phyelds
6
+ License: Apache-2.0
7
+ Author: Davide Domini
8
+ Author-email: davide.domini@unibo.it
9
+ Requires-Python: >=3.12,<3.14
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Dist: mesa[network] (>=3.5.1,<4.0.0)
16
+ Requires-Dist: phyelds (>=8.0.0,<9.0.0)
17
+ Project-URL: Repository, https://github.com/phyelds/phyelds
18
+ Description-Content-Type: text/markdown
19
+
20
+ # phyelds-mesa
21
+
22
+ [Mesa](https://mesa.readthedocs.io) integration for [phyelds](https://pypi.org/project/phyelds/):
23
+ run aggregate programs on the agents of a Mesa model.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install phyelds-mesa
29
+ ```
30
+
31
+ `phyelds-mesa` installs a compatible `phyelds` release and Mesa.
32
+
33
+ ## Usage
34
+
35
+ ```python
36
+ from phyelds.calculus import aggregate
37
+ from phyelds.libraries.device import sense
38
+ from phyelds.libraries.distances import hops_distance
39
+ from phyelds.libraries.spreading import distance_to
40
+ from phyelds.mesa import AggregateRuntime, within
41
+
42
+
43
+ @aggregate
44
+ def hops_to_source():
45
+ return distance_to(sense("source"), hops_distance())
46
+
47
+
48
+ runtime = AggregateRuntime(
49
+ hops_to_source,
50
+ neighbors=within(1.5), # or cell_neighbors in a grid
51
+ sensors=lambda agent: {"source": agent.unique_id == 1},
52
+ )
53
+ # In Model.step(): one synchronous round on every agent.
54
+ hops = runtime.run_round(model.agents) # {unique_id: result}
55
+ ```
56
+
57
+ See the [documentation](https://phyelds.github.io/phyelds/integration/mesa/).
58
+
@@ -0,0 +1,38 @@
1
+ # phyelds-mesa
2
+
3
+ [Mesa](https://mesa.readthedocs.io) integration for [phyelds](https://pypi.org/project/phyelds/):
4
+ run aggregate programs on the agents of a Mesa model.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ pip install phyelds-mesa
10
+ ```
11
+
12
+ `phyelds-mesa` installs a compatible `phyelds` release and Mesa.
13
+
14
+ ## Usage
15
+
16
+ ```python
17
+ from phyelds.calculus import aggregate
18
+ from phyelds.libraries.device import sense
19
+ from phyelds.libraries.distances import hops_distance
20
+ from phyelds.libraries.spreading import distance_to
21
+ from phyelds.mesa import AggregateRuntime, within
22
+
23
+
24
+ @aggregate
25
+ def hops_to_source():
26
+ return distance_to(sense("source"), hops_distance())
27
+
28
+
29
+ runtime = AggregateRuntime(
30
+ hops_to_source,
31
+ neighbors=within(1.5), # or cell_neighbors in a grid
32
+ sensors=lambda agent: {"source": agent.unique_id == 1},
33
+ )
34
+ # In Model.step(): one synchronous round on every agent.
35
+ hops = runtime.run_round(model.agents) # {unique_id: result}
36
+ ```
37
+
38
+ See the [documentation](https://phyelds.github.io/phyelds/integration/mesa/).
@@ -0,0 +1,39 @@
1
+ [tool.poetry]
2
+ name = "phyelds-mesa"
3
+ version = "8.1.0"
4
+ description = "Mesa integration for phyelds: aggregate programs on the agents of a Mesa model"
5
+ license = "Apache-2.0"
6
+ authors = [
7
+ "Davide Domini <davide.domini@unibo.it>",
8
+ "Gianluca Aguzzi <gianluca.aguzzi@unibo.it>"
9
+ ]
10
+ homepage = "https://github.com/phyelds/phyelds"
11
+ repository = "https://github.com/phyelds/phyelds"
12
+ readme = "README.md"
13
+
14
+ packages = [
15
+ { include = "phyelds/mesa", from = "src" },
16
+ ]
17
+
18
+ classifiers = [
19
+ "Operating System :: OS Independent",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ ]
22
+
23
+ [tool.poetry.dependencies]
24
+ python = ">=3.12,<3.14"
25
+ phyelds = ">=8.0.0,<9.0.0"
26
+ # Mesa imports networkx when it starts, but declares it only in its network extra.
27
+ mesa = { version = ">=3.5.1,<4.0.0", extras = ["network"] }
28
+
29
+ [tool.poetry.group.dev.dependencies]
30
+ pytest = "^8.0.0"
31
+ pylint = "^3.0.0"
32
+ flake8 = "^7.0.0"
33
+
34
+ [tool.pytest.ini_options]
35
+ testpaths = ["tests"]
36
+
37
+ [build-system]
38
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
39
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,5 @@
1
+ """Run phyelds programs on the agents of a [Mesa](https://mesa.readthedocs.io) model."""
2
+
3
+ from phyelds.mesa.runtime import AggregateRuntime, cell_neighbors, within
4
+
5
+ __all__ = ["AggregateRuntime", "cell_neighbors", "within"]
@@ -0,0 +1,124 @@
1
+ """
2
+ Run phyelds programs on the agents of a Mesa model.
3
+
4
+ Each agent is a device: its ``unique_id`` is the device id, its neighbors are chosen by a
5
+ function of the agent, and the program runs once per agent in each round.
6
+ """
7
+ from collections.abc import Callable, Iterable, Mapping
8
+ from typing import Any
9
+
10
+ from mesa import Agent
11
+
12
+ from phyelds import engine
13
+ from phyelds.data import State
14
+ from phyelds.vm import NodeContext
15
+ from phyelds.vm.internal import MutableEngine
16
+
17
+ Neighbors = Callable[[Agent], Iterable[Agent]]
18
+ Sensors = Callable[[Agent], Mapping[str, Any]]
19
+
20
+
21
+ class AggregateRuntime:
22
+ """
23
+ Runs an aggregate program on Mesa agents, one synchronous round at a time.
24
+
25
+ In a round every agent receives what its neighbors sent in the previous round, so the
26
+ result does not depend on the order of the agents. Call ``run_round`` from the
27
+ ``step`` of the model.
28
+
29
+ Args:
30
+ program: The ``@aggregate`` function to run on each agent.
31
+ neighbors: The agents a given agent talks to, e.g. ``within(radius)`` in a
32
+ continuous space or ``cell_neighbors`` in a grid.
33
+ sensors: Extra sensors of a given agent, added to ``position`` (its position, or
34
+ the coordinate of its cell) and ``time`` (the time of the model).
35
+ """
36
+
37
+ def __init__(
38
+ self, program: Callable[[], Any], *, neighbors: Neighbors, sensors: Sensors | None = None
39
+ ):
40
+ self.program = program
41
+ self.neighbors = neighbors
42
+ self.sensors = sensors
43
+ self.outputs: dict[int, dict[str, Any]] = {}
44
+ """What the program stored with ``store`` on each agent, in the last round."""
45
+ self._states: dict[int, dict[str, Any]] = {}
46
+ self._outboxes: dict[int, dict[str, Any]] = {}
47
+
48
+ def run_round(self, agents: Iterable[Agent]) -> dict[int, Any]:
49
+ """
50
+ Run the program once on each of ``agents``.
51
+
52
+ Agents missing from a round are forgotten: their state and messages are dropped.
53
+
54
+ Args:
55
+ agents: The agents of this round, e.g. ``model.agents``.
56
+
57
+ Returns:
58
+ The result of the program on each agent, by ``unique_id``.
59
+ """
60
+ states, outboxes, outputs, results = {}, {}, {}, {}
61
+ for agent in agents:
62
+ inbox = {
63
+ neighbor.unique_id: self._outboxes[neighbor.unique_id]
64
+ for neighbor in self.neighbors(agent)
65
+ if neighbor.unique_id in self._outboxes
66
+ }
67
+ node_engine = MutableEngine()
68
+ engine.set(node_engine)
69
+ context = NodeContext(agent.unique_id, self._sense(agent))
70
+ node_engine.setup(context, inbox, self._states.get(agent.unique_id, {}))
71
+ result = self.program()
72
+ outboxes[agent.unique_id] = node_engine.cooldown()
73
+ states[agent.unique_id] = node_engine.state_trace()
74
+ outputs[agent.unique_id] = context.outputs
75
+ results[agent.unique_id] = result.value if isinstance(result, State) else result
76
+ self._states, self._outboxes, self.outputs = states, outboxes, outputs
77
+ return results
78
+
79
+ def reset(self) -> None:
80
+ """Forget the state and the messages of every agent."""
81
+ self._states, self._outboxes, self.outputs = {}, {}, {}
82
+
83
+ def _sense(self, agent: Agent) -> dict[str, Any]:
84
+ """The default sensors of ``agent``, updated with the extra ones."""
85
+ sensors = {"position": _position(agent), "time": agent.model.time}
86
+ if self.sensors is not None:
87
+ sensors.update(self.sensors(agent))
88
+ return sensors
89
+
90
+
91
+ def within(radius: float) -> Neighbors:
92
+ """
93
+ Neighbors in a continuous space: the agents closer than ``radius``.
94
+
95
+ Args:
96
+ radius: The communication range.
97
+
98
+ Returns:
99
+ A function giving the neighbors of a ``ContinuousSpaceAgent``.
100
+ """
101
+ return lambda agent: agent.get_neighbors_in_radius(radius)[0]
102
+
103
+
104
+ def cell_neighbors(agent: Agent) -> list[Agent]:
105
+ """
106
+ Neighbors in a grid or network: the other agents in the cell of ``agent`` and around it.
107
+
108
+ Args:
109
+ agent: A ``CellAgent``.
110
+
111
+ Returns:
112
+ The agents of its cell, but itself, and of the neighborhood of its cell.
113
+ """
114
+ same_cell = [other for other in agent.cell.agents if other is not agent]
115
+ return same_cell + list(agent.cell.neighborhood.agents)
116
+
117
+
118
+ def _position(agent: Agent) -> tuple | None:
119
+ """Where ``agent`` is: its position in a continuous space, or the coordinate of its cell."""
120
+ position = getattr(agent, "position", None)
121
+ if position is not None:
122
+ return tuple(float(coordinate) for coordinate in position)
123
+ cell = getattr(agent, "cell", None)
124
+ return None if cell is None else cell.coordinate