Skip to the content.

Self clustering: an elected head per neighbourhood

What it shows

There is no central cluster manager here. Each device scores itself from four local signals, points to the best neighbour it can reach, and the clusters emerge from those pointers: a device that points to itself is a head, and every other device follows the pointers until it reaches one. The orchestrator then places a task inside the cluster its origin joined, giving the elected head a 25 percent discount so a neighbourhood gets a natural centre of gravity without any negotiation or message exchange.

The Python core has no hook for a custom computing node, so the election state does not live on the devices the way it does in the Java port. It lives in ClusterOrchestrator, registered as a simulation entity, which runs one clustering round every CLUSTER_UPDATE_INTERVAL of 30 seconds (the first at one second) rather than letting a coordinator device drive it. The scoring and the pointer-following stay module-level pure functions, elect_clusters and refresh_clusters, which is the counterpart of the two static methods the Java device class carried.

The score is a weighted sum of normalised signals: capacity (0.4, total MIPS over the most powerful alive device), battery (0.25, remaining level, or 1.0 for a mains device), stability (0.2, 1.0 for a fixed device, falling to 0 at 10 m/s), and neighbourhood (0.15, devices in range, saturating at 10). A device points to the best score among its reachable neighbours and itself, ties going to the lower identifier. Dead devices and sensors are left out.

flowchart TD
    dev["Each device scores itself<br/>capacity, battery, stability, degree"] --> point["Point to the best neighbour in range"]
    point --> self{"Points to itself?"}
    self -->|yes| head["Cluster head"]
    self -->|no| follow["Follow the pointers"]
    follow --> head
    head --> place["Place a task inside its origin's cluster"]
    place --> cost["cost = (1 + pending) * length / capacity<br/>plus radio time when remote"]
    cost --> disc["Head wins on equal footing (0.75 discount)"]
    classDef elect fill:#cfe8ff,stroke:#333;
    class head,disc elect;

The scenario

settings/ starts from a mist-only fleet:

The one edge data center in edge_datacenters.xml is never a destination under this architecture, so its idle draw is the only edge energy in the report.

Run it

From the repository root, after pip install -e .:

python examples/self_clustering/self_clustering.py

No-install variant:

PYTHONPATH=src python examples/self_clustering/self_clustering.py

What to look for

The console ends with the two cluster reports and the result row:

ClusterOrchestrator - cluster placement: 3431 placements, head=34.1 %, member=1.5 %, local=64.3 %, outside=0.0 %
ClusterOrchestrator - clustering: 11 clusters over 200 devices, 11 rounds, average size 18.2, largest 38
CLUSTER_LOCAL    devices= 200 tasks= 14500 ok= 11200 failed(delay)= 3300 avg_delay=0.1306   cloud=   0 edge=   0 mist=14500 lan_usage=83.6353   traffic_MB=13264.28  energy=45.1434
Placement kind Share of the 3431 decisions
head 34.1 %
member 1.5 %
local (the origin) 64.3 %
outside the cluster 0.0 %

Set HEAD_COST_FACTOR to 1.0 to drop the discount and watch the head share fall, or change CLUSTER_UPDATE_INTERVAL to see how stale membership and placement react as mobility moves the neighbourhoods between rounds.

Results go to examples/self_clustering/output/.

Files

Where to go next