Return to the work
Cordyceps Septet
Electrical activity in a fungus rendered as light and sound, with the reading of a small model trained on the voltage data.
The recording
Andrew Adamatzky inserted seven pairs of electrodes into a culture of Cordyceps militaris. Each pair recorded the voltage between its two points once a second for twenty-two days. Everything in the work comes from these readings. At the default pace, one day of the recording passes in one minute.
The light
In the visualization, each pair of electrodes is a line of light with the seven lines forming a ring. The bright ring is the present. The past recedes behind it, six hours at a time, with a faint ring for each hour and a brighter ring at the turn of each day.
Each channel drifts slowly over hours. A spike is a sudden jump away from that drift, more than four times the channel's usual tremor, so each channel is measured in relation to its own tendencies. A spike flares as a bead and dims as it recedes.
When two channels spike within thirty seconds of each other, a gilt arc joins them.
The sound
No sound was recorded. Each pair of electrodes has its own note. The low tone follows the drift. Each spike strikes a channel's note, and each meeting sounds both notes together. Sound and light come from the same numbers at the same moment.
The model
The blue light belongs to a small transformer model, the design behind large language models. A language model learns by guessing the next word, checking that guess, and then adjusting. This 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, which is about eighteen days.
The blue glow at the end of each line is its guess. The blue threads inside the ring show which channels it reads as it guesses. The gilt arcs outside the ring record what happened; the blue threads inside show what the model learned to attend to.
The model keeps none of the readings, but it remembers the relations. On the four days it never saw, it anticipates spikes better than chance on six of the seven channels, and it anticipates them less well when the other channels are scrambled. The quietest channel, which almost never spikes, is the one it reads most. The model cannot say whether the channels signal one another or answer 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 13,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.
Made by Alexandra Emberley with Claude, September 2026.
© 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.