Skip to content

Robotics

Robotics: autonomous robots and robot swarms.

LightEmittingRobotElement(label, visual=VisualSpec('agents', {'color_by': 'light'}), *, phi=0.0, light=0.0, speed=1.0)

Bases: Element

Light-emitting robot that moves in the direction phi at a speed set by the light of the others it senses.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'light'})
phi Annotated[float, StateSpec(role=direction)]

The initial direction of motion.

0.0
light Annotated[float, StateSpec(domain=closed_open(0, oo))]

The light of the other robots the robot measures now.

0.0
speed Annotated[float, StateSpec(domain=closed_open(0, oo))]

The speed the robot moves at.

1.0

Examples:

>>> from skcomplex.systems.engineering.robotics import LightEmittingRobotElement
>>> LightEmittingRobotElement('robot', phi=1.0).phi
1.0
Source code in src/skcomplex/systems/engineering/robotics/_light_emitting_robots.py
58
59
60
61
62
63
64
65
66
67
68
69
70
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'light'}),
    *,
    phi: Annotated[float, StateSpec(role='direction')] = 0.0,
    light: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    speed: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.phi = phi
    self.light = light
    self.speed = speed

LightSensitiveRobotElement(label, visual=None, *, x=0.0, y=0.0, phi=0.0, V0=1.0, V_inf=0.1, I0=1.0, r0=1.0, tau=1.0, R=2.0, delay=0.0)

Bases: Element

Robot in a Gaussian light field that slows down in light, reacting to the light it measured a delay ago.

The robot moves at the speed v = V_inf + (V0 - V_inf) exp(-I(t - delay)) in the direction phi, which diffuses with time scale tau, dphi = sqrt(2 / tau) dW. The light is I(x, y) = I0 exp(-(x^2 + y^2) / r0^2), and a wall at radius R reflects the robot back into a circular well.

Parameters:

Name Type Description Default
label str

The label of the robot or the group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
x Annotated[float, StateSpec(role=position)]

The initial horizontal position.

0.0
y Annotated[float, StateSpec(role=position)]

The initial vertical position.

0.0
phi Annotated[float, StateSpec(role=direction)]

The initial direction of motion.

0.0
V0 Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The speed in the dark.

1.0
V_inf Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The speed in strong light.

0.1
I0 Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The intensity at the centre of the light.

1.0
r0 Annotated[float, ParameterSpec(domain=open(0, oo))]

The width of the light.

1.0
tau Annotated[float, ParameterSpec(domain=open(0, oo))]

The time scale of rotational diffusion.

1.0
R Annotated[float, ParameterSpec(domain=open(0, oo))]

The radius of the well.

2.0
delay float

The sensory delay, 0 for a robot that reacts to the light it measures now.

0.0

Examples:

>>> from skcomplex.systems.engineering.robotics import LightSensitiveRobotElement
>>> LightSensitiveRobotElement('robot', delay=5.0).delay
5.0
Source code in src/skcomplex/systems/engineering/robotics/_robots.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    x: Annotated[float, StateSpec(role='position')] = 0.0,
    y: Annotated[float, StateSpec(role='position')] = 0.0,
    phi: Annotated[float, StateSpec(role='direction')] = 0.0,
    V0: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    V_inf: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
    I0: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    r0: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
    tau: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
    R: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 2.0,
    delay: float = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.x = x
    self.y = y
    self.phi = phi
    self.V0 = V0
    self.V_inf = V_inf
    self.I0 = I0
    self.r0 = r0
    self.tau = tau
    self.R = R
    self.delay = delay

SensoryDelayInteraction(label, *, target, I0=5.0, r0=1.0, V0=1.0, V_inf=0.1, tau=2.0, delay=0.0, step=0.5, side=10.0)

Bases: Interaction

Motion of light-emitting robots whose speed follows the light of the others, sensed with a delay.

Every robot emits the Gaussian light I0 exp(-d^2 / r0^2) at the distance d, and measures the total light I_i of the other robots, at the shortest distances on the torus. It moves at the speed V_inf + (V0 - V_inf) exp(-I_i(t - delay)) in its direction, which diffuses with the time scale tau, phi <- phi + sqrt(2 step / tau) w with a standard normal w. A positive delay reads the light measured that long ago, and a negative delay predicts it by the linear extrapolation I(t) - delay I'(t), never below 0. Before the delay has passed, a robot uses the earliest light it measured. Every step of the rule lasts step.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of robots.

required
I0 float

The intensity of the light of every robot.

5.0
r0 float

The width of the light of every robot.

1.0
V0 float

The speed in the dark.

1.0
V_inf float

The speed in strong light.

0.1
tau float

The time scale of rotational diffusion.

2.0
delay float

The sensory delay, negative for a prediction.

0.0
step float

The time of one step.

0.5
side float

The side of the periodic square arena.

10.0

Examples:

>>> from skcomplex.systems.engineering.robotics import SensoryDelayInteraction
>>> SensoryDelayInteraction('motion', target='robots', delay=-10.0).delay
-10.0
Source code in src/skcomplex/systems/engineering/robotics/_light_emitting_robots.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    I0: float = 5.0,
    r0: float = 1.0,
    V0: float = 1.0,
    V_inf: float = 0.1,
    tau: float = 2.0,
    delay: float = 0.0,
    step: float = 0.5,
    side: float = 10.0,
) -> None:
    super().__init__(label)
    self.target = target
    self.I0 = I0
    self.r0 = r0
    self.V0 = V0
    self.V_inf = V_inf
    self.tau = tau
    self.delay = delay
    self.step = step
    self.side = side

SwarmRobotElement(label, visual=VisualSpec('agents', {'color_by': 'illuminated', 'color_range': (0.0, 1.0)}), *, heading=0.0, connections=0.0, illuminated=1.0, speed=0.2, radio_range=4.0, alpha=20.0, avoid_illuminated=1.5, avoid_shadowed=1.0, shadow_angle=0.5, beacon_x=95.0, beacon_y=50.0, arena=100.0)

Bases: Element

Robot of the swarmtaxis algorithm, which keeps in radio contact with the swarm and avoids its neighbours.

Every step the robot counts the robots within its radio range. When it loses a connection and has fewer than alpha connections, it turns around to stay with the swarm, and when it gains one it turns at random. A robot closer than its avoidance radius to another turns away from it. A robot is shadowed when another robot lies between it and the beacon, within the radio range and shadow_angle of the bearing to the beacon, and it is illuminated otherwise. Illuminated robots avoid at a larger radius than shadowed ones, so they move away from the swarm towards the beacon, and the swarm follows. The robot then moves forward at its speed.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'illuminated', 'color_range': (0.0, 1.0)})
heading Annotated[float, StateSpec(role=direction)]

The direction of motion.

0.0
connections Annotated[float, StateSpec(domain=NN)]

The number of robots within radio range at the last step.

0.0
illuminated Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

Whether the robot sees the beacon.

1.0
speed Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The distance moved per step.

0.2
radio_range Annotated[float, ParameterSpec(domain=open(0, oo))]

The range of the radio pings.

4.0
alpha Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The number of connections below which a robot that loses one turns around.

20.0
avoid_illuminated Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The avoidance radius of illuminated robots.

1.5
avoid_shadowed Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The avoidance radius of shadowed robots.

1.0
shadow_angle Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The half angle of the cone towards the beacon in which another robot casts a shadow.

0.5
beacon_x Annotated[float, ParameterSpec()]

The horizontal position of the beacon.

95.0
beacon_y Annotated[float, ParameterSpec()]

The vertical position of the beacon.

50.0
arena Annotated[float, ParameterSpec(domain=open(0, oo))]

The side of the square arena, whose walls stop the robot.

100.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.engineering.robotics import SwarmRobotElement
>>> GroupElement('robots', element=SwarmRobotElement, size=2, location=[(1.0, 1.0), (2.0, 1.0)]).size
2
Source code in src/skcomplex/systems/engineering/robotics/_robots.py
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'illuminated', 'color_range': (0.0, 1.0)}),
    *,
    heading: Annotated[float, StateSpec(role='direction')] = 0.0,
    connections: Annotated[float, StateSpec(domain=NN)] = 0.0,
    illuminated: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 1.0,
    speed: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.2,
    radio_range: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 4.0,
    alpha: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 20.0,
    avoid_illuminated: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.5,
    avoid_shadowed: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    shadow_angle: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
    beacon_x: Annotated[float, ParameterSpec()] = 95.0,
    beacon_y: Annotated[float, ParameterSpec()] = 50.0,
    arena: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 100.0,
) -> None:
    super().__init__(label, visual=visual)
    self.heading = heading
    self.connections = connections
    self.illuminated = illuminated
    self.speed = speed
    self.radio_range = radio_range
    self.alpha = alpha
    self.avoid_illuminated = avoid_illuminated
    self.avoid_shadowed = avoid_shadowed
    self.shadow_angle = shadow_angle
    self.beacon_x = beacon_x
    self.beacon_y = beacon_y
    self.arena = arena

move(agent, system)

Update the connections, the shadow, and the heading, and move forward.

Parameters:

Name Type Description Default
agent SwarmRobotView

The robot.

required
system SystemView

The system.

required
Source code in src/skcomplex/systems/engineering/robotics/_robots.py
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
def move(self, agent: SwarmRobotView, system: SystemView) -> None:
    """Update the connections, the shadow, and the heading, and move forward.

    Args:
        agent:
            The robot.

        system:
            The system.
    """
    neighbors = agent.neighbors(agent.radio_range)
    count = len(neighbors)
    agent.illuminated = 0.0 if self._is_shadowed(agent, neighbors) else 1.0
    heading = self._avoid(agent, neighbors, self._turn(agent, system, count))
    agent.connections = count
    agent.heading = heading % (2 * math.pi)
    x, y = SwarmRobotElement._position(agent)
    edge = agent.arena * (1 - EDGE_FRACTION)
    x = min(max(x + agent.speed * math.cos(heading), 0.0), edge)
    y = min(max(y + agent.speed * math.sin(heading), 0.0), edge)
    agent.move_to((x, y))

light_emitting_robots(n_robots=50, *, delay=10.0, side=10.0, I0=5.0, r0=1.0, V0=1.0, V_inf=0.1, tau=2.0, step=0.5, random_state=0)

Return light-emitting robots, which cluster when they sense the light of the others late and spread when early.

Every robot emits a Gaussian light and measures the total light of the others, I_i = sum_j I0 exp(-d_ij^2 / r0^2) over the shortest distances on a periodic square arena. It moves at the speed V_inf + (V0 - V_inf) exp(-I_i(t - delay)) in a direction that diffuses with the time scale tau. A robot with a positive delay crosses a dark region at the fast speed it sensed earlier in the dark and slows down only once it is in the light, so the robots linger near each other and form clusters. A negative delay, a prediction of the light by its trend, makes a robot slow down before it reaches the light and speed up before it leaves it, so the robots avoid each other and spread over the arena. Every step lasts step, so the default run of 400 steps covers 200 s.

Parameters:

Name Type Description Default
n_robots int

The number of robots.

50
delay float

The sensory delay, negative for a prediction.

10.0
side float

The side of the periodic square arena.

10.0
I0 float

The intensity of the light of every robot.

5.0
r0 float

The width of the light of every robot.

1.0
V0 float

The speed in the dark.

1.0
V_inf float

The speed in strong light.

0.1
tau float

The time scale of rotational diffusion.

2.0
step float

The time of one step.

0.5
random_state int | Generator | None

The seed or generator of the initial positions and directions.

0

Returns:

Name Type Description
system System

The system, with the group robots of directions phi, measured lights light, and speeds speed, at positions in the arena.

Examples:

>>> from skcomplex.systems.engineering.robotics import light_emitting_robots
>>> light_emitting_robots(5).elements[0].size
5
Source code in src/skcomplex/systems/engineering/robotics/_light_emitting_robots.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
@register_system(
    field='engineering/robotics',
    tags=('robotics', 'active matter', 'delay', 'agents', 'swarms'),
    references=('Mijalkov et al. 2016', 'Argun et al. 2021'),
    defaults={'until': 400},
)
def light_emitting_robots(
    n_robots: int = 50,
    *,
    delay: float = 10.0,
    side: float = 10.0,
    I0: float = 5.0,
    r0: float = 1.0,
    V0: float = 1.0,
    V_inf: float = 0.1,
    tau: float = 2.0,
    step: float = 0.5,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return light-emitting robots, which cluster when they sense the light of the others late and spread when early.

    Every robot emits a Gaussian light and measures the total light of the others, `I_i = sum_j I0 exp(-d_ij^2 /
    r0^2)` over the shortest distances on a periodic square arena. It moves at the speed
    `V_inf + (V0 - V_inf) exp(-I_i(t - delay))` in a direction that diffuses with the time scale `tau`. A robot with a
    positive delay crosses a dark region at the fast speed it sensed earlier in the dark and slows down only once it is
    in the light, so the robots linger near each other and form clusters. A negative delay, a prediction of the light
    by its trend, makes a robot slow down before it reaches the light and speed up before it leaves it, so the robots
    avoid each other and spread over the arena. Every step lasts `step`, so the default run of 400 steps covers 200 s.

    Args:
        n_robots:
            The number of robots.

        delay:
            The sensory delay, negative for a prediction.

        side:
            The side of the periodic square arena.

        I0:
            The intensity of the light of every robot.

        r0:
            The width of the light of every robot.

        V0:
            The speed in the dark.

        V_inf:
            The speed in strong light.

        tau:
            The time scale of rotational diffusion.

        step:
            The time of one step.

        random_state:
            The seed or generator of the initial positions and directions.

    Returns:
        system:
            The system, with the group `robots` of directions `phi`, measured lights `light`, and speeds `speed`, at
            positions in the arena.

    Examples:
        >>> from skcomplex.systems.engineering.robotics import light_emitting_robots
        >>> light_emitting_robots(5).elements[0].size
        5
    """
    rng = np.random.default_rng(random_state)
    positions = rng.uniform(0.0, side, (n_robots, 2))
    robots = GroupElement(
        'robots',
        element=LightEmittingRobotElement,
        size=n_robots,
        values={'phi': rng.uniform(0.0, 2 * np.pi, n_robots), 'speed': V0},
        location=[tuple(map(float, point)) for point in positions],
    )
    motion = SensoryDelayInteraction(
        'motion',
        target='robots',
        I0=I0,
        r0=r0,
        V0=V0,
        V_inf=V_inf,
        tau=tau,
        delay=delay,
        step=step,
        side=side,
    )
    return System(
        elements=[robots],
        interactions=[motion],
        space=ContinuousSpace(extent=(side, side), boundary='periodic'),
    )

light_seeking_robots(n_robots=20, *, delay=5.0, V0=1.0, V_inf=0.1, I0=1.0, r0=1.0, tau=1.0, R=2.0)

Return light-sensitive robots in a circular well, whose sensory delay decides whether they gather in the light.

Each robot moves at V_inf + (V0 - V_inf) exp(-I(t - delay)) in a direction that diffuses with time scale tau, in the Gaussian light I = I0 exp(-(x^2 + y^2) / r0^2) at the centre of a well of radius R. The robots start at the centre, in directions spread around the circle, and do not interact. A positive delay drives them out of the light: a robot crosses the light at the fast speed it measured in the dark and lingers outside at the slow speed it measured in the light, so robots with a delay spend less time in the light than robots without one.

Parameters:

Name Type Description Default
n_robots int

The number of robots.

20
delay float

The sensory delay.

5.0
V0 float

The speed in the dark.

1.0
V_inf float

The speed in strong light.

0.1
I0 float

The intensity at the centre of the light.

1.0
r0 float

The width of the light.

1.0
tau float

The time scale of rotational diffusion.

1.0
R float

The radius of the well.

2.0

Returns:

Name Type Description
system System

The system, with the group robots of positions x, y and directions phi.

Examples:

>>> from skcomplex.systems.engineering.robotics import light_seeking_robots
>>> light_seeking_robots(4).elements[0].prepare().size_
4
Source code in src/skcomplex/systems/engineering/robotics/_robots.py
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
@register_system(
    field='engineering/robotics',
    tags=('robotics', 'active matter', 'delay', 'sde'),
    references=('Mijalkov et al. 2016', 'Argun et al. 2021'),
    defaults={'until': 100.0, 'dt': 0.5},
)
def light_seeking_robots(
    n_robots: int = 20,
    *,
    delay: float = 5.0,
    V0: float = 1.0,
    V_inf: float = 0.1,
    I0: float = 1.0,
    r0: float = 1.0,
    tau: float = 1.0,
    R: float = 2.0,
) -> System:
    """Return light-sensitive robots in a circular well, whose sensory delay decides whether they gather in the light.

    Each robot moves at `V_inf + (V0 - V_inf) exp(-I(t - delay))` in a direction that diffuses with time scale `tau`,
    in the Gaussian light `I = I0 exp(-(x^2 + y^2) / r0^2)` at the centre of a well of radius `R`. The robots start
    at the centre, in directions spread around the circle, and do not interact. A positive delay drives them out of
    the light: a robot crosses the light at the fast speed it measured in the dark and lingers outside at the slow
    speed it measured in the light, so robots with a delay spend less time in the light than robots without one.

    Args:
        n_robots:
            The number of robots.

        delay:
            The sensory delay.

        V0:
            The speed in the dark.

        V_inf:
            The speed in strong light.

        I0:
            The intensity at the centre of the light.

        r0:
            The width of the light.

        tau:
            The time scale of rotational diffusion.

        R:
            The radius of the well.

    Returns:
        system:
            The system, with the group `robots` of positions `x`, `y` and directions `phi`.

    Examples:
        >>> from skcomplex.systems.engineering.robotics import light_seeking_robots
        >>> light_seeking_robots(4).elements[0].prepare().size_
        4
    """
    headings = np.linspace(0.0, 2 * np.pi, n_robots, endpoint=False)
    robots = GroupElement(
        'robots',
        element=LightSensitiveRobotElement,
        size=n_robots,
        values={
            'x': np.zeros(n_robots),
            'y': np.zeros(n_robots),
            'phi': headings,
            'V0': V0,
            'V_inf': V_inf,
            'I0': I0,
            'r0': r0,
            'tau': tau,
            'R': R,
            'delay': delay,
        },
    )
    return System(elements=[robots])

swarmtaxis(n_robots=20, *, size=100.0, spread=3.0, speed=0.2, radio_range=4.0, avoid_illuminated=1.5, avoid_shadowed=1.0, random_state=0)

Return the swarmtaxis algorithm, which moves a swarm of robots towards a beacon that only some of them see.

The robots start in a cluster near the left edge of a square arena, with the beacon at the middle of the right edge. Each robot keeps radio contact with the swarm by turning around when it loses a connection, and avoids its neighbours. Robots that see the beacon avoid at a larger radius than shadowed robots, which moves the centre of the swarm towards the beacon.

Parameters:

Name Type Description Default
n_robots int

The number of robots.

20
size float

The side of the arena.

100.0
spread float

The standard deviation of the initial positions around the start.

3.0
speed float

The distance a robot moves per step.

0.2
radio_range float

The range of the radio pings.

4.0
avoid_illuminated float

The avoidance radius of illuminated robots.

1.5
avoid_shadowed float

The avoidance radius of shadowed robots.

1.0
random_state int | Generator | None

The seed or generator of the initial positions and headings.

0

Returns:

Name Type Description
system System

The system, with the group robots of headings heading and illumination illuminated.

Examples:

>>> from skcomplex.systems.engineering.robotics import swarmtaxis
>>> swarmtaxis(5).elements[0].prepare().size_
5
Source code in src/skcomplex/systems/engineering/robotics/_robots.py
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
@register_system(
    field='engineering/robotics',
    tags=('robotics', 'swarms', 'agents', 'taxis'),
    references=('Bjerknes, Winfield, and Melhuish 2007', 'Varughese et al. 2019'),
    defaults={'until': 300},
)
def swarmtaxis(
    n_robots: int = 20,
    *,
    size: float = 100.0,
    spread: float = 3.0,
    speed: float = 0.2,
    radio_range: float = 4.0,
    avoid_illuminated: float = 1.5,
    avoid_shadowed: float = 1.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the swarmtaxis algorithm, which moves a swarm of robots towards a beacon that only some of them see.

    The robots start in a cluster near the left edge of a square arena, with the beacon at the middle of the right edge.
    Each robot keeps radio contact with the swarm by turning around when it loses a connection, and avoids its
    neighbours. Robots that see the beacon avoid at a larger radius than shadowed robots, which moves the centre of the
    swarm towards the beacon.

    Args:
        n_robots:
            The number of robots.

        size:
            The side of the arena.

        spread:
            The standard deviation of the initial positions around the start.

        speed:
            The distance a robot moves per step.

        radio_range:
            The range of the radio pings.

        avoid_illuminated:
            The avoidance radius of illuminated robots.

        avoid_shadowed:
            The avoidance radius of shadowed robots.

        random_state:
            The seed or generator of the initial positions and headings.

    Returns:
        system:
            The system, with the group `robots` of headings `heading` and illumination `illuminated`.

    Examples:
        >>> from skcomplex.systems.engineering.robotics import swarmtaxis
        >>> swarmtaxis(5).elements[0].prepare().size_
        5
    """
    rng = np.random.default_rng(random_state)
    start = np.array([size / 5, size / 2])
    points = np.clip(start + rng.normal(0.0, spread, (n_robots, 2)), 0.0, size)
    robots = GroupElement(
        'robots',
        element=SwarmRobotElement,
        size=n_robots,
        values={
            'heading': rng.uniform(0.0, 2 * np.pi, n_robots),
            'speed': speed,
            'radio_range': radio_range,
            'alpha': float(n_robots),
            'avoid_illuminated': avoid_illuminated,
            'avoid_shadowed': avoid_shadowed,
            'beacon_x': size - BEACON_MARGIN,
            'beacon_y': size / 2,
            'arena': size,
        },
        location=[tuple(map(float, point)) for point in points],
    )
    return System(elements=[robots], space=ContinuousSpace(extent=(size, size)))