The graph
hover a box · click to pin · drag to move · Shift + wheelwheel to scroll along · Ctrl + wheel to zoomAt this zoom the names are too small to read: hover a box for its card, or press + or 100%.
In short. Which earlier genes the model looks at while it guesses the next one. It reads a path of proteins, drawn in the Graph tab or the first ten genes of the window; in a map, each row is a protein of the path and each column one it can look back at, darker when it looks there more. Inside, the model has 12 layers of 8 , each with its own habits; a , where idle heads rest, is left out by default. Attention shows where the model looks, not why it guesses what it guesses: a view for the curious, not a result.
Attention maps
hover a cell · click a head in the map to show itRows: each protein of the path as the model reads it; columns: the start token, the proteins before the path when a genome’s own path brings them (summed), and the path itself. The model is causal: a protein sees only those before it. Much of the weight rests on the start token, where a head goes when nothing in the path concerns it (an attention sink); leave it out to see the rest, each row renormalised. Attention shows where the model looks, not why it calls what it calls.
Coupled regions
click an arc or an element, or pick in the lists · hover for details
What the model gets right and where it doubts, measured on the dnaA window: 80 genes from dnaA in all 2,002 genomes, 130,837 calls, each on a genome’s real proteins.
Reading the window
the dnaA window, column by columnThe spine starts at the origin. Columns 0 to 5, dnaA, dnaN, recF, gyrB, yidB and yidA, sit in 92 to 100% of genomes, persistent and outside any region of plasticity. The model names dnaN, recF and gyrB in 99.9 to 100% of genomes, yidB in 93% and yidA in 99%.
The first fork is the dgo operon, galactonate catabolism, columns 7 to 11, after yidX (column 6). About 85% of genomes carry it, and panRGP puts it in a region of plasticity in almost every genome that does. The model is not lost there. Inside the operon it names each gene in 87 to 99% of genomes, with hardly any hesitation. A variable region can keep a predictable internal order.
Then a long persistent stretch, cbrA to yicS through the ilv and uhp operons, where the model names the next gene in 92% of genomes and seldom hesitates. Around columns 42 to 52 the graph opens: half the calls there fall in a region of plasticity, and the model’s hesitation peaks, up to 1.4 bits. From column 42 about fifty genomes carry the LEE pathogenicity island of enteropathogenic strains, which runs on to the window’s end: ler at column 42, then the esc and ces genes, tir and eae (column 71). A few hundred genomes carry other type III secretion and sugar transport genes there.
The window ends in the waa locus, which builds the lipopolysaccharide core: waaA in 38% of genomes (55% of those that reach column 69), then a spread of glycosyltransferases that differ between core types.
Where the model is sure, and where it hesitates
130,837 calls · 2,002 genomesEach call is joined to the gene actually at that position, with its PPanGGOLiN partition and whether panRGP places it in a region of genomic plasticity.
| PPanGGOLiN partition of the gene | calls | first guess right | via the exemplar bank | entropy |
|---|
| region of genomic plasticity | calls | first guess right | via the exemplar bank | entropy |
|---|
Accuracy follows the partition. Read in its own vocabulary, the model names the next gene 95% of the time when it is persistent, 80% when shell, 29% when cloud, and 94% outside a region of plasticity against 78% inside.
So does its hesitation, by a wide margin. Entropy is 0.21 bits outside a region of plasticity and 0.83 inside, 0.16 bits on persistent genes and 3.3 on cloud genes. The effect holds under three checks:
- Same position. At 91% of the 65 columns where both occur, calls inside a region of plasticity are more hesitant, by 0.84 bits in the median.
- Same genome. In 84% of 1,942 genomes, the model is more hesitant inside its regions of plasticity than outside.
- As a detector. Entropy alone separates calls inside and outside regions of plasticity with an AUC of 0.755. It also flags errors: 0.45 bits when the model is wrong, 0.23 when right.
One genome would not have shown it. Read on O157:H7 Sakai alone (79 positions), the model is no more hesitant inside regions of plasticity (0.44 bits) than outside (0.55); only the two thousand genomes show the difference. It is not uniform either: the dgo operon sits in a region of plasticity in most genomes, yet the model is calm there and mostly right.
Against the graph. Predicting the next family from the previous one alone, the most frequent successor learned on the other half of the genomes, gives 90.0%; the model gives 88.8%. Where the graph is certain the model adds nothing. After a fork, where two or more families follow, the model is right 79.6% of the time against 72.0% for the graph: it reads the genes before, and their sequence variants (alleles), which a graph of families does not hold.
How often the model named the next gene
real prefixes only| columns | calls | mean entropy | first guess right | via the exemplar bank |
|---|
Every call is made on the real proteins of a real genome, so this measures the model, not a substitution. The anchor is what makes the difference. Along the replication genes right after the origin (dnaN, recF, gyrB) its first guess is right in 99.97% of genomes, out of fifty thousand families, and its distribution is nearly a point mass. It stays right nine times in ten along the persistent stretch, and drops where the genomes vary, down to three times in four in the waa locus.
The model’s families are its own. Bacformer predicts among 50,000 clusters of its training set. An exemplar bank built from 37 genomes covers 9,814 of them; where a family’s cluster is missing from it, the bank scores the model wrong even when it is right. At yidB the model predicts the same cluster in 1,969 of 1,976 genomes, one the bank does not hold: 0.2% by the bank, 92.6% read in the model’s own vocabulary. Its own vocabulary is read with a decoder, P(family | cluster): the families actually present when the model predicts a cluster, learned on one half of the genomes and applied to the other. On an invariant column any consistent model looks right this way; the forks are where it is tested.
Data, model and credit
PanGramGraph reads the pangenome graph of a species with the grammar of gene order that a genomic language model learned: the graph shows what the genomes carry, the model what it expects next.
What it is for. A browser of gene neighbourhoods across E. coli: where a gene sits, what comes before and after it, in how many genomes and in which strains (serotype, host, source, country where PanGBank records them). And a test of what a genomic language model has learned of that order, and what it has not. It does not type an isolate, call a pathotype or report resistance, and it reads PanGBank’s genomes only: to place a genome of your own, project it onto the pangenome with PPanGGOLiN (see PanGBank).
The genomes, gene families, partitions and regions of plasticity all come from PanGBank (pangenome 11587, GTDB_refseq v2.0.0, GTDB R232, 2,002 genomes), built with PPanGGOLiN, Gautreau et al. 2020, PLOS Computational Biology 16(3):e1007732, with regions of plasticity from panRGP, Bazin et al. 2020, Bioinformatics 36(Suppl_2):i651. PanGBank data are CC BY-SA 4.0, as is the GTDB taxonomy it adapts. The language model is Bacformer, Wiatrak et al., bioRxiv 2025.07.20.665723, on ESM-2 protein embeddings, Lin et al. 2023, Science 379:1123.
The readings of a window in Findings that are not hand-written or automatic are
written by DeepSeek-V4.1-Flash (DeepSeek), called by the page’s server through Hugging Face Inference Providers
(DeepInfra, else Novita), from facts the server computes, following the reading skill reading_skill.md; the
server checks every sentence against the facts lines it cites.
PanGBank pangenome 11587 · the Bacformer model card · the code, on GitHub · the page and its model’s server, on a Hugging Face Space
How it was computed
scripts in the repositoryFor whoever wants to check or rerun a number: each step, the script in the repository that makes it, and the file it writes. Nothing here is needed to read the page.
| what | script | output |
|---|---|---|
| the dnaA window of the 2,002 genomes, 80 genes each | pgb_window.py | pgb/window.json |
| their proteins embedded with ESM-2, and Bacformer’s call at every position | pgb_model.py | pgb/model_calls.npz |
| the graph of the dnaA window, the decoder, perplexity and ghosts | pgb_graph.py | pgb/graph_pgb.json |
| the 540 complete chromosomes, their proteins and the model’s call at every gene | pg_chrom.py, pg_embed.py, pg_calls.py | pgb/chrom*, pgb/calls_* |
| the pangenome’s own numbers, what each genome is searched by, and the map of the chromosome | pgb_page_meta.py | pgb/pangenome_info.json, pgb/genome_meta.json, pgb/chrom_map.json |
| any other window of the chromosome, when the page asks | pgb_region.py | served live |
| distant families that go together within lineages, and their sets | pg_epistasis.py --ctrl_content, pg_region_sets.py | pgb/region_sets.json |
| the same at close range, 50–500 genes apart in separate insertions, replicated in lineage-disjoint halves | pg_epistasis.py --halves lineage, pg_region_sets.py (close mode) | pgb/region_sets_close.json |
| what each link is in the genomes: one element at several spots, one tract, separate insertions, one insertion site, replicated in lineage-disjoint halves | pg_link_flags.py | pgb/link_flags.json, pgb/link_flags_close.json |
| the knockout engine: full-precision passes over whole chromosomes, the decoder of the other lineage half, whole-element controls, self-checks | pg_knock.py | pgb/knock/base_* |
| the in-silico knockout of every link, and the model’s verdict on each arc (long range and close range) | pg_knockout.py --all | pgb/knock/all_arcs.json, close_arcs.json; the run reports pgb/knock/all/all_report.md, close/close_report.md |
| the genomes carrying each element at its spot | pg_region_carriers.py | pgb/region_carriers.json |
| what the model does without one element, on demand | serve_live.py /influence (pg_knock.influence) | served live |
| a window’s reading by DeepSeek-V4.1-Flash: the window’s facts, the prompt and its skill, the check of every sentence against the facts it cites | pg_reading.py, reading_skill.md | /reading, served live; the 112 readings written in advance, pgb/readings/*.json |
| each genome’s page on PanGBank, for the links from the cards | pgb_pangbank_ids.py | pgb/pangbank_genome_ids.json |
| the model’s server: calls in one genome, drawn paths, windows, attention, readings | serve_live.py | the public Space, or 127.0.0.1:8765 when run locally |
| this page | build_page.py | standalone/pangramgraph.html |
Why every genome. The graph was first drawn from eleven complete chromosomes. The adjacencies the model expected and none of the eleven carried were then looked up in PanGBank’s 2,002 genomes: they were there 2.7 times as often as chance. What the model expects beyond a small sample exists in the species, which is why the page shows every genome.