Swarm behaviours¶
Experimental
This is a study in progress: its API will probably change.
A swarm behaviour is an aggregate function that returns where a robot moves: a
Vector, a velocity or a heading. Behaviours are summed and weighted to compose new
ones, and the program sends the result to the velocity actuator the robot moves by. The
blocks follow MacroSwarm: A Field-based Compositional Framework for Swarm
Programming (Aguzzi, Casadei, Viroli, LMCS 2025)
and its ScaFi implementation.
from phyelds.experimental.swarm import Vector, go_to, separation
from phyelds.libraries.device import velocity
velocity.send((go_to(target) + separation(Vector(), within=1.0) * 2).normalize())
Move a swarm in the playground
Vectors, in the plane and in space¶
Vector(x, y, z=0) adds, subtracts and scales with the usual operators, and has dot,
cross, length, normalize, limit and rotate (around the vertical axis).
Vector(*position) is the vector of a position of two or three coordinates, and
tuple(v) its components. A planar swarm leaves z at zero, so the same blocks move
robots on the ground and drones in the air: those that draw a direction, as brownian,
take dimensions=3 to draw it in space.
Vectors travel in messages, so they can be shared with neighbors and spread.
Moving the robots¶
The program sends the velocity; whoever runs it moves the robot. In the
simulator, follow_velocity moves a node by the velocity its
program sends, in any dimension, capped at max_speed:
from phyelds.simulator.actions import follow_velocity
for node in simulator.environment.nodes.values():
simulator.schedule_event(0.5, follow_velocity, simulator, 1.0, node, max_speed=0.2)
A swarm that gathers and spreads out changes how many neighbors each drone has: with
adaptive_range, each node adapts its range to
keep about the same number, as the examples on this page do.
On a robot, read it after each round with velocity.read(outputs), as from
Webots or Mesa, and drive the
motors with it.
The blocks¶
Movement: phyelds.experimental.swarm.movement¶
| Block | Moves the robot |
|---|---|
stand_still() |
Nowhere: zero |
brownian(scale=1.0, *, dimensions=2) |
In a random direction, of length scale |
go_to(target, speed=1.0) |
Towards target; zero once on it. is_close(target, distance) tells whether it got there |
explore(minimum, maximum, speed=1.0) |
Towards a random goal in a box, a new one once reached |
maintain_trajectory(velocity, period) |
As velocity() drew, drawing again every period |
maintain_until(velocity, condition) |
As velocity, until condition holds |
avoid_obstacles(obstacles, min_distance) |
Away from the obstacles closer than min_distance, given as vectors to them |
neighbors_vectors(position=None) |
The field of the vectors from the robot to each neighbor |
The blocks that need to know where the robot is (go_to, is_close, explore,
neighbors_vectors) read the position sensor.
Flocking: phyelds.experimental.swarm.flocking¶
| Block | Steers the robot |
|---|---|
separation(velocity, *, within=None, nearest=None) |
Away from its neighbors |
alignment(velocity, ...) |
As its neighbors move |
cohesion(velocity, ...) |
Towards its neighbors |
reynolds(velocity, *, vision, separation_within, ...) |
The boids: the three above, weighted, plus velocity |
vicsek(velocity, *, alpha=0.1, noise=0.1, ...) |
Towards the mean velocity, with some noise |
cucker_smale(velocity, strength, *, noise=0.1, ...) |
Towards the velocity of each neighbor, the more the closer |
Each takes the velocity of the robot in the previous round, as the models iterate on velocities; remember it across rounds:
from phyelds.calculus import remember_and_evolve
velocity = remember_and_evolve(Vector(), lambda previous: vicsek(previous.value)).value
separation, alignment and cohesion return a steering, the heading minus
velocity: pass Vector() to get the bare heading. within and nearest restrict the
neighbors a block looks at, as the queries of MacroSwarm: to those closer than a
distance, or to the closest few.
Leaders and teams: phyelds.experimental.swarm.leader¶
A leader is a Boolean field: chosen, sensed or elected. Each robot follows the nearest leader, along the network.
| Block | Does |
|---|---|
sink_at(leader) |
Heads to the leader, along the shortest path |
spin_around(center) |
Turns clockwise around the center |
align_with_leader(leader, velocity) |
Moves as the leader: its velocity, spread |
team_formation(intra, extra, *, condition=..., leader=None) |
Splits the swarm in teams around elected leaders, gathering them until condition holds; returns a Team |
Team.inside(behaviour) |
Runs behaviour(leader) in each formed team, apart from the others |
is_team_formed(leader, target_distance) |
Whether every member has its closest neighbor within target_distance |
cast_along(source, value, step) |
Spreads value along the shortest paths, changing it by the vector to each neighbor on the way |
Every robot must run a leader block, the leader too, since the leader is the source the others follow: compute it outside the branches that tell the leader apart.
Example: a mission¶
A swarm gathers around its leader, crosses the field with it, then turns around it. The phases are a collective state machine: only the leader senses when to go on, and the machine spreads each phase to the swarm. In each phase, the blocks say how a drone moves.
stateDiagram-v2
[*] --> GATHER
GATHER --> CROSS
CROSS --> CIRCLE
class Phase(IntEnum):
"""The phases of the mission."""
GATHER = 0
CROSS = 1
CIRCLE = 2
mission = StateMachine(*Phase)
def moving(towards: Vector) -> Vector:
"""Head ``towards``, keeping apart from the other drones."""
return (towards + separation(Vector(), within=SPACING) * 1.5).normalize()
@mission.state(Phase.GATHER, to=[Phase.CROSS])
def gather() -> Vector:
"""Head to the leader, until it says to go."""
if sense("go"):
mission.go(Phase.CROSS, priority=-local_time())
return moving(sink_at(sense("leader")))
@mission.state(Phase.CROSS, to=[Phase.CIRCLE])
def cross() -> Vector:
"""The leader heads to the target, the others move as it does, close to it."""
if sense("circle"):
mission.go(Phase.CIRCLE, priority=-local_time())
leader = sense("leader")
heading = align_with_leader(leader, go_to(TARGET) if leader else Vector())
return moving(heading + sink_at(leader) * 0.3)
@mission.state(Phase.CIRCLE)
def circle() -> Vector:
"""Turn around the leader, close to it."""
leader = sense("leader")
# Every drone runs the blocks, the leader too: it is the source of their gradients.
turning = moving(spin_around(leader) + sink_at(leader) * 0.3)
return Vector() if leader else turning
@aggregate
def drone() -> Phase:
"""The program every drone runs: the phase it agrees on, and where it moves."""
phase, heading = mission.run()
velocity.send(heading)
return phase

Blue is gather, orange cross, green circle. The complete example is
docs/examples/swarm.py.
What is not here¶
MacroSwarm also has formations by slots around a leader (line, centeredCircle,
vShape): they are not ported.
Its plans, sequences of behaviours each ending on a condition, are collective state machines here, as in the example above.