Skip to content

Collective behaviour

Collective behaviour: animals and people that coordinate through signals, trails, and contact.

AlarmInteraction(label, *, calm, nervous, spontaneous=0.01, contagion=2.0, stochastic=True)

Bases: ReactionInteraction

Calm rats that turn nervous on their own or by meeting nervous rats, at the alarm propensity.

A calm rat turns nervous at rate spontaneous + contagion * nervous / N, where N is the total number of rats, so the propensity of the reaction calm to nervous is that rate times the calm rats. It is a ReactionInteraction that moves one rat from the calm to the nervous rats, a rate equation, or in stochastic mode a random event per rat.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
calm Annotated[str, RoleSpec()]

The path of the calm rats.

required
nervous Annotated[str, RoleSpec()]

The path of the nervous rats.

required
spontaneous Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The rate at which a calm rat turns nervous on its own.

0.01
contagion Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The rate at which a calm rat turns nervous when all others are nervous.

2.0
stochastic bool

Whether changes are random events.

True

Examples:

>>> from skcomplex.core.components.system import System
>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.biology.collective_behaviour import AlarmInteraction, RatsElement
>>> alarm = AlarmInteraction('alarm', calm='calm', nervous='nervous', stochastic=False)
>>> rats = [RatsElement('calm', n=90.0), RatsElement('nervous', n=10.0)]
>>> system = System(elements=rats, interactions=[alarm])
>>> trajectory = Simulation(system, dt=0.1).prepare().run(until=5.0).trajectory_
>>> round(float(trajectory.values('calm.n')[-1] + trajectory.values('nervous.n')[-1]), 6)
100.0
Source code in src/skcomplex/systems/biology/collective_behaviour/_decisions.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
def __init__(
    self,
    label: str,
    *,
    calm: Annotated[str, RoleSpec()],
    nervous: Annotated[str, RoleSpec()],
    spontaneous: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.01,
    contagion: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 2.0,
    stochastic: bool = True,
) -> None:
    super().__init__(label)
    self.calm = calm
    self.nervous = nervous
    self.spontaneous = spontaneous
    self.contagion = contagion
    self.stochastic = stochastic

AntElement(label, visual=VisualSpec('agents', {'color_by': 'carrying', 'color_range': (0.0, 1.0)}), *, carrying=0.0, arrived=0.0, nest_x=5.0, nest_y=10.0, food_x=34.0, food_y=10.0, food_radius=3.0, attraction=5.0, outward=0.5, deposit=1.0)

Bases: Element

Ant that searches for food guided by pheromone and carries food home, marking the way with pheromone.

A searching ant moves to a neighbouring cell with a probability that grows with its pheromone and, by outward, with its distance from the nest, as a scent of the nest would, and picks up food within a radius of the food. A carrying ant walks straight home, deposits pheromone on every cell it enters, and drops the food at the nest. The system adds the walk home, the deposit, the search, the pick-up and drop, and the arrival as member interactions, and an ant picks up or drops food the step after it arrives.

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': 'carrying', 'color_range': (0.0, 1.0)})
carrying Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

Whether the ant carries food.

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

Whether the ant stood at the nest with food, or near the food without it, at the end of the previous step.

0.0
nest_x Annotated[float, ParameterSpec()]

The column of the nest.

5.0
nest_y Annotated[float, ParameterSpec()]

The row of the nest.

10.0
food_x Annotated[float, ParameterSpec()]

The column of the food.

34.0
food_y Annotated[float, ParameterSpec()]

The row of the food.

10.0
food_radius Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The distance from the food within which ants pick it up.

3.0
attraction Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

How strongly pheromone draws searching ants.

5.0
outward Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

How strongly searching ants move away from the nest.

0.5
deposit Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The pheromone an ant deposits per step home.

1.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.biology.collective_behaviour import AntElement
>>> GroupElement('ants', element=AntElement, size=5, location=(0, 0)).size
5
Source code in src/skcomplex/systems/biology/collective_behaviour/_foraging.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'carrying', 'color_range': (0.0, 1.0)}),
    *,
    carrying: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0.0,
    arrived: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0.0,
    nest_x: Annotated[float, ParameterSpec()] = 5.0,
    nest_y: Annotated[float, ParameterSpec()] = 10.0,
    food_x: Annotated[float, ParameterSpec()] = 34.0,
    food_y: Annotated[float, ParameterSpec()] = 10.0,
    food_radius: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 3.0,
    attraction: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 5.0,
    outward: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
    deposit: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.carrying = carrying
    self.arrived = arrived
    self.nest_x = nest_x
    self.nest_y = nest_y
    self.food_x = food_x
    self.food_y = food_y
    self.food_radius = food_radius
    self.attraction = attraction
    self.outward = outward
    self.deposit = deposit

BoidElement(label, visual=VisualSpec('agents', {'color_by': 'vx'}), *, vx=1.0, vy=0.0, radius=5.0, separation_radius=1.0, cohesion=0.01, separation=0.1, alignment=0.1, min_speed=0.5, max_speed=1.0, width=30.0, height=30.0)

Bases: Element

Boid of Reynolds, a bird that steers by three rules toward, away from, and along its neighbours.

Every step a boid sees the boids within radius. FlockingInteraction steers it by cohesion toward their center, by separation away from those closer than separation_radius, and by alignment toward their mean velocity, with its speed between min_speed and max_speed, and FlightInteraction moves it on a torus of side width by height.

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': 'vx'})
vx Annotated[float, StateSpec(role=velocity)]

The initial horizontal velocity.

1.0
vy Annotated[float, StateSpec(role=velocity)]

The initial vertical velocity.

0.0
radius Annotated[float, ParameterSpec(domain=open(0, oo))]

The distance within which a boid sees others.

5.0
separation_radius Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The distance within which a boid avoids others.

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

The weight of cohesion.

0.01
separation Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The weight of separation.

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

The weight of alignment.

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

The smallest speed.

0.5
max_speed Annotated[float, ParameterSpec(domain=open(0, oo))]

The largest speed.

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

The width of the torus.

30.0
height Annotated[float, ParameterSpec(domain=open(0, oo))]

The height of the torus.

30.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.biology.collective_behaviour import BoidElement
>>> GroupElement('flock', element=BoidElement, size=10, location=(5.0, 5.0)).size
10
Source code in src/skcomplex/systems/biology/collective_behaviour/_flocks.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'vx'}),
    *,
    vx: Annotated[float, StateSpec(role='velocity')] = 1.0,
    vy: Annotated[float, StateSpec(role='velocity')] = 0.0,
    radius: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 5.0,
    separation_radius: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    cohesion: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.01,
    separation: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
    alignment: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
    min_speed: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
    max_speed: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
    width: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 30.0,
    height: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 30.0,
) -> None:
    super().__init__(label, visual=visual)
    self.vx = vx
    self.vy = vy
    self.radius = radius
    self.separation_radius = separation_radius
    self.cohesion = cohesion
    self.separation = separation
    self.alignment = alignment
    self.min_speed = min_speed
    self.max_speed = max_speed
    self.width = width
    self.height = height

BranchElement(label, visual=None, *, n=0.0)

Bases: CompartmentElement

Branch of a path that ants can take, holding the number n of pheromone marks left by the ants that took it.

Parameters:

Name Type Description Default
label str

The label of the branch.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
n Annotated[float, StateSpec(domain=closed_open(0, oo))]

The initial number of marks.

0.0

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import BranchElement
>>> BranchElement('left', n=5.0).n
5.0
Source code in src/skcomplex/systems/biology/collective_behaviour/_decisions.py
42
43
44
45
46
47
48
49
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    n: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
) -> None:
    super().__init__(label, visual=visual, n=n)

ColonySearchInteraction(label, *, segments, edges=(), lengths=(), start=0, target=1, ants=20, steps=80, alpha=0.8, beta=1.0, rho=0.5, Q=1.0, cut=None)

Bases: Interaction

Rounds of ants that walk a road network by pheromone and length, mark the paths they find, and let it fade.

In every round, following Argun et al. (2021, section 15.2), ants ants start at the vertex start and take up to steps steps. At vertex i an ant moves to the neighbour j with the probability tau_ij^alpha w_ij^beta / sum_k tau_ik^alpha w_ik^beta, with the weight w_ij = 1 / d_ij of the length d_ij, Eq. (15.2), and stops at the vertex target. The path of every ant that arrives is cleared of its loops, and every segment of it receives the pheromone Q / L of its length L, Eq. (15.3), after the pheromone of every segment has evaporated by the factor 1 - rho, Eq. (15.4). At the round cut, two segments near the middle of the shortest path found so far are cut, as long as the network stays connected, and the colony must heal the disruption. The interaction reports the length of the shortest path of every round as shortest.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
segments Annotated[str, RoleSpec()]

The path of the group of segments.

required
edges Sequence[tuple[int, int]]

The two ends of every segment, in the order of the group.

()
lengths Sequence[float]

The length of every segment.

()
start int

The vertex the ants start from.

0
target int

The vertex the ants look for.

1
ants int

The number of ants of a round.

20
steps int

The largest number of steps of an ant in a round.

80
alpha float

The exponent of the pheromone.

0.8
beta float

The exponent of the weight.

1.0
rho float

The evaporation rate.

0.5
Q float

The pheromone an ant lays along a path.

1.0
cut int | None

The round at which the shortest path found is cut, or None for none.

None

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import ColonySearchInteraction
>>> ColonySearchInteraction('search', segments='roads', edges=((0, 1),), lengths=(1.0,), target=1).ants
20
Source code in src/skcomplex/systems/biology/collective_behaviour/_colony_optimization.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def __init__(
    self,
    label: str,
    *,
    segments: Annotated[str, RoleSpec()],
    edges: Sequence[tuple[int, int]] = (),
    lengths: Sequence[float] = (),
    start: int = 0,
    target: int = 1,
    ants: int = 20,
    steps: int = 80,
    alpha: float = 0.8,
    beta: float = 1.0,
    rho: float = 0.5,
    Q: float = 1.0,
    cut: int | None = None,
) -> None:
    super().__init__(label)
    self.segments = segments
    self.edges = edges
    self.lengths = lengths
    self.start = start
    self.target = target
    self.ants = ants
    self.steps = steps
    self.alpha = alpha
    self.beta = beta
    self.rho = rho
    self.Q = Q
    self.cut = cut

search(system)

Run one round of ants, cut the best path at the round cut, and update the pheromone.

Parameters:

Name Type Description Default
system SystemView

The system.

required
Source code in src/skcomplex/systems/biology/collective_behaviour/_colony_optimization.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def search(self, system: SystemView) -> None:
    """Run one round of ants, cut the best path at the round `cut`, and update the pheromone.

    Args:
        system:
            The system.
    """
    segments = system.agents(self.segments, TrailSegmentView)
    tau = np.array([item.tau for item in segments])
    is_open = np.array([item.passable for item in segments]) > 0
    best = system.storage.get('best')
    if (
        self.cut is not None
        and system.time >= self.cut
        and isinstance(best, list)
        and not system.storage.get('cut')
    ):
        is_open = self._disrupt(is_open, best)
        tau[~is_open] = 0.0
        system.storage['cut'] = True
    paths = [self._walk(tau, is_open, system.rng) for _ in range(self.ants)]
    found = [path for path in paths if path]
    lengths = np.asarray(self.lengths, dtype=float)
    deposit = np.zeros_like(tau)
    shortest = np.inf
    for path in found:
        total = float(lengths[path].sum())
        deposit[path] += self.Q / total
        if total < shortest:
            shortest = total
            system.storage['best'] = path
    tau = ((1 - self.rho) * tau + deposit) * is_open
    for item, level, state in zip(segments, tau, is_open, strict=True):
        item.tau = float(level)
        item.passable = float(state)
    system.report('shortest', shortest if np.isfinite(shortest) else float('nan'))

FlightInteraction(label, *, target, probability=1.0)

Bases: MovementInteraction

Flight of every boid by its velocity (vx, vy) in one step, on the torus of side width by height.

Every boid moves in its own activation, after the rules and member interactions listed before it.

Parameters:

Name Type Description Default
label str

The label of the interaction, unique among the parts of its system.

required
target Annotated[str, RoleSpec()]

The path of the group of boids.

required
probability Annotated[float, ParameterSpec(domain=closed(0, 1))]

The probability that a boid moves in a step.

1.0

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import FlightInteraction
>>> FlightInteraction('flight', target='flock').schedule
'member'
Source code in src/skcomplex/systems/biology/collective_behaviour/_flocks.py
253
254
255
256
257
258
259
260
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    probability: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 1.0,
) -> None:
    super().__init__(label, target=target, step=self._fly, probability=probability, schedule='member')

FlockingInteraction(label, *, target, phase=None)

Bases: CouplingInteraction

Reynolds' steering of every boid toward, away from, and along the boids it sees, with a bounded speed.

The velocity of a boid gains the mean offset to the boids within radius weighted by cohesion, loses the summed offset to the boids within separation_radius weighted by separation, and gains the mean difference of their velocities from its own weighted by alignment. Its speed is then clipped between min_speed and max_speed. Offsets are the shortest ones on the torus of side width by height. It is the agent form of coupling over two radii of one priority, with a clamp of the norm.

Parameters:

Name Type Description Default
label str

The label of the interaction, unique among the parts of its system.

required
target Annotated[str, RoleSpec()]

The path of the group of boids.

required
phase str | None

The phase of the step the steering runs in, or None for the default phase.

None

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import FlockingInteraction
>>> [spec.reduce for spec in FlockingInteraction('flocking', target='flock').radii]
['mean', 'sum', 'mean']
Source code in src/skcomplex/systems/biology/collective_behaviour/_flocks.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
def __init__(self, label: str, *, target: Annotated[str, RoleSpec()], phase: str | None = None) -> None:
    super().__init__(
        label,
        target=target,
        write=('vx', 'vy'),
        form='agent',
        radii=(
            RadiusSpec('radius', self._get_cohesion, reduce='mean'),
            RadiusSpec('separation_radius', self._get_separation),
            RadiusSpec('radius', self._get_alignment, reduce='mean'),
        ),
        clamp=ClampSpec(min_norm='min_speed', max_norm='max_speed'),
        phase=phase,
    )

GroundElement(label, visual=None, *, G=0.0, V=0.0, T=200.0, D=0.5, tau=1.0)

Bases: Element

Ground whose structure G is raised by footprints and recovers, with a trail potential V that smooths it.

The structure relaxes back to bare ground as G' = -G / T, and walkers add footprints. The trail potential follows V' = D ΔV + (G - V) / tau, so it settles to the structure smoothed over a length sqrt(D tau), the distance from which walkers notice a trail. The element holds the states and their parameters, and the system adds the fading, the diffusion, and the relaxation as interactions.

Parameters:

Name Type Description Default
label str

The label of the ground or the lattice field.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
G Annotated[float, StateSpec(domain=closed_open(0, oo))]

The initial ground structure.

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

The initial trail potential.

0.0
T Annotated[float, ParameterSpec(domain=open(0, oo))]

The durability of trails, the time over which they fade.

200.0
D Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The diffusion of the trail potential.

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

The relaxation time of the trail potential.

1.0

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import GroundElement
>>> GroundElement('ground').T
200.0
Source code in src/skcomplex/systems/biology/collective_behaviour/_trails.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    G: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    V: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    T: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 200.0,
    D: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
    tau: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.G = G
    self.V = V
    self.T = T
    self.D = D
    self.tau = tau

HeadingInteraction(label, *, target, phase=None)

Bases: MapInteraction

Steering of the velocity of every boid by cohesion, separation, and alignment, with a bounded speed.

The velocity of a boid gains the mean offset to the boids within radius weighted by cohesion, loses the summed offset to the boids within separation_radius weighted by separation, and gains the mean difference of their velocities from its own weighted by alignment. Its speed is then clipped between min_speed and max_speed. Offsets are the shortest ones of the space. It is the agent form of a map of the velocity under the 'member' schedule.

Parameters:

Name Type Description Default
label str

The label of the interaction, unique among the parts of its system.

required
target Annotated[str, RoleSpec()]

The path of the group of boids.

required
phase str | None

The phase of the step the steering runs in, or None for the default phase.

None

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import HeadingInteraction
>>> HeadingInteraction('heading', target='flock').schedule
'member'
Source code in src/skcomplex/systems/biology/collective_behaviour/_flocks.py
356
357
358
359
360
361
362
363
364
365
def __init__(self, label: str, *, target: Annotated[str, RoleSpec()], phase: str | None = None) -> None:
    super().__init__(
        label,
        target=target,
        state='velocity',
        function=self._steer,
        schedule='member',
        form='agent',
        phase=phase,
    )

HerdedSheepElement(label, visual=VisualSpec('agents', {}), *, hx=0.0, hy=0.0)

Bases: Element

Sheep of a herded flock, an agent with a heading that a shepherding interaction moves.

Parameters:

Name Type Description Default
label str

The label of the sheep or of the flock.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

VisualSpec('agents', {})
hx Annotated[float, StateSpec()]

The first component of the heading of the last move.

0.0
hy Annotated[float, StateSpec()]

The second component of the heading of the last move.

0.0

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import HerdedSheepElement
>>> HerdedSheepElement('sheep', hx=1.0).hx
1.0
Source code in src/skcomplex/systems/biology/collective_behaviour/_herding.py
56
57
58
59
60
61
62
63
64
65
66
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {}),
    *,
    hx: Annotated[float, StateSpec()] = 0.0,
    hy: Annotated[float, StateSpec()] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.hx = hx
    self.hy = hy

MorsePotentialInteraction(label, first, second, cutoff=1.0, position='x', *, period=None, C_a=1.0, l_a=2.0, C_r=2.0, l_r=0.5)

Bases: PairCouplingInteraction

Attraction at long range and repulsion at short range, C_r exp(-r / l_r) - C_a exp(-r / l_a).

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
first str

The path of the group or particle of the first members.

required
second str

The path of the group or particle of the second members.

required
cutoff float

The distance beyond which pairs do not interact.

1.0
position str

The name of the position state of the members.

'x'
period float | None

The side of a periodic box, for minimum image distances, or None for an open space.

None
C_a Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The strength of the attraction.

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

The range of the attraction.

2.0
C_r Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The strength of the repulsion.

2.0
l_r Annotated[float, ParameterSpec(domain=open(0, oo))]

The range of the repulsion.

0.5

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import MorsePotentialInteraction
>>> MorsePotentialInteraction('flocking', first='birds', second='birds', cutoff=10.0).l_a
2.0
Source code in src/skcomplex/systems/biology/collective_behaviour/_flocks.py
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
def __init__(
    self,
    label: str,
    first: str,
    second: str,
    cutoff: float = 1.0,
    position: str = 'x',
    *,
    period: float | None = None,
    C_a: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    l_a: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 2.0,
    C_r: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 2.0,
    l_r: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.5,
) -> None:
    super().__init__(
        label,
        first,
        second,
        cutoff=cutoff,
        position=position,
        potential=self._get_pair_energy,
        period=period,
    )
    self.C_a = C_a
    self.l_a = l_a
    self.C_r = C_r
    self.l_r = l_r

PheromoneElement(label, visual=None, *, level=0.0, D=0.05, evaporation=0.05)

Bases: Element

Pheromone of one cell of the ground, a level that diffuses to the neighbouring cells and evaporates.

The element holds the level and its parameters. The system adds the diffusion D laplacian(level) and the evaporation -evaporation level as interactions.

Parameters:

Name Type Description Default
label str

The label of the element or the lattice field.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
level Annotated[object, StateSpec(domain=closed_open(0, oo))]

The initial level of pheromone.

0.0
D Annotated[object, ParameterSpec(domain=closed_open(0, oo))]

The diffusion coefficient of the pheromone.

0.05
evaporation Annotated[object, ParameterSpec(domain=closed_open(0, oo))]

The rate at which the pheromone evaporates.

0.05

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import PheromoneElement
>>> PheromoneElement('pheromone').evaporation
0.05
Source code in src/skcomplex/systems/biology/collective_behaviour/_foraging.py
61
62
63
64
65
66
67
68
69
70
71
72
73
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    level: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    D: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.05,
    evaporation: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.05,
) -> None:
    super().__init__(label, visual=visual)
    self.level = level
    self.D = D
    self.evaporation = evaporation

RatsElement(label, visual=None, *, n=0.0)

Bases: CompartmentElement

Rats in one behavioural state, calm or nervous, counted by the state n.

Parameters:

Name Type Description Default
label str

The label of the state.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
n Annotated[float, StateSpec(domain=closed_open(0, oo))]

The initial number of rats.

0.0

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import RatsElement
>>> RatsElement('calm', n=90.0).n
90.0
Source code in src/skcomplex/systems/biology/collective_behaviour/_decisions.py
71
72
73
74
75
76
77
78
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    n: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
) -> None:
    super().__init__(label, visual=visual, n=n)

SchoolingInteraction(label, *, target, phase=None)

Bases: CouplingInteraction

Couzin's turn of every fish toward a desired direction set by zones around it, by at most max_turn.

When neighbours lie within the zone of repulsion alpha, the desired direction points away from them, and repulsion overrides the other zones. Otherwise it points toward the neighbours within the zone of interaction rho and along their headings and the fish's own, and an informed fish adds its preferred direction with the weight omega. A fish with no neighbour keeps its heading. Offsets are the shortest ones on the torus of side size. It is the agent form of coupling over two radii of different priorities, with a clamp of the turn.

Parameters:

Name Type Description Default
label str

The label of the interaction, unique among the parts of its system.

required
target Annotated[str, RoleSpec()]

The path of the group of fish.

required
phase str | None

The phase of the step the turn runs in, or None for the default phase.

None

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import SchoolingInteraction
>>> [spec.priority for spec in SchoolingInteraction('schooling', target='shoal').radii]
[1, 0, 0]
Source code in src/skcomplex/systems/biology/collective_behaviour/_leadership.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def __init__(self, label: str, *, target: Annotated[str, RoleSpec()], phase: str | None = None) -> None:
    super().__init__(
        label,
        target=target,
        write=('vx', 'vy'),
        form='agent',
        combine=self._get_desired,
        radii=(
            RadiusSpec('alpha', self._get_repulsion, priority=1),
            RadiusSpec('rho', self._get_attraction),
            RadiusSpec('rho', self._get_alignment),
        ),
        clamp=ClampSpec(max_angle='max_turn'),
        phase=phase,
    )

SelfPropulsionInteraction(label, *, target, alpha=1.0, beta=0.5)

Bases: RateInteraction

Rayleigh self-propulsion, v' = (alpha - beta |v|²) v, which drives points to the speed sqrt(alpha / beta).

It is a rate on the position x, a state of order 2, whose velocity v is the first derivative x_d1.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the particle or group.

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

The propulsion.

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

The friction at high speed.

0.5

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import SelfPropulsionInteraction
>>> SelfPropulsionInteraction('flight', target='birds', alpha=1.5).alpha
1.5
Source code in src/skcomplex/systems/biology/collective_behaviour/_flocks.py
489
490
491
492
493
494
495
496
497
498
499
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    alpha: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    beta: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.5,
) -> None:
    super().__init__(label, target=target, state='x')
    self.alpha = alpha
    self.beta = beta

ShepherdElement(label, visual=VisualSpec('agents', {}), *, driving=0.0)

Bases: Element

Shepherd of a flock, an agent whose mode records whether it collects stray sheep or drives the flock.

Parameters:

Name Type Description Default
label str

The label of the shepherd or of the group of shepherds.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

VisualSpec('agents', {})
driving Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

1 while the shepherd drives the flock and 0 while it collects a stray sheep.

0.0

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import ShepherdElement
>>> ShepherdElement('dog').driving
0.0
Source code in src/skcomplex/systems/biology/collective_behaviour/_herding.py
102
103
104
105
106
107
108
109
110
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {}),
    *,
    driving: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.driving = driving

ShepherdingInteraction(label, *, sheep, shepherds, goal=(0.0, 0.0), ra=2.0, rs=65.0, n=20, weights=(0.5, 1.05, 2.0, 1.0, 0.3), delta=1.0, delta_s=1.5, graze=0.05)

Bases: Interaction

Herding of a flock towards a goal by a shepherd that collects stray sheep and drives the flock from behind.

Following Strömbom et al. (2014), a sheep within rs of the shepherd moves the distance delta along h H + c C + rho_a R_a + rho_s R_s + e E, of unit vectors: its last heading H, the attraction C to the centre of its n nearest neighbours, the repulsion R_a from the sheep closer than ra, the repulsion R_s from the shepherd, and noise E. A sheep far from the shepherd grazes, a random step with the probability graze, and is repelled by close sheep. The shepherd collects the sheep furthest from the centre of the flock once it is further than ra N^(2/3), heading to the point ra behind it, and otherwise drives the flock from the point ra sqrt(N) behind its centre, opposite the goal. It moves at the speed delta_s with noise, and stands still within 3 ra of a sheep.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
sheep Annotated[str, RoleSpec()]

The path of the flock.

required
shepherds Annotated[str, RoleSpec()]

The path of the group of the shepherd.

required
goal Sequence[float]

The position of the goal.

(0.0, 0.0)
ra float

The distance of repulsion between sheep.

2.0
rs float

The distance at which sheep detect the shepherd.

65.0
n int

The number of nearest neighbours whose centre attracts a sheep.

20
weights Sequence[float]

The weights (h, c, rho_a, rho_s, e) of the heading, the attraction, the two repulsions, and the noise.

(0.5, 1.05, 2.0, 1.0, 0.3)
delta float

The step of a sheep.

1.0
delta_s float

The step of the shepherd.

1.5
graze float

The probability of a grazing step.

0.05

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import ShepherdingInteraction
>>> ShepherdingInteraction('herding', sheep='sheep', shepherds='dogs').rs
65.0
Source code in src/skcomplex/systems/biology/collective_behaviour/_herding.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
def __init__(
    self,
    label: str,
    *,
    sheep: Annotated[str, RoleSpec()],
    shepherds: Annotated[str, RoleSpec()],
    goal: Sequence[float] = (0.0, 0.0),
    ra: float = 2.0,
    rs: float = 65.0,
    n: int = 20,
    weights: Sequence[float] = (0.5, 1.05, 2.0, 1.0, 0.3),
    delta: float = 1.0,
    delta_s: float = 1.5,
    graze: float = 0.05,
) -> None:
    super().__init__(label)
    self.sheep = sheep
    self.shepherds = shepherds
    self.goal = goal
    self.ra = ra
    self.rs = rs
    self.n = n
    self.weights = weights
    self.delta = delta
    self.delta_s = delta_s
    self.graze = graze

herd(system)

Move every sheep and every shepherd from the positions at the start of the step.

Parameters:

Name Type Description Default
system SystemView

The system.

required
Source code in src/skcomplex/systems/biology/collective_behaviour/_herding.py
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
def herd(self, system: SystemView) -> None:
    """Move every sheep and every shepherd from the positions at the start of the step.

    Args:
        system:
            The system.
    """
    flock = system.agents(self.sheep, HerdedSheepView)
    dogs = system.agents(self.shepherds, ShepherdView)
    sheep = np.array([np.asarray(item.location, dtype=float) for item in flock])
    shepherds = np.array([np.asarray(item.location, dtype=float) for item in dogs])
    headings = np.array([[item.hx, item.hy] for item in flock])
    moved, headings = self._move_sheep(sheep, shepherds, headings, system.rng)
    targets, driving = self._targets(sheep, shepherds)
    steps = self._unit(targets - shepherds) + 0.3 * self._unit(system.rng.normal(size=shepherds.shape))
    distances = np.linalg.norm(sheep[None, :, :] - shepherds[:, None, :], axis=2)
    blocked = distances.min(axis=1) < 3 * self.ra
    shepherds = shepherds + np.where(blocked[:, None], 0.0, self.delta_s * self._unit(steps))
    width, height = system.dimensions or (np.inf, np.inf)
    for sheep_view, position, heading in zip(flock, moved, headings, strict=True):
        sheep_view.move_to(self._inside(position, width, height))
        sheep_view.hx, sheep_view.hy = float(heading[0]), float(heading[1])
    for dog, position, mode in zip(dogs, shepherds, driving, strict=True):
        dog.move_to(self._inside(position, width, height))
        dog.driving = float(mode)

ShoalMemberElement(label, visual=VisualSpec('agents', {'color_by': 'omega'}), *, vx=1.0, vy=0.0, omega=0.0, deviation=0.0, preferred=0.0, alpha=1.0, rho=6.0, speed=0.3, max_turn=0.4, noise=0.05, size=100.0)

Bases: Element

Fish of a Couzin shoal that avoids close neighbours, follows farther ones, and may know a preferred direction.

Every step SchoolingInteraction turns a fish toward its desired direction by at most max_turn. When neighbours lie within the zone of repulsion alpha, it points away from them. Otherwise it points toward the neighbours within the zone of interaction rho and along their headings and its own. An informed fish adds its preferred direction with the weight omega, while a naive fish has omega = 0. It then draws a Gaussian angular noise deviation of standard deviation noise, and SwimmingInteraction turns it by the noise and moves it at the speed speed.

Parameters:

Name Type Description Default
label str

The label of the fish or the group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'omega'})
vx Annotated[float, StateSpec(role=velocity)]

The horizontal component of the unit heading.

1.0
vy Annotated[float, StateSpec(role=velocity)]

The vertical component of the unit heading.

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

The weight of the preferred direction, 0 for a naive fish.

0.0
deviation Annotated[float, StateSpec()]

The angular noise of the last turn.

0.0
preferred Annotated[float, ParameterSpec()]

The angle of the preferred direction of the informed fish.

0.0
alpha Annotated[float, ParameterSpec(domain=open(0, oo))]

The radius of the zone of repulsion.

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

The radius of the zone of interaction.

6.0
speed Annotated[float, ParameterSpec(domain=open(0, oo))]

The distance a fish swims per step.

0.3
max_turn Annotated[float, ParameterSpec(domain=open(0, oo))]

The largest turning angle per step.

0.4
noise Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The standard deviation of the angular noise.

0.05
size Annotated[float, ParameterSpec(domain=open(0, oo))]

The side of the square torus.

100.0

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import ShoalMemberElement
>>> GroupElement('shoal', element=ShoalMemberElement, size=10, location=(5.0, 5.0)).size
10
Source code in src/skcomplex/systems/biology/collective_behaviour/_leadership.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'omega'}),
    *,
    vx: Annotated[float, StateSpec(role='velocity')] = 1.0,
    vy: Annotated[float, StateSpec(role='velocity')] = 0.0,
    omega: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    deviation: Annotated[float, StateSpec()] = 0.0,
    preferred: Annotated[float, ParameterSpec()] = 0.0,
    alpha: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
    rho: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 6.0,
    speed: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.3,
    max_turn: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.4,
    noise: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.05,
    size: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 100.0,
) -> None:
    super().__init__(label, visual=visual)
    self.vx = vx
    self.vy = vy
    self.omega = omega
    self.deviation = deviation
    self.preferred = preferred
    self.alpha = alpha
    self.rho = rho
    self.speed = speed
    self.max_turn = max_turn
    self.noise = noise
    self.size = size

SpatialBoidElement(label, visual=VisualSpec('agents'), *, velocity=0.0, radius=5.0, separation_radius=1.0, cohesion=0.01, separation=0.1, alignment=0.1, min_speed=0.5, max_speed=1.0)

Bases: Element

Boid in three dimensions, a bird that steers by the velocity and the position of the boids it sees.

Every step HeadingInteraction steers a boid by cohesion, separation, and alignment with the boids within radius, with its speed between min_speed and max_speed, and SpatialFlightInteraction moves it along its velocity.

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')
velocity Annotated[ArrayLike, StateSpec(vector=True, role=velocity)]

The initial velocity, one component per dimension of the space.

0.0
radius Annotated[float, ParameterSpec(domain=open(0, oo))]

The distance within which a boid sees others.

5.0
separation_radius Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The distance within which a boid avoids others.

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

The weight of cohesion.

0.01
separation Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The weight of separation.

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

The weight of alignment.

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

The smallest speed.

0.5
max_speed Annotated[float, ParameterSpec(domain=open(0, oo))]

The largest speed.

1.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.biology.collective_behaviour import SpatialBoidElement
>>> GroupElement('flock', element=SpatialBoidElement, size=10).size
10
Source code in src/skcomplex/systems/biology/collective_behaviour/_flocks.py
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents'),
    *,
    velocity: Annotated[npt.ArrayLike, StateSpec(vector=True, role='velocity')] = 0.0,
    radius: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 5.0,
    separation_radius: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    cohesion: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.01,
    separation: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
    alignment: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
    min_speed: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
    max_speed: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.velocity = velocity
    self.radius = radius
    self.separation_radius = separation_radius
    self.cohesion = cohesion
    self.separation = separation
    self.alignment = alignment
    self.min_speed = min_speed
    self.max_speed = max_speed

SpatialFlightInteraction(label, *, target, probability=1.0)

Bases: MovementInteraction

Flight of every boid by its velocity in one step, with the boundary of its space acting on the new position.

Every boid moves in its own activation, after the rules and member interactions listed before it.

Parameters:

Name Type Description Default
label str

The label of the interaction, unique among the parts of its system.

required
target Annotated[str, RoleSpec()]

The path of the group of boids.

required
probability Annotated[float, ParameterSpec(domain=closed(0, 1))]

The probability that a boid moves in a step.

1.0

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import SpatialFlightInteraction
>>> SpatialFlightInteraction('flight', target='flock').schedule
'member'
Source code in src/skcomplex/systems/biology/collective_behaviour/_flocks.py
537
538
539
540
541
542
543
544
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    probability: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 1.0,
) -> None:
    super().__init__(label, target=target, step=self._fly, probability=probability, schedule='member')

SwimmingInteraction(label, *, target, probability=1.0, phase=None)

Bases: MovementInteraction

Swim of every fish by speed along its heading, turned first by its angular noise deviation.

The heading (vx, vy) stays a unit vector, and the fish moves on the torus of side size.

Parameters:

Name Type Description Default
label str

The label of the interaction, unique among the parts of its system.

required
target Annotated[str, RoleSpec()]

The path of the group of fish.

required
probability Annotated[float, ParameterSpec(domain=closed(0, 1))]

The probability that a fish swims in a step.

1.0
phase str | None

The phase of the step the swim runs in, or None for the default phase.

None

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import SwimmingInteraction
>>> SwimmingInteraction('swimming', target='shoal', phase='swim').phase
'swim'
Source code in src/skcomplex/systems/biology/collective_behaviour/_leadership.py
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    probability: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 1.0,
    phase: str | None = None,
) -> None:
    super().__init__(
        label,
        target=target,
        step=self._swim,
        probability=probability,
        schedule='member',
        phase=phase,
    )

TrailChoiceInteraction(label, *, chosen, other, flux=1.0, k=20.0, h=2.0, stochastic=True)

Bases: ReactionInteraction

Choice of a branch by arriving ants, with Deneubourg's probability (k + n)^h / ((k + n)^h + (k + n')^h).

Ants arrive at the fork at rate flux, and each takes the chosen branch with a probability that grows with its marks n against the marks n' of the other branch, and marks it. It is a ReactionInteraction that produces one mark on the chosen branch at that propensity and reads the other branch as a modifier, a rate equation, or in stochastic mode a random event per ant.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
chosen Annotated[str, RoleSpec()]

The path of the branch this choice marks.

required
other Annotated[str, RoleSpec()]

The path of the competing branch.

required
flux Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The rate at which ants arrive at the fork.

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

The attractiveness of an unmarked branch.

20.0
h Annotated[float, ParameterSpec(domain=open(0, oo))]

The nonlinearity of the choice.

2.0
stochastic bool

Whether ants are random events.

True

Examples:

>>> from skcomplex.core.components.system import System
>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.biology.collective_behaviour import BranchElement, TrailChoiceInteraction
>>> choice = TrailChoiceInteraction('choice', chosen='left', other='right', stochastic=False)
>>> system = System(elements=[BranchElement('left'), BranchElement('right')], interactions=[choice])
>>> trajectory = Simulation(system, dt=0.1).prepare().run(until=2.0).trajectory_
>>> round(float(trajectory.values('left.n')[-1]), 3)
1.025
Source code in src/skcomplex/systems/biology/collective_behaviour/_decisions.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def __init__(
    self,
    label: str,
    *,
    chosen: Annotated[str, RoleSpec()],
    other: Annotated[str, RoleSpec()],
    flux: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    k: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 20.0,
    h: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 2.0,
    stochastic: bool = True,
) -> None:
    super().__init__(label)
    self.chosen = chosen
    self.other = other
    self.flux = flux
    self.k = k
    self.h = h
    self.stochastic = stochastic

TrailSegmentElement(label, visual=None, *, tau=1.0, passable=1.0)

Bases: Element

Segment of a road network between two points, with the pheromone ants left on it and whether it is open.

Parameters:

Name Type Description Default
label str

The label of the segment or of the group of segments.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
tau Annotated[float, StateSpec(domain=closed_open(0, oo))]

The pheromone level.

1.0
passable Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

1 while the segment can be walked and 0 once it is cut.

1.0

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import TrailSegmentElement
>>> TrailSegmentElement('segment').tau
1.0
Source code in src/skcomplex/systems/biology/collective_behaviour/_colony_optimization.py
64
65
66
67
68
69
70
71
72
73
74
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    tau: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    passable: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.tau = tau
    self.passable = passable

WalkerElement(label, visual=VisualSpec('agents', {'color_by': 'goal_x'}), *, goal_x=0.0, goal_y=0.0, arrived=0.0, directedness=3.0, attraction=1.0, intensity=1.0, G_max=5.0, width=30.0, height=30.0)

Bases: Element

Pedestrian who walks toward a goal, drawn to existing trails, and leaves footprints that build them.

The walker heads along the unit vector toward its goal plus attraction times the gradient of the trail potential V, after Helbing, Keltsch, and Molnár. Every step it moves to a neighbouring cell with a probability that grows with exp(directedness * cos angle), where the angle is between the step and the heading, and adds the footprint intensity (1 - G / G_max) to the ground. The step after it arrives at its goal it leaves the green, and a new walker enters there with another entry point as goal, drawn at random, a renewal in place. The system adds the step, the footprint, the renewal, and the arrival as member interactions.

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': 'goal_x'})
goal_x Annotated[float, StateSpec()]

The column of the goal.

0.0
goal_y Annotated[float, StateSpec()]

The row of the goal.

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

Whether the walker stood at its goal at the end of the previous step, 1 or 0.

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

How strongly the walker heads for its goal.

3.0
attraction Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

How strongly trails draw the walker.

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

The footprint on bare ground.

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

The largest ground structure.

5.0
width Annotated[float, ParameterSpec(domain=open(0, oo))]

The number of columns of the grid.

30.0
height Annotated[float, ParameterSpec(domain=open(0, oo))]

The number of rows of the grid.

30.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.biology.collective_behaviour import WalkerElement
>>> GroupElement('walkers', element=WalkerElement, size=4, location=(0, 0)).size
4
Source code in src/skcomplex/systems/biology/collective_behaviour/_trails.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'goal_x'}),
    *,
    goal_x: Annotated[float, StateSpec()] = 0.0,
    goal_y: Annotated[float, StateSpec()] = 0.0,
    arrived: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0.0,
    directedness: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 3.0,
    attraction: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    intensity: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    G_max: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 5.0,
    width: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 30.0,
    height: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 30.0,
) -> None:
    super().__init__(label, visual=visual)
    self.goal_x = goal_x
    self.goal_y = goal_y
    self.arrived = arrived
    self.directedness = directedness
    self.attraction = attraction
    self.intensity = intensity
    self.G_max = G_max
    self.width = width
    self.height = height

active_walkers(size=30, *, n_walkers=20, attraction=1.0, T=200.0, random_state=0)

Return active walkers who walk between the corners of a green and wear a system of trails into it.

Walkers prefer existing trails to the direct way, and trails fade unless they are used, so a few shared trails emerge that bundle the ways between the corners, a compromise between short and comfortable routes.

Parameters:

Name Type Description Default
size int

The number of rows and columns of the green.

30
n_walkers int

The number of walkers.

20
attraction float

How strongly trails draw the walkers.

1.0
T float

The durability of trails.

200.0
random_state int | Generator | None

The seed or generator of the starting corners and goals.

0

Returns:

Name Type Description
system System

The system, with the walkers people/walkers, the ground structure land/ground.G, and the trail potential land/ground.V.

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import active_walkers
>>> [child.label for child in active_walkers().systems]
['people', 'land']
Source code in src/skcomplex/systems/biology/collective_behaviour/_trails.py
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
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
@register_system(
    field='biology/collective_behaviour',
    tags=('agents', 'field', 'trail formation', 'co-simulation'),
    references=('Helbing, Keltsch, and Molnár 1997', 'arXiv:cond-mat/0012229'),
    defaults={'until': 300, 'dt': 1},
)
def active_walkers(
    size: int = 30,
    *,
    n_walkers: int = 20,
    attraction: float = 1.0,
    T: float = 200.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return active walkers who walk between the corners of a green and wear a system of trails into it.

    Walkers prefer existing trails to the direct way, and trails fade unless they are used, so a few shared trails
    emerge that bundle the ways between the corners, a compromise between short and comfortable routes.

    Args:
        size:
            The number of rows and columns of the green.

        n_walkers:
            The number of walkers.

        attraction:
            How strongly trails draw the walkers.

        T:
            The durability of trails.

        random_state:
            The seed or generator of the starting corners and goals.

    Returns:
        system:
            The system, with the walkers `people/walkers`, the ground structure `land/ground.G`, and the trail potential
            `land/ground.V`.

    Examples:
        >>> from skcomplex.systems.biology.collective_behaviour import active_walkers
        >>> [child.label for child in active_walkers().systems]
        ['people', 'land']
    """
    rng = np.random.default_rng(random_state)
    corners = _corners(size, size)
    starts = rng.integers(len(corners), size=n_walkers)
    goals = [corners[(start + 1 + int(rng.integers(len(corners) - 1))) % len(corners)] for start in starts]
    walkers = GroupElement(
        'walkers',
        element=WalkerElement,
        size=n_walkers,
        values={
            'goal_x': np.array([float(goal[0]) for goal in goals]),
            'goal_y': np.array([float(goal[1]) for goal in goals]),
            'attraction': attraction,
            'width': float(size),
            'height': float(size),
        },
        location=[corners[start] for start in starts],
    )
    ground = FieldElement(
        'ground',
        element=GroundElement,
        space=LatticeSpace((size, size), boundary='reflecting'),
        values={'T': T},
    )
    return System(
        systems=[
            System('people', elements=[walkers], space=LatticeSpace((size, size), boundary='fixed')),
            System(
                'land',
                elements=[ground],
                interactions=[
                    RelaxationInteraction('fading', target='ground', state='G', time=_get_durability),
                    DiffusionInteraction('spreading', target='ground', state='V', coefficient=_get_diffusion),
                    RelaxationInteraction(
                        'smoothing',
                        target='ground',
                        state='V',
                        goal=_get_structure,
                        time=_get_relaxation,
                    ),
                ],
            ),
        ],
        interactions=[
            MovementInteraction('walk', target='people/walkers', step=_get_step, condition=_walks, schedule='member'),
            TransferInteraction(
                'footprints',
                source='people/walkers',
                target='land/ground',
                state=Source(),
                into='G',
                amount=_get_footprint,
                pairing='site',
                guard=_steps,
                form='agent',
                schedule='member',
            ),
            LifeCycleInteraction(
                'renewal',
                target='people/walkers',
                death=_waits,
                renew=True,
                values=_get_goal,
                schedule='member',
            ),
            MapInteraction(
                'arrival',
                target='people/walkers',
                state='arrived',
                function=_get_arrived,
                form='agent',
                schedule='member',
            ),
        ],
    )

ant_colony_optimization(n_points=40, *, ants=20, steps=80, alpha=0.8, beta=1.0, rho=0.5, Q=1.0, cut=None, seed=0)

Return ants that find the shortest path between two points of a Delaunay road network, round after round.

Every round the ants walk from the point nearest one corner of the unit square towards the point nearest the opposite corner, choosing segments by their pheromone and length, and mark the paths they find with pheromone in inverse proportion to their length, while all pheromone evaporates. Short paths gather pheromone fastest, so the colony converges on a short path, usually the shortest, as in exercise 15.7 of Argun et al. (2021). Cutting the best path at the round cut makes the colony explore again and settle on a new route, exercise 15.12.

Parameters:

Name Type Description Default
n_points int

The number of points of the network.

40
ants int

The number of ants of a round.

20
steps int

The largest number of steps of an ant in a round.

80
alpha float

The exponent of the pheromone.

0.8
beta float

The exponent of the weight 1 / d of a segment.

1.0
rho float

The evaporation rate.

0.5
Q float

The pheromone an ant lays along a path.

1.0
cut int | None

The round at which the best path is cut, or None for none.

None
seed int | None

The seed of the points.

0

Returns:

Name Type Description
system System

The system, with the pheromone roads.tau and the open segments roads.passable of the agent group roads, and the shortest path of every round search.shortest.

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import ant_colony_optimization
>>> ant_colony_optimization(10).interactions[0].ants
20
Source code in src/skcomplex/systems/biology/collective_behaviour/_colony_optimization.py
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
@register_system(
    field='biology/collective_behaviour',
    tags=('ant colony optimization', 'stigmergy', 'shortest path', 'agents'),
    references=('Dorigo 1992', 'Argun et al. 2021'),
    defaults={'until': 100},
)
def ant_colony_optimization(
    n_points: int = 40,
    *,
    ants: int = 20,
    steps: int = 80,
    alpha: float = 0.8,
    beta: float = 1.0,
    rho: float = 0.5,
    Q: float = 1.0,
    cut: int | None = None,
    seed: int | None = 0,
) -> System:
    """Return ants that find the shortest path between two points of a Delaunay road network, round after round.

    Every round the ants walk from the point nearest one corner of the unit square towards the point nearest the
    opposite corner, choosing segments by their pheromone and length, and mark the paths they find with pheromone in
    inverse proportion to their length, while all pheromone evaporates. Short paths gather pheromone fastest, so the
    colony converges on a short path, usually the shortest, as in exercise 15.7 of Argun et al. (2021). Cutting the
    best path at the round `cut` makes the colony explore again and settle on a new route, exercise 15.12.

    Args:
        n_points:
            The number of points of the network.

        ants:
            The number of ants of a round.

        steps:
            The largest number of steps of an ant in a round.

        alpha:
            The exponent of the pheromone.

        beta:
            The exponent of the weight `1 / d` of a segment.

        rho:
            The evaporation rate.

        Q:
            The pheromone an ant lays along a path.

        cut:
            The round at which the best path is cut, or `None` for none.

        seed:
            The seed of the points.

    Returns:
        system:
            The system, with the pheromone `roads.tau` and the open segments `roads.passable` of the agent group
            `roads`, and the shortest path of every round `search.shortest`.

    Examples:
        >>> from skcomplex.systems.biology.collective_behaviour import ant_colony_optimization
        >>> ant_colony_optimization(10).interactions[0].ants
        20
    """
    points = np.random.default_rng(seed).random((n_points, 2))
    triangles = Delaunay(points).simplices
    pairs = {(int(min(a, b)), int(max(a, b))) for triangle in triangles for a, b in itertools.combinations(triangle, 2)}
    edges = sorted(pairs)
    lengths = [float(np.linalg.norm(points[a] - points[b])) for a, b in edges]
    start = int(np.argmin(np.linalg.norm(points, axis=1)))
    target = int(np.argmin(np.linalg.norm(points - 1.0, axis=1)))
    roads = GroupElement('roads', element=TrailSegmentElement, size=len(edges))
    search = ColonySearchInteraction(
        'search',
        segments='roads',
        edges=tuple(edges),
        lengths=tuple(lengths),
        start=start,
        target=target,
        ants=ants,
        steps=steps,
        alpha=alpha,
        beta=beta,
        rho=rho,
        Q=Q,
        cut=cut,
    )
    return System(elements=[roads], interactions=[search], time=DiscreteTime())

binary_ant_decision(*, flux=1.0, k=20.0, h=2.0, evaporation=0.0, stochastic=True)

Return ants that choose between two equal branches to a food source, following the marks of earlier ants.

Every ant reinforces the branch it takes, so the first random fluctuations grow and most ants end up on one branch, the symmetry breaking of the double bridge experiment. The deterministic mode stays symmetric.

Parameters:

Name Type Description Default
flux float

The rate at which ants arrive at the fork.

1.0
k float

The attractiveness of an unmarked branch.

20.0
h float

The nonlinearity of the choice.

2.0
evaporation float

The rate at which each mark evaporates.

0.0
stochastic bool

Whether ants and evaporation are random events.

True

Returns:

Name Type Description
system System

The system, with the marks left.n and right.n.

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import binary_ant_decision
>>> [branch.label for branch in binary_ant_decision().elements]
['left', 'right']
Source code in src/skcomplex/systems/biology/collective_behaviour/_decisions.py
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
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
@register_system(
    field='biology/collective_behaviour',
    tags=('stigmergy', 'collective decision', 'symmetry breaking', 'jump process'),
    references=('Deneubourg et al. 1990', 'Gros 2015'),
    defaults={'until': 200.0, 'dt': 1.0},
)
def binary_ant_decision(
    *,
    flux: float = 1.0,
    k: float = 20.0,
    h: float = 2.0,
    evaporation: float = 0.0,
    stochastic: bool = True,
) -> System:
    """Return ants that choose between two equal branches to a food source, following the marks of earlier ants.

    Every ant reinforces the branch it takes, so the first random fluctuations grow and most ants end up on one
    branch, the symmetry breaking of the double bridge experiment. The deterministic mode stays symmetric.

    Args:
        flux:
            The rate at which ants arrive at the fork.

        k:
            The attractiveness of an unmarked branch.

        h:
            The nonlinearity of the choice.

        evaporation:
            The rate at which each mark evaporates.

        stochastic:
            Whether ants and evaporation are random events.

    Returns:
        system:
            The system, with the marks `left.n` and `right.n`.

    Examples:
        >>> from skcomplex.systems.biology.collective_behaviour import binary_ant_decision
        >>> [branch.label for branch in binary_ant_decision().elements]
        ['left', 'right']
    """
    branches = [BranchElement(label) for label in ('left', 'right')]
    evaporations = [
        ReactionInteraction(
            f'evaporation_{label}',
            reactants={(label, 'n'): 1},
            k=evaporation,
            mode=MODES[stochastic],
        )
        for label in ('left', 'right')
    ]
    choices = [
        TrailChoiceInteraction(
            f'choice_{chosen}',
            chosen=chosen,
            other=other,
            flux=flux,
            k=k,
            h=h,
            stochastic=stochastic,
        )
        for chosen, other in (('left', 'right'), ('right', 'left'))
    ]
    return System(elements=branches, interactions=[*evaporations, *choices])

bird_swarm(n_birds=30, *, alpha=1.0, beta=0.5, C_a=1.0, l_a=2.0, C_r=2.0, l_r=0.5, random_state=0)

Return a swarm of self-propelled birds held together by a pair potential that attracts far and repels near.

Every bird is driven to the speed sqrt(alpha / beta) and feels the Morse-type potential of every other bird, so the swarm stays together and organizes into a flock or a rotating mill.

Parameters:

Name Type Description Default
n_birds int

The number of birds.

30
alpha float

The propulsion.

1.0
beta float

The friction at high speed.

0.5
C_a float

The strength of the attraction.

1.0
l_a float

The range of the attraction.

2.0
C_r float

The strength of the repulsion.

2.0
l_r float

The range of the repulsion.

0.5
random_state int | Generator | None

The seed or generator of the initial positions and velocities.

0

Returns:

Name Type Description
system System

The system, with the positions birds.x and the velocities birds.x_d1.

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import bird_swarm
>>> bird_swarm(5).elements[0].prepare().size_
5
Source code in src/skcomplex/systems/biology/collective_behaviour/_flocks.py
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
@register_system(
    field='biology/collective_behaviour',
    tags=('swarms', 'particles', 'active matter', 'self-propulsion'),
    references=("D'Orsogna et al. 2006", 'Gros 2015'),
    defaults={'until': 50.0, 'dt': 0.5},
)
def bird_swarm(
    n_birds: int = 30,
    *,
    alpha: float = 1.0,
    beta: float = 0.5,
    C_a: float = 1.0,
    l_a: float = 2.0,
    C_r: float = 2.0,
    l_r: float = 0.5,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return a swarm of self-propelled birds held together by a pair potential that attracts far and repels near.

    Every bird is driven to the speed `sqrt(alpha / beta)` and feels the Morse-type potential of every other bird, so
    the swarm stays together and organizes into a flock or a rotating mill.

    Args:
        n_birds:
            The number of birds.

        alpha:
            The propulsion.

        beta:
            The friction at high speed.

        C_a:
            The strength of the attraction.

        l_a:
            The range of the attraction.

        C_r:
            The strength of the repulsion.

        l_r:
            The range of the repulsion.

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

    Returns:
        system:
            The system, with the positions `birds.x` and the velocities `birds.x_d1`.

    Examples:
        >>> from skcomplex.systems.biology.collective_behaviour import bird_swarm
        >>> bird_swarm(5).elements[0].prepare().size_
        5
    """
    rng = np.random.default_rng(random_state)
    positions, velocities = rng.uniform(-3.0, 3.0, (n_birds, 2)), rng.normal(0.0, 1.0, (n_birds, 2))
    birds = GroupElement('birds', element=PointElement, size=n_birds, values={'x': positions, 'x_d1': velocities})
    potential = MorsePotentialInteraction(
        'flocking',
        first='birds',
        second='birds',
        cutoff=10 * l_a,
        C_a=C_a,
        l_a=l_a,
        C_r=C_r,
        l_r=l_r,
    )
    flight = SelfPropulsionInteraction('flight', target='birds', alpha=alpha, beta=beta)
    return System(elements=[birds], interactions=[potential, flight], space=ContinuousSpace(n_dim=2))

boids(n_boids=40, *, size=30.0, radius=5.0, cohesion=0.01, separation=0.1, alignment=0.1, random_state=0)

Return Reynolds' boids in two dimensions, birds that flock by local rules without a leader.

The boids start at random positions with random headings on a torus. Alignment makes neighbours fly in the same direction and cohesion pulls them together, so the flock becomes polarized while separation keeps the birds apart.

Parameters:

Name Type Description Default
n_boids int

The number of boids.

40
size float

The side of the square torus.

30.0
radius float

The distance within which a boid sees others.

5.0
cohesion float

The weight of cohesion.

0.01
separation float

The weight of separation.

0.1
alignment float

The weight of alignment.

0.1
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 flock and the velocities vx and vy of its boids.

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import boids
>>> boids(10).elements[0].prepare().size_
10
Source code in src/skcomplex/systems/biology/collective_behaviour/_flocks.py
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
@register_system(
    field='biology/collective_behaviour',
    tags=('agents', 'flocking', 'self-organization', 'continuous space'),
    references=('Reynolds 1987', 'Downey 2018'),
    defaults={'until': 100, 'dt': 1},
)
def boids(
    n_boids: int = 40,
    *,
    size: float = 30.0,
    radius: float = 5.0,
    cohesion: float = 0.01,
    separation: float = 0.1,
    alignment: float = 0.1,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return Reynolds' boids in two dimensions, birds that flock by local rules without a leader.

    The boids start at random positions with random headings on a torus. Alignment makes neighbours fly in the same
    direction and cohesion pulls them together, so the flock becomes polarized while separation keeps the birds apart.

    Args:
        n_boids:
            The number of boids.

        size:
            The side of the square torus.

        radius:
            The distance within which a boid sees others.

        cohesion:
            The weight of cohesion.

        separation:
            The weight of separation.

        alignment:
            The weight of alignment.

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

    Returns:
        system:
            The system, with the group `flock` and the velocities `vx` and `vy` of its boids.

    Examples:
        >>> from skcomplex.systems.biology.collective_behaviour import boids
        >>> boids(10).elements[0].prepare().size_
        10
    """
    rng = np.random.default_rng(random_state)
    headings = rng.uniform(0.0, 2 * math.pi, n_boids)
    flock = GroupElement(
        'flock',
        element=BoidElement,
        size=n_boids,
        values={
            'vx': np.cos(headings),
            'vy': np.sin(headings),
            'radius': radius,
            'cohesion': cohesion,
            'separation': separation,
            'alignment': alignment,
            'width': size,
            'height': size,
        },
        location=[tuple(point) for point in rng.uniform(0.0, size, (n_boids, 2))],
    )
    interactions = [FlockingInteraction('flocking', target='flock'), FlightInteraction('flight', target='flock')]
    return System(
        elements=[flock],
        interactions=interactions,
        space=ContinuousSpace(extent=(size, size), boundary='periodic'),
    )

boids_3d(n_boids=50, *, size=20.0, radius=6.0, cohesion=0.01, separation=0.1, alignment=0.1, random_state=0)

Return boids in three dimensions, birds that flock by local rules in a periodic cube.

The boids start at random positions with random headings. Every step a boid steers by the boids within radius and then flies along its velocity, so with alignment alone the flock turns to one heading and keeps its spread in depth.

Parameters:

Name Type Description Default
n_boids int

The number of boids.

50
size float

The side of the periodic cube.

20.0
radius float

The distance within which a boid sees others.

6.0
cohesion float

The weight of cohesion.

0.01
separation float

The weight of separation.

0.1
alignment float

The weight of alignment.

0.1
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 flock and the vector state velocity of its boids.

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import boids_3d
>>> boids_3d(10).space.extent
(20.0, 20.0, 20.0)
Source code in src/skcomplex/systems/biology/collective_behaviour/_flocks.py
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
@register_system(
    field='biology/collective_behaviour',
    tags=('agents', 'flocking', 'self-organization', 'continuous space', 'three dimensions'),
    references=('Reynolds 1987', 'Downey 2018'),
    defaults={'until': 100, 'dt': 1},
)
def boids_3d(
    n_boids: int = 50,
    *,
    size: float = 20.0,
    radius: float = 6.0,
    cohesion: float = 0.01,
    separation: float = 0.1,
    alignment: float = 0.1,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return boids in three dimensions, birds that flock by local rules in a periodic cube.

    The boids start at random positions with random headings. Every step a boid steers by the boids within `radius` and
    then flies along its velocity, so with alignment alone the flock turns to one heading and keeps its spread in depth.

    Args:
        n_boids:
            The number of boids.

        size:
            The side of the periodic cube.

        radius:
            The distance within which a boid sees others.

        cohesion:
            The weight of cohesion.

        separation:
            The weight of separation.

        alignment:
            The weight of alignment.

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

    Returns:
        system:
            The system, with the group `flock` and the vector state `velocity` of its boids.

    Examples:
        >>> from skcomplex.systems.biology.collective_behaviour import boids_3d
        >>> boids_3d(10).space.extent
        (20.0, 20.0, 20.0)
    """
    rng = np.random.default_rng(random_state)
    headings = rng.normal(size=(n_boids, 3))
    flock = GroupElement(
        'flock',
        element=SpatialBoidElement,
        size=n_boids,
        values={
            'velocity': headings / np.linalg.norm(headings, axis=1, keepdims=True),
            'radius': radius,
            'cohesion': cohesion,
            'separation': separation,
            'alignment': alignment,
        },
        location=[tuple(point) for point in rng.uniform(0.0, size, (n_boids, 3))],
    )
    interactions = [
        HeadingInteraction('heading', target='flock'),
        SpatialFlightInteraction('flight', target='flock'),
    ]
    return System(
        elements=[flock],
        interactions=interactions,
        space=ContinuousSpace(extent=(size, size, size), boundary='periodic'),
    )

couzin_leadership(n_fish=50, *, informed=0.1, omega=0.5, preferred=0.0, size=100.0, random_state=0)

Return the Couzin shoal, in which a small informed minority leads a group that does not know who is informed.

The fish start in a disk of radius 5 at the center of a torus with random headings, and a fraction informed of them prefers the direction at the angle preferred with the weight omega. Repulsion, attraction, and alignment keep the shoal together, and the informed fish pull its mean heading toward their direction. The accuracy, the closeness of the group's heading to the preferred direction, grows with the size of the minority, and a minority of about 10% already steers a group of 50.

Parameters:

Name Type Description Default
n_fish int

The number of fish.

50
informed float

The fraction of informed fish.

0.1
omega float

The weight of the preferred direction of the informed fish.

0.5
preferred float

The angle of the preferred direction.

0.0
size float

The side of the square torus.

100.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 shoal, the unit headings vx and vy of its fish, and the weight omega of each, positive for the informed ones.

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import couzin_leadership
>>> int((couzin_leadership(20, informed=0.2).elements[0].values['omega'] > 0).sum())
4
Source code in src/skcomplex/systems/biology/collective_behaviour/_leadership.py
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
@register_system(
    field='biology/collective_behaviour',
    tags=('agents', 'schooling', 'leadership', 'collective decision', 'continuous space'),
    references=('Couzin et al. 2005', 'Mihailović et al. 2023'),
    defaults={'until': 300, 'dt': 1},
)
def couzin_leadership(
    n_fish: int = 50,
    *,
    informed: float = 0.1,
    omega: float = 0.5,
    preferred: float = 0.0,
    size: float = 100.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the Couzin shoal, in which a small informed minority leads a group that does not know who is informed.

    The fish start in a disk of radius 5 at the center of a torus with random headings, and a fraction `informed` of
    them prefers the direction at the angle `preferred` with the weight `omega`. Repulsion, attraction, and alignment
    keep the shoal together, and the informed fish pull its mean heading toward their direction. The accuracy, the
    closeness of the group's heading to the preferred direction, grows with the size of the minority, and a minority
    of about 10% already steers a group of 50.

    Args:
        n_fish:
            The number of fish.

        informed:
            The fraction of informed fish.

        omega:
            The weight of the preferred direction of the informed fish.

        preferred:
            The angle of the preferred direction.

        size:
            The side of the square torus.

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

    Returns:
        system:
            The system, with the group `shoal`, the unit headings `vx` and `vy` of its fish, and the weight `omega` of
            each, positive for the informed ones.

    Examples:
        >>> from skcomplex.systems.biology.collective_behaviour import couzin_leadership
        >>> int((couzin_leadership(20, informed=0.2).elements[0].values['omega'] > 0).sum())
        4
    """
    rng = np.random.default_rng(random_state)
    radius = 5.0 * np.sqrt(rng.uniform(0.0, 1.0, n_fish))
    place = rng.uniform(0.0, 2 * math.pi, n_fish)
    headings = rng.uniform(0.0, 2 * math.pi, n_fish)
    weights = np.zeros(n_fish)
    weights[: round(informed * n_fish)] = omega
    locations = size / 2 + np.column_stack([radius * np.cos(place), radius * np.sin(place)])
    shoal = GroupElement(
        'shoal',
        element=ShoalMemberElement,
        size=n_fish,
        values={'vx': np.cos(headings), 'vy': np.sin(headings), 'omega': weights, 'preferred': preferred, 'size': size},
        location=[tuple(point) for point in locations],
    )
    return System(
        elements=[shoal],
        interactions=[
            SchoolingInteraction('schooling', target='shoal', phase='turn'),
            MapInteraction(
                'deviation',
                target='shoal',
                state='deviation',
                function=_draw_deviation,
                form='agent',
                schedule='member',
                phase='turn',
            ),
            SwimmingInteraction('swimming', target='shoal', phase='swim'),
        ],
        space=ContinuousSpace(extent=(size, size), boundary='periodic'),
        time=DiscreteTime(
            schedule=PhasesScheduleSpec([PhaseSpec('turn', activation='simultaneous'), PhaseSpec('swim')]),
        ),
    )

pheromone_foraging(width=40, height=20, *, n_ants=40, D=0.05, evaporation=0.05, attraction=5.0)

Return ants that find food and lay a pheromone trail home, which other ants follow: stigmergy.

The ants live on a grid and the pheromone on a lattice of the same shape, as two partitions of a co-simulation that exchange the field and the deposits at every step. The nest is near the left edge and the food near the right.

Parameters:

Name Type Description Default
width int

The number of columns.

40
height int

The number of rows.

20
n_ants int

The number of ants, which start at the nest.

40
D float

The diffusion of the pheromone.

0.05
evaporation float

The evaporation rate of the pheromone.

0.05
attraction float

How strongly pheromone draws searching ants.

5.0

Returns:

Name Type Description
system System

The system, with the ants colony/ants and the pheromone ground/pheromone.level.

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import pheromone_foraging
>>> [child.label for child in pheromone_foraging().systems]
['colony', 'ground']
Source code in src/skcomplex/systems/biology/collective_behaviour/_foraging.py
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
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
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
@register_system(
    field='biology/collective_behaviour',
    tags=('field', 'agents', 'stigmergy', 'co-simulation'),
    references=('Argun et al. 2021', 'Carmichael and Hadžikadić 2019'),
    defaults={'until': 600, 'dt': 1},
)
def pheromone_foraging(
    width: int = 40,
    height: int = 20,
    *,
    n_ants: int = 40,
    D: float = 0.05,
    evaporation: float = 0.05,
    attraction: float = 5.0,
) -> System:
    """Return ants that find food and lay a pheromone trail home, which other ants follow: stigmergy.

    The ants live on a grid and the pheromone on a lattice of the same shape, as two partitions of a co-simulation
    that exchange the field and the deposits at every step. The nest is near the left edge and the food near the right.

    Args:
        width:
            The number of columns.

        height:
            The number of rows.

        n_ants:
            The number of ants, which start at the nest.

        D:
            The diffusion of the pheromone.

        evaporation:
            The evaporation rate of the pheromone.

        attraction:
            How strongly pheromone draws searching ants.

    Returns:
        system:
            The system, with the ants `colony/ants` and the pheromone `ground/pheromone.level`.

    Examples:
        >>> from skcomplex.systems.biology.collective_behaviour import pheromone_foraging
        >>> [child.label for child in pheromone_foraging().systems]
        ['colony', 'ground']
    """
    nest, food = (5, height // 2), (width - 6, height // 2)
    ants = GroupElement(
        'ants',
        element=AntElement,
        size=n_ants,
        values={
            'nest_x': float(nest[0]),
            'nest_y': float(nest[1]),
            'food_x': float(food[0]),
            'food_y': float(food[1]),
            'attraction': attraction,
        },
        location=nest,
    )
    pheromone = FieldElement(
        'pheromone',
        element=PheromoneElement,
        space=LatticeSpace((width, height)),
        boundary={'level': 'zero-flux'},
        values={'D': D, 'evaporation': evaporation},
    )
    return System(
        systems=[
            System('colony', elements=[ants], space=LatticeSpace((width, height), boundary='fixed')),
            System(
                'ground',
                elements=[pheromone],
                interactions=[
                    DiffusionInteraction('diffusion', target='pheromone', state='level', coefficient=_get_diffusion),
                    EvaporationInteraction('evaporation', target='pheromone', state='level'),
                ],
            ),
        ],
        interactions=[
            MovementInteraction(
                'homing',
                target='colony/ants',
                step=_get_home_step,
                condition=_homes,
                schedule='member',
            ),
            TransferInteraction(
                'marking',
                source='colony/ants',
                target='ground/pheromone',
                state=Source(),
                into='level',
                amount=_get_deposit,
                pairing='site',
                guard=_carries,
                form='agent',
                schedule='member',
            ),
            MovementInteraction(
                'search',
                target='colony/ants',
                step=_get_search_step,
                condition=_searches,
                schedule='member',
            ),
            MapInteraction(
                'carrying',
                target='colony/ants',
                state='carrying',
                function=_get_carrying,
                form='agent',
                schedule='member',
            ),
            MapInteraction(
                'arrival',
                target='colony/ants',
                state='arrived',
                function=_get_arrived,
                form='agent',
                schedule='member',
            ),
        ],
    )

rats(n_rats=100, *, nervous=5, spontaneous=0.01, contagion=2.0, calming=1.0, stochastic=True)

Return a colony of calm and nervous rats, in which nervousness spreads by contact and fades on its own.

Without spontaneous alarm the colony has a phase transition at contagion = calming: below it every rat calms down, and above it a share 1 - calming / contagion of the rats stays nervous.

Parameters:

Name Type Description Default
n_rats int

The number of rats.

100
nervous int

The initial number of nervous rats.

5
spontaneous float

The rate at which a calm rat turns nervous on its own.

0.01
contagion float

The rate at which a calm rat turns nervous when all others are nervous.

2.0
calming float

The rate at which a nervous rat calms down.

1.0
stochastic bool

Whether changes are random events.

True

Returns:

Name Type Description
system System

The system, with the numbers calm.n and nervous.n.

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import rats
>>> [group.n for group in rats().elements]
[95.0, 5.0]
Source code in src/skcomplex/systems/biology/collective_behaviour/_decisions.py
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
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
@register_system(
    field='biology/collective_behaviour',
    tags=('contagion', 'phase transition', 'jump process'),
    references=('Gros 2015',),
    defaults={'until': 20.0, 'dt': 0.1},
)
def rats(
    n_rats: int = 100,
    *,
    nervous: int = 5,
    spontaneous: float = 0.01,
    contagion: float = 2.0,
    calming: float = 1.0,
    stochastic: bool = True,
) -> System:
    """Return a colony of calm and nervous rats, in which nervousness spreads by contact and fades on its own.

    Without spontaneous alarm the colony has a phase transition at `contagion = calming`: below it every rat calms
    down, and above it a share `1 - calming / contagion` of the rats stays nervous.

    Args:
        n_rats:
            The number of rats.

        nervous:
            The initial number of nervous rats.

        spontaneous:
            The rate at which a calm rat turns nervous on its own.

        contagion:
            The rate at which a calm rat turns nervous when all others are nervous.

        calming:
            The rate at which a nervous rat calms down.

        stochastic:
            Whether changes are random events.

    Returns:
        system:
            The system, with the numbers `calm.n` and `nervous.n`.

    Examples:
        >>> from skcomplex.systems.biology.collective_behaviour import rats
        >>> [group.n for group in rats().elements]
        [95.0, 5.0]
    """
    groups = [RatsElement('calm', n=float(n_rats - nervous)), RatsElement('nervous', n=float(nervous))]
    alarm = AlarmInteraction(
        'alarm',
        calm='calm',
        nervous='nervous',
        spontaneous=spontaneous,
        contagion=contagion,
        stochastic=stochastic,
    )
    calming_reaction = ReactionInteraction(
        'calming',
        reactants={('nervous', 'n'): 1},
        products={('calm', 'n'): 1},
        k=calming,
        mode=MODES[stochastic],
    )
    return System(elements=groups, interactions=[alarm, calming_reaction])

shepherding(n_sheep=30, *, size=100.0, goal=(5.0, 5.0), seed=0)

Return a shepherd that collects a scattered flock and drives it to a goal, the heuristic of Strömbom et al.

The sheep start scattered in the middle of a square field and the shepherd behind them, opposite the goal. Sheep flee a near shepherd while they keep together and apart, and graze otherwise. The shepherd alternates between collecting the sheep furthest from the flock and driving the flock from behind towards the goal, and the zig-zag of this alternation brings the flock to the goal in a few hundred steps.

Parameters:

Name Type Description Default
n_sheep int

The number of sheep.

30
size float

The side of the square field.

100.0
goal tuple[float, float]

The position of the goal.

(5.0, 5.0)
seed int | None

The seed of the initial positions.

0

Returns:

Name Type Description
system System

The system, with the agent groups sheep and shepherds, the latter of one shepherd.

Examples:

>>> from skcomplex.systems.biology.collective_behaviour import shepherding
>>> [group.label for group in shepherding(10).elements]
['sheep', 'shepherds']
Source code in src/skcomplex/systems/biology/collective_behaviour/_herding.py
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
@register_system(
    field='biology/collective_behaviour',
    tags=('shepherding', 'herding', 'agents', 'swarm control'),
    references=('Strömbom et al. 2014', 'Long et al. 2020'),
    defaults={'until': 300},
)
def shepherding(
    n_sheep: int = 30,
    *,
    size: float = 100.0,
    goal: tuple[float, float] = (5.0, 5.0),
    seed: int | None = 0,
) -> System:
    """Return a shepherd that collects a scattered flock and drives it to a goal, the heuristic of Strömbom et al.

    The sheep start scattered in the middle of a square field and the shepherd behind them, opposite the goal. Sheep
    flee a near shepherd while they keep together and apart, and graze otherwise. The shepherd alternates between
    collecting the sheep furthest from the flock and driving the flock from behind towards the goal, and the zig-zag
    of this alternation brings the flock to the goal in a few hundred steps.

    Args:
        n_sheep:
            The number of sheep.

        size:
            The side of the square field.

        goal:
            The position of the goal.

        seed:
            The seed of the initial positions.

    Returns:
        system:
            The system, with the agent groups `sheep` and `shepherds`, the latter of one shepherd.

    Examples:
        >>> from skcomplex.systems.biology.collective_behaviour import shepherding
        >>> [group.label for group in shepherding(10).elements]
        ['sheep', 'shepherds']
    """
    rng = np.random.default_rng(seed)
    positions = rng.uniform(0.4 * size, 0.7 * size, (n_sheep, 2))
    sheep = GroupElement(
        'sheep',
        element=HerdedSheepElement,
        size=n_sheep,
        location=[tuple(map(float, position)) for position in positions],
    )
    dogs = GroupElement(
        'shepherds',
        element=ShepherdElement,
        size=1,
        location=[(0.9 * size, 0.9 * size)],
    )
    herding = ShepherdingInteraction(
        'herding',
        sheep='sheep',
        shepherds='shepherds',
        goal=goal,
        n=max(1, round(0.9 * n_sheep)),
    )
    return System(
        elements=[sheep, dogs],
        interactions=[herding],
        space=ContinuousSpace(extent=(size, size)),
        time=DiscreteTime(),
    )