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.system import System
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.models.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).check().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/models/biology/collective_behaviour/_alarm.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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 model 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.elements import GroupElement
>>> from skcomplex.models.biology.collective_behaviour import AntElement
>>> GroupElement('ants', element=AntElement, size=5, location=(0, 0)).size
5
Source code in src/skcomplex/models/biology/collective_behaviour/_ant.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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.elements import GroupElement
>>> from skcomplex.models.biology.collective_behaviour import BoidElement
>>> GroupElement('flock', element=BoidElement, size=10, location=(5.0, 5.0)).size
10
Source code in src/skcomplex/models/biology/collective_behaviour/_boid.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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.models.biology.collective_behaviour import BranchElement
>>> BranchElement('left', n=5.0).n
5.0
Source code in src/skcomplex/models/biology/collective_behaviour/_branch.py
34
35
36
37
38
39
40
41
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)

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.models.biology.collective_behaviour import FlightInteraction
>>> FlightInteraction('flight', target='flock').schedule
'member'
Source code in src/skcomplex/models/biology/collective_behaviour/_flight.py
38
39
40
41
42
43
44
45
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.models.biology.collective_behaviour import FlockingInteraction
>>> [spec.reduce for spec in FlockingInteraction('flocking', target='flock').radii]
['mean', 'sum', 'mean']
Source code in src/skcomplex/models/biology/collective_behaviour/_flocking.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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 model 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.models.biology.collective_behaviour import GroundElement
>>> GroundElement('ground').T
200.0
Source code in src/skcomplex/models/biology/collective_behaviour/_ground.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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

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.models.biology.collective_behaviour import MorsePotentialInteraction
>>> MorsePotentialInteraction('flocking', first='birds', second='birds', cutoff=10.0).l_a
2.0
Source code in src/skcomplex/models/biology/collective_behaviour/_morse_potential.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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 model 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.models.biology.collective_behaviour import PheromoneElement
>>> PheromoneElement('pheromone').evaporation
0.05
Source code in src/skcomplex/models/biology/collective_behaviour/_pheromone.py
43
44
45
46
47
48
49
50
51
52
53
54
55
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.models.biology.collective_behaviour import RatsElement
>>> RatsElement('calm', n=90.0).n
90.0
Source code in src/skcomplex/models/biology/collective_behaviour/_rats.py
34
35
36
37
38
39
40
41
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.models.biology.collective_behaviour import SchoolingInteraction
>>> [spec.priority for spec in SchoolingInteraction('schooling', target='shoal').radii]
[1, 0, 0]
Source code in src/skcomplex/models/biology/collective_behaviour/_schooling.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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.models.biology.collective_behaviour import SelfPropulsionInteraction
>>> SelfPropulsionInteraction('flight', target='birds', alpha=1.5).alpha
1.5
Source code in src/skcomplex/models/biology/collective_behaviour/_self_propulsion.py
40
41
42
43
44
45
46
47
48
49
50
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

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.models.biology.collective_behaviour import ShoalMemberElement
>>> GroupElement('shoal', element=ShoalMemberElement, size=10, location=(5.0, 5.0)).size
10
Source code in src/skcomplex/models/biology/collective_behaviour/_leadership.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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

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.models.biology.collective_behaviour import SwimmingInteraction
>>> SwimmingInteraction('swimming', target='shoal', phase='swim').phase
'swim'
Source code in src/skcomplex/models/biology/collective_behaviour/_swimming.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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.system import System
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.models.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).check().prepare().run(until=2.0).trajectory_
>>> round(float(trajectory.values('left.n')[-1]), 3)
1.025
Source code in src/skcomplex/models/biology/collective_behaviour/_trail_choice.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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

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 model 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.elements import GroupElement
>>> from skcomplex.models.biology.collective_behaviour import WalkerElement
>>> GroupElement('walkers', element=WalkerElement, size=4, location=(0, 0)).size
4
Source code in src/skcomplex/models/biology/collective_behaviour/_walker.py
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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 model, with the walkers people/walkers, the ground structure land/ground.G, and the trail potential land/ground.V.

Examples:

>>> from skcomplex.models.biology.collective_behaviour import active_walkers
>>> [child.label for child in active_walkers().systems]
['people', 'land']
Source code in src/skcomplex/models/biology/collective_behaviour/_trails.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
@register_model(
    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 model, with the walkers `people/walkers`, the ground structure `land/ground.G`, and the trail potential
            `land/ground.V`.

    Examples:
        >>> from skcomplex.models.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',
            ),
        ],
    )

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 model, with the marks left.n and right.n.

Examples:

>>> from skcomplex.models.biology.collective_behaviour import binary_ant_decision
>>> [branch.label for branch in binary_ant_decision().elements]
['left', 'right']
Source code in src/skcomplex/models/biology/collective_behaviour/_decisions.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@register_model(
    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 model, with the marks `left.n` and `right.n`.

    Examples:
        >>> from skcomplex.models.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 model, with the positions birds.x and the velocities birds.x_d1.

Examples:

>>> from skcomplex.models.biology.collective_behaviour import bird_swarm
>>> bird_swarm(5).elements[0].check().prepare().size_
5
Source code in src/skcomplex/models/biology/collective_behaviour/_flocks.py
 99
100
101
102
103
104
105
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
135
136
137
138
139
140
141
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
@register_model(
    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 model, with the positions `birds.x` and the velocities `birds.x_d1`.

    Examples:
        >>> from skcomplex.models.biology.collective_behaviour import bird_swarm
        >>> bird_swarm(5).elements[0].check().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 model, with the group flock and the velocities vx and vy of its boids.

Examples:

>>> from skcomplex.models.biology.collective_behaviour import boids
>>> boids(10).elements[0].check().prepare().size_
10
Source code in src/skcomplex/models/biology/collective_behaviour/_flocks.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
@register_model(
    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 model, with the group `flock` and the velocities `vx` and `vy` of its boids.

    Examples:
        >>> from skcomplex.models.biology.collective_behaviour import boids
        >>> boids(10).elements[0].check().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'),
    )

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 model, 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.models.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/models/biology/collective_behaviour/_leadership.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
@register_model(
    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 model, 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.models.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 model, with the ants colony/ants and the pheromone ground/pheromone.level.

Examples:

>>> from skcomplex.models.biology.collective_behaviour import pheromone_foraging
>>> [child.label for child in pheromone_foraging().systems]
['colony', 'ground']
Source code in src/skcomplex/models/biology/collective_behaviour/_foraging.py
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
135
136
137
138
139
140
141
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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
222
223
224
225
226
227
228
229
230
231
232
@register_model(
    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 model, with the ants `colony/ants` and the pheromone `ground/pheromone.level`.

    Examples:
        >>> from skcomplex.models.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 model, with the numbers calm.n and nervous.n.

Examples:

>>> from skcomplex.models.biology.collective_behaviour import rats
>>> [group.n for group in rats().elements]
[95.0, 5.0]
Source code in src/skcomplex/models/biology/collective_behaviour/_decisions.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
@register_model(
    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 model, with the numbers `calm.n` and `nervous.n`.

    Examples:
        >>> from skcomplex.models.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])