Return to the work
Cordyceps Septet
Electrical activity in the Cordyceps militaris fungus rendered as light and sound against the reading of a small model trained on the voltage data.
Data
In a study published in 2022, Andrew Adamatzky, a computer scientist who directs the Unconventional Computing Laboratory at the University of the West of England in Bristol, placed pairs of electrodes into a culture of Cordyceps militaris. Seven pairs, or channels, recorded the voltage between their two points once a second for twenty-two days. At the default pace, a day of the recording passes in one minute.
Light
In the visualization, each channel becomes a line of light, with the seven lines forming a ring. The brightest ring is the present, with the past receding behind it, six hours at a time, and an echo of the ring marking each hour.
Each channel is measured against its own behaviour: its slow drift over hours, and its usual small tremor around that drift. A spike is a sudden jump away from the drift, more than four times the channel’s usual tremor. Each spike flares as a white bead and then dims as it recedes.
When two channels spike within thirty seconds of each other, they meet, and a gold arc joins them.
Sound
Each channel has its own note. The low tones follow the drift. Each spike strikes a channel’s note, and each meeting sounds both notes together.
The model
The blue light belongs to a small transformer model, based on the architecture introduced in 2017 by Ashish Vaswani and colleagues in their paper “Attention Is All You Need.” The same architecture underlies large language models, which are trained on vast amounts of text to predict what comes next. The mechanism at its centre is called attention. Rather than reading one step at a time, a transformer compares each part of its input with the parts that came before it and weighs how much each one matters. In a language model, this lets a word draw on others far back in a passage. Here, it lets each minute of a channel draw on what all seven channels were doing in the minutes before.
For this visualization, the model reads the last sixteen minutes of all seven channels and guesses whether each channel will spike in the next ten minutes. It was trained on the first four-fifths of the recording, about eighteen days, by comparing its guesses with what happened and adjusting its learned numbers.
The blue glow where each line meets the bright ring is the model’s guess: the larger and brighter the glow, the more strongly it expects that channel to spike. The blue threads inside the ring join the channels it reads as it makes that guess, and the stronger its attention, the brighter the thread.
The gold arcs outside the ring record what happened, while the blue light shows what the model expects and where it is looking.
On the four days it never saw, it anticipates spikes better than chance on six of the seven channels, and does less well when the readings of the other six channels are shuffled so that their timing no longer lines up, which suggests it has learned from the timing of the relations between them. The quietest channel, which almost never spikes, is the one it attends to most strongly. The model does not tell us whether the channels are signalling one another or responding to a shared condition such as moisture or growth.
In numbers
Seven electrode pairs; 1.9 million readings per pair; 5,948 spikes; 527 meetings, of which about 425 would occur by chance. The model holds about 10,000 learned numbers in two layers.
Sources
Data: Andrew Adamatzky, “Language of fungi derived from their electrical spiking activity,” Royal Society Open Science 9 (2022): 211926. The recordings are published on Zenodo under a Creative Commons Attribution 4.0 licence.
Model design: Ashish Vaswani and colleagues, “Attention Is All You Need,” Advances in Neural Information Processing Systems 30 (2017).
The model’s code is open source under the MIT Licence.
© 2026 Alexandra Emberley. All rights reserved, except the model’s code (MIT Licence) and the recording (CC BY 4.0, Andrew Adamatzky).
Return to the work
The model
The small transformer in this work, written in Python with numpy and open source under the MIT Licence. Anyone may read, run, change, and share it. The learned numbers it holds after training are inside this page. The recording belongs to Andrew Adamatzky and carries its own licence.
# Cordyceps Septet
# The model: a small transformer, written from scratch in Python with numpy.
#
# Copyright (c) 2026 Alexandra Emberley.
# Released under the MIT Licence; the full text is at the end of this file.
import numpy as np
# The shape of the model.
C = 7 # channels: seven pairs of electrodes
K = 16 # minutes the model reads at once
T = C * K # tokens: one for each channel in each minute (112)
F = 3 # what each token carries: drift, spike strength, change in drift
D = 24 # the width of the model's inner space
H = 2 # attention heads: two ways of reading at once
DH = 12 # width of each head
FF = 48 # width of the small layer that follows attention
L = 2 # layers
# Each token knows its minute and its channel.
tidx = np.repeat(np.arange(K), C)
cidx = np.tile(np.arange(C), K)
# A token may read any token from its own minute or earlier, never from the future.
MASK = np.where(tidx[None, :] <= tidx[:, None], 0.0, -1e9)
def init(seed=0):
"""Fill every table with small random numbers. Training replaces them with learned ones."""
r = np.random.default_rng(seed)
g = lambda *s, sc=1: r.normal(0, sc, s)
p = {'We': g(F, D, sc=0.5), 'be': np.zeros(D), # how a reading enters the inner space
'Ec': g(C, D, sc=0.3), 'Et': g(K, D, sc=0.3), # a signature for each channel and each minute
'Wout': g(D, sc=0.1), 'bout': np.array(-2.5)} # how the inner space becomes a guess
for l in range(L):
p[f'Wq{l}'] = g(D, H * DH, sc=D ** -.5) # queries: what each token looks for
p[f'Wk{l}'] = g(D, H * DH, sc=D ** -.5) # keys: what each token offers
p[f'Wv{l}'] = g(D, H * DH, sc=D ** -.5) # values: what each token passes on
p[f'Wo{l}'] = g(H * DH, D, sc=(H * DH) ** -.5 * 0.5)
p[f'W1{l}'] = g(D, FF, sc=D ** -.5); p[f'b1{l}'] = np.zeros(FF)
p[f'W2{l}'] = g(FF, D, sc=FF ** -.5 * 0.5); p[f'b2{l}'] = np.zeros(D)
return p
def forward(p, x):
"""Read sixteen minutes of seven channels and guess, for each token, whether its channel
will spike in the next ten minutes. Returns the guesses, what learning needs, and attention."""
B = x.shape[0]
X = x @ p['We'] + p['be'] + p['Ec'][cidx] + p['Et'][tidx]
cache, atts = [], []
for l in range(L):
# Attention: each token compares what it looks for with what every earlier token offers.
Q = (X @ p[f'Wq{l}']).reshape(B, T, H, DH)
Kk = (X @ p[f'Wk{l}']).reshape(B, T, H, DH)
V = (X @ p[f'Wv{l}']).reshape(B, T, H, DH)
S = np.einsum('bihd,bjhd->bhij', Q, Kk) / np.sqrt(DH) + MASK
S = S - S.max(-1, keepdims=True)
P = np.exp(S); P /= P.sum(-1, keepdims=True) # how much each token reads each other token
O = np.einsum('bhij,bjhd->bihd', P, V).reshape(B, T, H * DH)
X1 = X + O @ p[f'Wo{l}'] # add what was read to what was there
# A small layer that thinks about what was read.
Hh = np.tanh(X1 @ p[f'W1{l}'] + p[f'b1{l}'])
X2 = X1 + Hh @ p[f'W2{l}'] + p[f'b2{l}']
cache.append((X, Q, Kk, V, P, O, X1, Hh)); atts.append(P); X = X2
logit = X @ p['Wout'] + p['bout'] # the guess, before it becomes a probability
return logit, (x, X, cache), atts
def loss_grad(p, x, y, w):
"""Compare the guesses with what happened, and work backwards through every step to find
how each number in each table should change to make the guesses a little better."""
logit, (x, XL, cache), _ = forward(p, x)
B = x.shape[0]
pr = 1 / (1 + np.exp(-logit)); n = w.sum()
loss = -(w * (y * np.log(pr + 1e-9) + (1 - y) * np.log(1 - pr + 1e-9))).sum() / n
dl = w * (pr - y) / n
g = {k: np.zeros_like(v) for k, v in p.items()}
g['Wout'] = np.einsum('btd,bt->d', XL, dl); g['bout'] = np.array(dl.sum())
dX = dl[..., None] * p['Wout']
for l in reversed(range(L)):
X, Q, Kk, V, P, O, X1, Hh = cache[l]
g[f'W2{l}'] = np.einsum('btf,btd->fd', Hh, dX); g[f'b2{l}'] = dX.sum((0, 1))
dZ = (dX @ p[f'W2{l}'].T) * (1 - Hh ** 2)
g[f'W1{l}'] = np.einsum('btd,btf->df', X1, dZ); g[f'b1{l}'] = dZ.sum((0, 1))
dX1 = dX + dZ @ p[f'W1{l}'].T
g[f'Wo{l}'] = np.einsum('bte,btd->ed', O, dX1)
dO = (dX1 @ p[f'Wo{l}'].T).reshape(B, T, H, DH)
dP = np.einsum('bihd,bjhd->bhij', dO, V); dV = np.einsum('bhij,bihd->bjhd', P, dO)
dS = P * (dP - (dP * P).sum(-1, keepdims=True)) / np.sqrt(DH)
dQ = np.einsum('bhij,bjhd->bihd', dS, Kk).reshape(B, T, -1)
dK = np.einsum('bhij,bihd->bjhd', dS, Q).reshape(B, T, -1)
dV = dV.reshape(B, T, -1)
g[f'Wq{l}'] = np.einsum('btd,bte->de', X, dQ)
g[f'Wk{l}'] = np.einsum('btd,bte->de', X, dK)
g[f'Wv{l}'] = np.einsum('btd,bte->de', X, dV)
dX = dX1 + dQ @ p[f'Wq{l}'].T + dK @ p[f'Wk{l}'].T + dV @ p[f'Wv{l}'].T
g['We'] = np.einsum('btf,btd->fd', x, dX); g['be'] = dX.sum((0, 1))
np.add.at(g['Ec'], cidx, dX.sum(0)); np.add.at(g['Et'], tidx, dX.sum(0))
return loss, g
# MIT Licence
# Copyright (c) 2026 Alexandra Emberley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
# software and associated documentation files (the "Software"), to deal in the Software
# without restriction, including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
# to whom the Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
# FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.