phyelds-webots 8.2.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,63 @@
1
+ Metadata-Version: 2.1
2
+ Name: phyelds-webots
3
+ Version: 8.2.0
4
+ Summary: Webots integration for phyelds: aggregate programs on the robots of a Webots simulation
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: phyelds (>=8.2.0,<9.0.0)
16
+ Project-URL: Repository, https://github.com/phyelds/phyelds
17
+ Description-Content-Type: text/markdown
18
+
19
+ # phyelds-webots
20
+
21
+ [Webots](https://cyberbotics.com) integration for [phyelds](https://pypi.org/project/phyelds/):
22
+ run aggregate programs on the robots of a Webots simulation.
23
+
24
+ ## Installation
25
+
26
+ Install it in the Python that Webots runs the controllers with (Python 3.12 or later):
27
+
28
+ ```bash
29
+ pip install phyelds-webots
30
+ ```
31
+
32
+ The `controller` module comes with Webots.
33
+
34
+ ## Usage
35
+
36
+ Give each robot an `Emitter`, a `Receiver` and, for the `position` sensor, a `GPS`; the range
37
+ of the emitter decides who the neighbors are. Then, in the controller:
38
+
39
+ ```python
40
+ from controller import Robot
41
+
42
+ from phyelds.calculus import aggregate
43
+ from phyelds.libraries.device import sense
44
+ from phyelds.libraries.distances import hops_distance
45
+ from phyelds.libraries.spreading import distance_to
46
+ from phyelds.webots import AggregateController
47
+
48
+
49
+ @aggregate
50
+ def hops_to_source():
51
+ return distance_to(sense("source"), hops_distance())
52
+
53
+
54
+ robot = Robot()
55
+ aggregate_controller = AggregateController(
56
+ robot, hops_to_source, gps="gps", sensors=lambda: {"source": robot.getName() == "source"}
57
+ )
58
+ while robot.step(int(robot.getBasicTimeStep())) != -1:
59
+ hops = aggregate_controller.run_round() # one round per step
60
+ ```
61
+
62
+ See the [documentation](https://phyelds.github.io/phyelds/integration/webots/).
63
+
@@ -0,0 +1,44 @@
1
+ # phyelds-webots
2
+
3
+ [Webots](https://cyberbotics.com) integration for [phyelds](https://pypi.org/project/phyelds/):
4
+ run aggregate programs on the robots of a Webots simulation.
5
+
6
+ ## Installation
7
+
8
+ Install it in the Python that Webots runs the controllers with (Python 3.12 or later):
9
+
10
+ ```bash
11
+ pip install phyelds-webots
12
+ ```
13
+
14
+ The `controller` module comes with Webots.
15
+
16
+ ## Usage
17
+
18
+ Give each robot an `Emitter`, a `Receiver` and, for the `position` sensor, a `GPS`; the range
19
+ of the emitter decides who the neighbors are. Then, in the controller:
20
+
21
+ ```python
22
+ from controller import Robot
23
+
24
+ from phyelds.calculus import aggregate
25
+ from phyelds.libraries.device import sense
26
+ from phyelds.libraries.distances import hops_distance
27
+ from phyelds.libraries.spreading import distance_to
28
+ from phyelds.webots import AggregateController
29
+
30
+
31
+ @aggregate
32
+ def hops_to_source():
33
+ return distance_to(sense("source"), hops_distance())
34
+
35
+
36
+ robot = Robot()
37
+ aggregate_controller = AggregateController(
38
+ robot, hops_to_source, gps="gps", sensors=lambda: {"source": robot.getName() == "source"}
39
+ )
40
+ while robot.step(int(robot.getBasicTimeStep())) != -1:
41
+ hops = aggregate_controller.run_round() # one round per step
42
+ ```
43
+
44
+ See the [documentation](https://phyelds.github.io/phyelds/integration/webots/).
@@ -0,0 +1,37 @@
1
+ [tool.poetry]
2
+ name = "phyelds-webots"
3
+ version = "8.2.0"
4
+ description = "Webots integration for phyelds: aggregate programs on the robots of a Webots simulation"
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/webots", 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.2.0"
26
+
27
+ [tool.poetry.group.dev.dependencies]
28
+ pytest = "^8.0.0"
29
+ pylint = "^3.0.0"
30
+ flake8 = "^7.0.0"
31
+
32
+ [tool.pytest.ini_options]
33
+ testpaths = ["tests"]
34
+
35
+ [build-system]
36
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
37
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,103 @@
1
+ """
2
+ Run phyelds programs on the robots of a [Webots](https://cyberbotics.com) simulation.
3
+
4
+ Each robot runs its own controller, so each robot is a device: its messages travel as bytes
5
+ from its ``Emitter`` to the ``Receiver`` of the robots in range.
6
+ """
7
+ import json
8
+ from collections.abc import Callable, Mapping
9
+ from typing import Any
10
+
11
+ from phyelds.vm.device import Device
12
+
13
+ __all__ = ["AggregateController"]
14
+
15
+
16
+ class AggregateController:
17
+ """
18
+ Runs an aggregate program on a Webots robot, one round per simulation step.
19
+
20
+ The device id is the name of the robot, so robots must have unique names. The neighbors
21
+ of a robot are the robots its ``Emitter`` reaches; Webots delivers a message in the step
22
+ after it was sent, so with a round at every step of every robot the rounds are
23
+ synchronous.
24
+
25
+ Args:
26
+ robot: The ``controller.Robot`` of this controller.
27
+ program: The ``@aggregate`` function to run.
28
+ emitter: The name of the ``Emitter`` device of the robot.
29
+ receiver: The name of the ``Receiver`` device of the robot.
30
+ gps: The name of a ``GPS`` device, whose values become the ``position`` sensor.
31
+ sensors: Extra sensors, read at each round, added to ``time`` (the simulation time)
32
+ and ``position``.
33
+ paths: The path mode of the engine, ``"debug"`` or ``"compact"``: the same on
34
+ every robot.
35
+
36
+ Raises:
37
+ ValueError: If the robot has no device with one of the given names.
38
+ """
39
+
40
+ def __init__( # pylint: disable=too-many-arguments
41
+ self,
42
+ robot: Any,
43
+ program: Callable[[], Any],
44
+ *,
45
+ emitter: str = "emitter",
46
+ receiver: str = "receiver",
47
+ gps: str | None = None,
48
+ sensors: Callable[[], Mapping[str, Any]] | None = None,
49
+ paths: str = "debug",
50
+ ):
51
+ self.robot = robot
52
+ self.sensors = sensors
53
+ self._device = Device(robot.getName(), program, paths=paths)
54
+ self._emitter = _get_device(robot, emitter)
55
+ self._receiver = _get_device(robot, receiver)
56
+ self._gps = None if gps is None else _get_device(robot, gps)
57
+ time_step = int(robot.getBasicTimeStep())
58
+ self._receiver.enable(time_step)
59
+ if self._gps is not None:
60
+ self._gps.enable(time_step)
61
+
62
+ @property
63
+ def result(self) -> Any:
64
+ """The value the program computed in the last round."""
65
+ return self._device.result
66
+
67
+ @property
68
+ def outputs(self) -> dict[str, Any]:
69
+ """What the program stored with ``store`` in the last round."""
70
+ return self._device.outputs
71
+
72
+ def run_round(self) -> Any:
73
+ """
74
+ Run the program once: read the messages received, compute, and send.
75
+
76
+ Call it after each ``robot.step``.
77
+
78
+ Returns:
79
+ The value the program computed.
80
+ """
81
+ while self._receiver.getQueueLength() > 0:
82
+ sender, _, payload = self._receiver.getBytes().partition(b"\n")
83
+ self._device.receive(json.loads(sender), payload)
84
+ self._receiver.nextPacket()
85
+ payload = self._device.run_round(self._sense())
86
+ self._emitter.send(json.dumps(self._device.device_id).encode() + b"\n" + payload)
87
+ return self.result
88
+
89
+ def _sense(self) -> dict[str, Any]:
90
+ """The sensors of this round."""
91
+ position = None if self._gps is None else tuple(self._gps.getValues())
92
+ sensors = {"position": position, "time": self.robot.getTime()}
93
+ if self.sensors is not None:
94
+ sensors.update(self.sensors())
95
+ return sensors
96
+
97
+
98
+ def _get_device(robot: Any, name: str) -> Any:
99
+ """The device ``name`` of ``robot``."""
100
+ device = robot.getDevice(name)
101
+ if device is None:
102
+ raise ValueError(f"robot {robot.getName()!r} has no device named {name!r}")
103
+ return device