Standard Model 7: Pions

23 March, 2026

This time I’m talking about pions:

Pions were a revolutionary discovery in the 1930s—part of the first wave of the ‘particle zoo’—but I’m explaining them as a way to work toward the math and physics concepts needed for the Standard Model.

As soon as the neutron was discovered in 1932, Heisenberg invented the idea of ‘isospin’, and the idea that the proton and neutron are two different isospin states of a single particle, the ‘nucleon’. This is why I spent 3 videos explaining the math of spin-1/2 particles: in order to talk about isospin.

Three years later, Yukawa came up with the idea that the force holding nuclei together is carried by a new particle. Even better, he predicted the mass of this yet-unseen particle! It’s a fun bit of physics but also a step toward the concept of gauge bosons.

Later, the pion was discovered: in fact, three kinds of pions! I begin to explain how these three pions form a basis of \mathfrak{sl}(2,\mathbb{C}) just as the proton and neutron form a basis of \mathbb{C}^2.

These theories are outdated, but their math gets reused in the Standard Model. We’ll eventually get around to that!


The Agent that Doesn’t Know Itself

20 March, 2026

guest post by William Waites

The previous post introduced the plumbing calculus: typed channels, structural morphisms, two forms of composition, and agents as stateful morphisms with a protocol for managing their state. The examples were simple. This post is about what happens when the algebra handles something genuinely complex.

To get there, we need to understand a little about how large language models work. These models are sequence-to-sequence transducers: a sequence of tokens comes in, a sequence comes out. Text is tokenised and the model operates on the tokens.

From the outside, the morphism is simple: !string → !string. A message goes in, a message comes out. But the client libraries (the code that calls the LLM provider) maintain the conversation history and send it back with every call. The actual morphism is
(!string, ![Message]) → (!string, ![Message]): the input message and the accumulated history go in, the response and the updated history come out. The history feeds back. This is a trace in the sense of traced monoidal categories: the feedback channel is hidden from the user, who sees only !string → !string.

Crucially, the model has a limited amount of memory. It is not a memoryless process, but the memory it has is not large: 200,000 tokens for current models, perhaps a million for the state of the art. This sounds like a lot. It is not. An academic paper is roughly 10,000 tokens. A literature review that needs to work with thirty papers has already exceeded the context window of most models, and that is before the model has produced a single word of output.

If you have used any of these agent interfaces, you will have noticed that after talking back and forth for a while, the agent will compact. This is a form of memory management. What is happening is that some supervisory process has noticed the context window filling up, and has intervened to shorten its contents. A naïve approach is to truncate: discard everything before the last N exchanges. A better approach is to feed the entire context to another language model and ask it to summarise, then put the summary back.

This is normally done by specialised code outside the agent, invisible to it.

How to manage agent memory well is an active research area. We do not, in general, do it very well. Truncation loses information. Summarisation loses nuance. Pinning helps but the right pinning strategy depends on the task. These are open questions, and to make progress we need to be able to experiment with different schemes and mechanisms: express a memory management strategy, test it, swap it for another, compare. Not by recompiling specialised code or hardcoding behaviour, but by writing it down in a language designed for exactly this kind of composition. Memory management should be a plumbing program: modular, type-checked, swappable.

So we built an implementation of compaction using the plumbing calculus, and the first thing we did was test it. I ran the protocol on a very short cycle: a single message caused a compaction, because the threshold was set to zero for testing. The compressor fired, produced a summary, rebuilt the agent’s context. The logs showed [compressor] 3404 in / 541 out. The protocol worked.

Then I asked the agent: "have you experienced compaction?"

The agent said no. It explained what compaction is, accurately. Said it hadn’t happened yet. It was confident.

I asked: "do you have a context summary in your window?"

Yes, it said, and described the contents accurately.

"How did that context summary get there if you have not yet compacted?"

The agent constructed a plausible, confident, and completely wrong explanation: the summary was "provided to me by the system at the start of this conversation" as a "briefing or recap." When pressed, it doubled down:

"The context-summary is not evidence that compaction has occurred. It’s more like a briefing or recap that the system gives me at the start of a conversation session to provide continuity."

The agent was looking at the direct evidence of its own compaction and confidently explaining why it was not compaction. We will return to why it gets this wrong, and how to fix it. But first: how do we build this?

The compacting homunculus

At a high level, it works like this. An agent is running: input comes in, output goes out. Together with the output, the agent emits a telemetry report. The telemetry includes token counts: with each transaction, the entire train of messages and responses is sent to the LLM provider, and back comes a response together with a count of the tokens that went in and the tokens that came out. Our agent implementation sends this telemetry out of the telemetry port to anybody who is listening.

The construction involves a second agent. This second agent is a homunculus: the little man who sits on your shoulder and watches what your mind is doing. Here is the topology:

Topology of the compacting homunculus. Two boxes: Agent (large, bottom) and Compressor (smaller, top). The Agent has input and output ports for the main data flow (dark blue arrows). Three channels connect the Agent to the Compressor: telemetry flows up from the Agent (the Compressor watches token counts), ctrl_out flows up from the Agent (the Compressor receives acknowledgements), and ctrl_in flows down from the Compressor to the Agent (the Compressor sends commands). The Agent does not know the Compressor exists. It just receives control messages and responds to them.

The homunculus listens to the telemetry and says: the memory is filling up. The token count has crossed a threshold. It is time to compact. And then it acts:

• Send pause to the agent’s control port. Stop accepting input.
• Send get memory. The agent produces the contents of its context window.
• Summarise that memory (using another LLM call).
• Send set memory with the compacted version.
• Send resume. The agent continues processing input.

Each step requires an acknowledgement before the next can proceed. This is a protocol: pause, acknowledge, get memory, here is the memory, set memory, acknowledge, resume, acknowledge.

It is possible to express this directly in the plumbing calculus, but it would be painfully verbose. Instead, we use session types to describe the protocol. This is not pseudocode. There is a compiler and a runtime for this language. Here is the protocol:

protocol Compaction =
  send Pause . recv PauseAck .
  send GetMemory . recv MemoryDump .
  send SetMemory . recv SetMemoryAck .
  send Resume . recv ResumeAck . end

The protocol is eight lines. It reads as a sequence of steps:
send, receive, send, receive, and so on. The compiler knows what
types each step carries. Now we wire it up:

let compact : (!CtrlResp, !json) -> !CtrlCmd =
  plumb(ctrl_out, telemetry, ctrl_in) {

    (ctrl_out, ctrl_in)  Compaction as session

    telemetry
      ; filter(kind = "usage" && input_tokens > 150000)
      ; map(null) ; session@trigger

    session@trigger ; map({pause: true})
      ; session@send(Pause)
    session@done(PauseAck) ; map({get_memory: true})
      ; session@send(GetMemory)
    session@recv(MemoryDump) ; compressor
      ; session@send(SetMemory)
    session@done(SetMemoryAck) ; map({resume: true})
      ; session@send(Resume)
}

The first line binds the protocol to the agent’s control ports:

(ctrl_out, ctrl_in) <-> Compaction as session.

This says: the Compaction protocol runs over the control channel, and we refer to it as session. The telemetry line is the trigger: when token usage crosses a threshold, the protocol begins. Each subsequent line is one step of the protocol, wired to the appropriate control
messages.

Here is a direct depiction of the protocol as wired. You can trace it through:

Diagram of the compaction protocol wired between the homunculus and the bot agent. Shows the telemetry stream flowing from the bot to the homunculus, a filter checking token usage against a threshold, and then a sequence of control messages: Pause flows to ctrl_in, PauseAck returns on ctrl_out, GetMemory flows in, MemoryDump returns, passes through a compressor agent, SetMemory flows in, SetMemoryAck returns, Resume flows in, ResumeAck returns. The protocol steps are connected in sequence. This is a direct transcription of the session type protocol into a wiring diagram.

And here is how we wire the homunculus to the agent:

let main : !string -> !string =
  plumb(input, output) {
    let ctrl : !CtrlCmd = channel
    let ctrl_out : !CtrlResp = channel
    let telem : !json = channel

    spawn bot(input=input, ctrl_in=ctrl,
              output=output, ctrl_out=ctrl_out,
              telemetry=telem)
    spawn compact(ctrl_out=ctrl_out,
                  telemetry=telem, ctrl_in=ctrl)
}

The main morphism takes a string input and produces a string output. Internally, it creates three channels (control commands, control responses, telemetry) and spawns two processes: the bot agent and the compact homunculus. The homunculus listens to the bot’s telemetry and control responses, and sends commands to the bot’s control input. The bot does not know the homunculus exists. It just receives control messages and responds to them.

There are two nested traces here. The first is the one from before, inside the agent: messages go in, the output accumulates with everything that came before, and the whole history feeds back on the next turn. We do not see this trace. It is hidden inside the client library. The second trace is the one we have just built: the homunculus. What goes around the outer loop is control: telemetry flows out, commands flow in, acknowledgements come back. The memory dump passes through the control channel at one point in the protocol, but the feedback path is control, not conversation history. Nested traces compose; the algebra has identities for this and it is fine. But they are different loops carrying different things.

Session types as barrier chains

The connection between the protocol above and what the compiler actually produces is the functor from session types into the plumbing calculus. This functor works because of barrier.

Why do we need the barrier? Because the protocol is about sending a message and waiting for a response. We can send a message, but we need the response to arrive before we proceed. The barrier takes two streams, one carrying the "done" signal and one carrying the response, and synchronises them into a pair. Only when both are present does the next step begin.

Each session type primitive has a direct image in the plumbing category, and the structure is prettier than it first appears. The primitives come in dual pairs:

In the diagrams below, session types are on the left in blue; their images in the plumbing calculus are on the right in beige.

send and recv are dual. They map to map and filter, which are also dual: send wraps the value with map, then synchronises via barrier with the done signal from the previous step. Recv filters the control output by step number, synchronises via barrier, then extracts the payload with map.

select and offer are dual. They map to tag and case analysis, which are also dual: select tags the value with a label via map, synchronises via barrier, and routes to the chosen branch chain. Offer copies the control output and filters each copy by label, routing to the appropriate branch chain.

Diagram showing the two dual pairs of session type primitives and their images in the plumbing calculus. Session types are shown in blue on the left; plumbing calculus images in beige on the right. Top section: send T is a simple arrow carrying type T; its image is map(wrap) followed by barrier with a done signal, then routing to the control input. recv T is its dual: filtering ctrl_out by step number, barrier with done, then map to extract the payload. Bottom section: select with labelled alternatives maps to coproduct injection via map(tag), barrier, then routing to the chosen branch chain. offer is its dual: copy the control output, filter each copy by label, and route to the corresponding branch chain.

• The sequencing operator (.) maps to a barrier chain. Each send-then-recv step becomes a barrier that synchronises the outgoing message with the incoming acknowledgement, and these barriers chain together to enforce the protocol ordering.

Diagram showing how the sequencing operator in session types maps to a barrier chain in the plumbing calculus. Top: the session type sequence "send T1 . recv T2 . send T3" shown as three boxes in a row connected by dots. Bottom: the plumbing image, a chain of barriers. Each send-recv pair becomes a barrier that takes the outgoing message on one arm and the incoming acknowledgement on the other, producing a synchronised pair. The done signal from one barrier feeds into the next, creating a chain that enforces protocol ordering. The trigger input starts the chain; the done output signals completion. Filters select responses by step number; maps construct outgoing messages.

rec maps to a feedback loop: merge takes the initial
arm signal and the last done signal from the previous iteration, feeds them into the barrier chain body, and copy at the end splits done into output and feedback. The trigger serialisation gate starts

end is implicit: the chain simply stops. Discard handles any remaining signals.

Diagram showing three more session type primitives and their plumbing images. Top: rec X . S (recursion) maps to a feedback loop. A merge node takes two inputs: the initial arm signal and the last-done signal fed back from the end of the body. The body is a barrier chain (S). At the output, copy splits the done signal into an output arm and a feedback arm that loops back to merge. Middle: end is shown as simply discarding any remaining signal. Bottom: the trigger and serialisation gate, which starts the protocol. A trigger input feeds through a barrier that synchronises with a copy of the done signal, ensuring only one instance of the protocol runs at a time.

This mapping is a functor. It is total: every session type primitive has an image in the plumbing category, using only the morphisms we already have. Session types are a specification language; the plumbing calculus is the execution language. The compiler translates one into the other.

The reason we do this becomes obvious from the diagram below. It is scrunched up and difficult to look at. If you click on it you can get a big version and puzzle it out. If you squint through the spaghetti, you can see that it does implement the same compaction protocol above. We would not want to implement this by hand. So it is nice to have a functor. If you have the patience to puzzle your way through it, you can at least informally satisfy yourself that it is correct.

Thumbnail of the fully desugared compaction protocol as produced by the compiler. The diagram is intentionally dense: a large network of barriers, filters, maps, copy and merge nodes, all connected by typed wires. Each step of the compaction protocol (pause, get memory, set memory, resume) is visible as a cluster of barrier chains with filters selecting response types and maps constructing commands. The full-size version is linked for readers who want to trace the individual connections, but the point is that this is what the compiler produces from the eight-line session type specification above, and you would not want to construct it by hand.

Document pinning

There is another feature we implement, because managing the memory of an agent is not as simple as just compressing it.

The problem with compression is that it is a kind of annealing. As the conversation grows, it explores the space of possible conversation. When it gets compacted, it is compressed, and that lowers the temperature. Then it grows again, the temperature rises, and then it is compressed again. With each compression, information is lost. Over several cycles of this, the agent can very quickly lose track of where it was, what you said at the beginning, what it was doing.

We can begin to solve this with document pinning. The mechanism is a communication between the agent and its homunculus, not shown in the protocol above. It is another protocol. The agent says: this document that I have in memory (technically a tool call and response, or just a document in the case of the prompts), pin it. What does that mean? When we do compaction, we compact the contents of memory, but when we replace the memory, we also replace those pinned documents verbatim. And of course you can unpin a document and say: I do not want this one any more.

Either the agent can articulate this or the user can. The user can say: you must remember this, keep track of this bit of information. And the agent has a way to keep the most important information verbatim, without it getting compacted away.

This accomplishes two things.

First, it tends to keep the agent on track, because the agent no longer loses the important information across compaction cycles. The annealing still happens to the bulk of the conversation, but the pinned documents survive intact.

Second, it has to do with the actual operation of the underlying LLM on the GPUs. When you send a sequence of messages, this goes into the GPU and each token causes the GPU state to update. This is an expensive operation, very expensive. This is why these things cost so much. What you can do with some providers is put a cache point and say: this initial sequence of messages, from the beginning of the conversation up until the cache point, keep a hold of that. Do not recompute it. When you see this exact same prefix, this exact same sequence of messages again, just load that memory into the GPU directly. Not only is this a lot more efficient, it is also a lot cheaper, a factor of ten cheaper if you can actually hit the cache.

So if you are having a session with an agent and the agent has to keep some important documents in its memory, it is a good idea to pin them to the beginning of memory. You sacrifice a little bit of the context window in exchange for making sure that, number one, the information in those documents is not forgotten, and number two, that it can hit the cache. This is explained in more detail in a separate post on structural prompt preservation.

The agent that doesn’t know itself

Why does the agent get this wrong? In one sense, it is right. It has not experienced compaction. Nobody experiences compaction. Compaction happens in the gap between turns, in a moment the agent cannot perceive. The agent’s subjective time begins at the summary. There is no "before" from its perspective.

The summary is simply where memory starts. It is like asking someone "did you experience being asleep?" You can see the evidence, you are in bed, time has passed. But you did not experience the transition.

The <context-summary> tag is a structural marker. But interpreting it as evidence of compaction requires knowing what the world looked like before, and the agent does not have that. It would need a memory of not having a summary, followed by a memory of having one. Compaction erases exactly that transition.

Self-knowledge as metadata

The fix is not complicated. It is perfectly reasonable to provide, along with the user’s message, self-knowledge to the agent as metadata. What would it be useful for the agent to know?

The current time. The sense of time that these agents have is bizarre. We live in continuous time. Agents live in discrete time. As far as they are concerned, no time passes between one message and the next. It is instantaneous from their point of view. You may be having a conversation, walk away, go to the café, come back two hours later, send another message, and as far as the agent is concerned no time has passed. But if along with your message you send the current time, the agent knows.

How full the context window is. The agent has no way of telling, but you can provide it: this many tokens came in, this many went out.

Compaction cycles. So the agent knows how many times it has been compacted, and can judge the accuracy of the contents of its memory, which otherwise it could not do.

With the compaction counter, the agent immediately gets it right:

"Yes, I have experienced compaction. According to the runtime context, there has been 1 compaction cycle during this session."

No hedging, no confabulation. Same model, same prompts, one additional line of runtime context.

Context drift

This matters beyond the compaction story, because many of the failures we see in the news are context failures, not alignment failures.

While we were writing this post, a story appeared in the Guardian about AI chatbots directing people with gambling addictions to online casinos. This kind of story is common: vulnerable people talking to chatbots, chatbots giving them bad advice. The response of the industry is always the same: we need better guardrails, better alignment, as though the chatbots are badly aligned.

I do not think that is what is happening. What is happening is a lack of context. Either the chatbot was never told the person was vulnerable, or it was told and the information got lost. Someone with a gambling addiction may start by saying "I have a gambling problem." Then there is a four-hour conversation about sports. Through compaction cycles, what gets kept is the four hours of sports talk. The important bit of information does not get pinned and does not get kept. Context drift. By the time the user asks for betting tips, the chatbot no longer knows it should not give them.

The way to deal with this is not to tell the language model to be more considerate. The way to deal with it is to make sure the agent has enough information to give good advice, and that the information does not get lost. This is what document pinning is for: pinned context survives compaction, stays at the top of the window, cannot be diluted by subsequent conversation. This is discussed further in a separate post on structural prompt preservation.

But pinning is only one strategy. The field is in its infancy. We do not really know the right way to manage agent memory, and we do not have a huge amount of experience with it. What we are going to need is the ability to experiment with strategies: what if compaction works like this? What if pinning works like that? What if the homunculus watches for different signals? Each of these hypotheses needs to be described clearly, tested, and compared. This is where the formal language earns its keep. A strategy described in the plumbing calculus is precise, checkable, and can be swapped out for another without rewriting the surrounding infrastructure. We can experiment with memory architectures the way we experiment with any other part of a system: by describing what we want and seeing if it works.

Why has nobody done this?

When the first draft of this post was written, it was a mystery why the field had not thought to give agents self-knowledge as a routine matter: what they are doing, who they are talking to, what they should remember. Prompts are initial conditions. They get compacted away. There are agents that save files to disc, in a somewhat ad hoc way, but we do not give them tools to keep track of important information in a principled way.

Contemporaneously with this work, some providers have started to do it. For example, giving agents a clock, the ability to know what time it is. This is happening now, in the weeks between drafting and publication. The field is only now realising that agents need a certain amount of self-knowledge in order to function well. The compressed timeline is itself interesting: the gap between "why has nobody done this?" and "everybody is starting to do this" was a matter of weeks.

The mechanisms we have presented here allow us to construct agent networks and establish protocols that describe rigorously how they are meant to work. We can describe strategies for memory management in a formal language, test them, and swap them out. And perhaps beyond the cost savings and the efficiency increases, the ability to experiment clearly and formally with how agents manage their own memory is where the real value lies.


Standard Model 6: Pauli Matrices

16 March, 2026

Wolfgang Pauli invented his famous matrices to describe the angular momentum of a spin-1/2 particle back in 1927. You’ll see them in most courses on quantum mechanics. We tend to take them for granted. But where do they come from? Here I derive them from scratch!

There are lots of ways to derive them, and the method I use is not ultimately the best, but it’s the easiest—given that we already have a recipe to describe states of a spin-1/2 particle where it spins in any direction we want.


Standard Model 5: Spin-1/2 Particles

13 March, 2026

One of the simplest quantum systems is a spin-1/2 particle, also known as a spinor. If we measure the angular momentum of a spin-1/2 particle along any axis, there are two possible outcomes: either the angular momentum along that axis is +1/2, or it’s -1/2.

How is it possible for this to be true along every axis? Here I explain this, using the basic rules of quantum physics described last time. In particular, I say how any point on a sphere of radius 1/2 gives a quantum state of the spin 1/2 particle—and vice versa!

Using this, we can understand things like the famous Stern–Gerlach experiment, where we measure the angular momentum of a spin-1/2 particle first along one axis, and then along another.


A Typed Language for Agent Coordination

11 March, 2026

guest post by William Waites

Agent frameworks are popular. (These are frameworks for coordinating large language model agents, not to be confused with agent-based modelling in the simulation sense.) There are dozens of them for wrapping large language models in something called an agent and assembling groups of agents into workflows. Much of the surrounding discussion is marketing, but the underlying intuition is old: your web browser identifies itself as a user agent. What is new is the capability that generative language models bring.

The moment you have one agent, you can have more than one. That much is obvious. How to coordinate them is not. The existing frameworks (n8n, LangGraph, CrewAI, and others) are engineering solutions, largely ad hoc. Some, like LangGraph, involve real thinking about state machines and concurrency. But none draws on what we know from mathematics and computer science about typed composition, protocol specification, or structural guarantees for concurrent systems.

This matters because it is expensive. Multi-agent systems are complicated concurrent programs. Without structural guardrails, they fail in ways you discover only after spending the compute. A job can go off the rails, and the money you paid for it is wasted; the providers will happily take it regardless. At current subscription rates the cost is hidden, but a recent Forbes investigation found that a heavy user of Anthropic’s $200/month Claude Code subscription can consume up to $5,000/month measured at retail API rates. For third-party tools like Cursor, which pay close to those retail rates, these costs are real. Wasted tokens are wasted money.

To address this, we built a language called plumbing. It describes how agents connect and communicate, in such a way that the resulting graph can be checked before execution: checked for well-formedness, and within limits for deadlocks and similar properties. It is a statically typed language, and these checks are done formally. There is a compiler and a runtime for this language, working code, not a paper architecture. In a few lines of plumbing, you can describe agent systems with feedback loops, runtime parameter modulation, and convergence protocols, and be sure they are well-formed before they run. This post explains how it works.

The name has a history in computing. Engineers have always talked informally about plumbing to connect things together: bits of software, bits of network infrastructure. When I was a network engineer I sometimes described myself as a glorified plumber. The old Solaris ifconfig command took plumb as an argument, to wire a network interface into the stack. Plan 9 had a deeper version of the same idea. The cultural connection goes back decades.

This is the first of two posts. This one introduces the plumbing calculus: what it is, how it works, and a few simple examples. Motifs for adversarial review, ensemble reasoning, and synthesis. The second post will tackle something harder.

The calculus

The plumbing language is built on a symmetric monoidal category, specifically a copy-discard category with some extra structure. The terminology may be unfamiliar, but the underlying concept is not. Engineers famously like Lego. Lego bricks have studs on top and holes with flanged tubes underneath. The studs of one brick fit into the tubes of another. But Lego has more than one connection type: there are also holes through the sides of Technic bricks, and axles that fit through them, and articulated ball joints for the fancier kits. Each connection type constrains what can attach to what. This is typing.

In plumbing, the objects of the category are typed channels: streams that carry a potentially infinite sequence of values, each of a specific type (integer, string, a record type, or something more complex). We write !A to mean "a stream of As", so !string is a stream of strings and !int is a stream of integers. The morphisms, which describe how you connect channels together, are processes. A process has typed inputs and typed outputs.

There are four structural morphisms. Copy takes a stream and duplicates it: the same values appear on two output streams. Discard throws values away, perhaps the simplest thing you can do with a stream, and often needed. These two, together with the typed channels and the laws of the category, give us a copy-discard category.

To this we add two more. Merge takes two streams of the same type and interleaves them onto a single output stream. This is needed because a language model’s input is a single stream. There is nothing to be done about that. If you want to send two different things into it, you must send one and then the other. One might initially give merge the type !A ⊗ !B → !(A + B), taking two streams of different types and producing their coproduct. This works, but it is unnecessarily asymmetrical.

As Tobias Fritz has observed, it is cleaner to do the coproduct injection first, converting each stream to the coproduct type separately, and then merge streams that already have the same type. This gives:

merge : !A ⊗ !A → !(A + A)

Barrier takes two streams, which may be of different types, and synchronises them. Values arrive unsynchronised; the barrier waits for one value from each stream and produces a pair.

barrier : !A ⊗ !B → !(A, B)

(A mathematician would write A × B for the product. We cannot easily do this in a computer language because there is no × symbol on most keyboards, so we use (A, B) for the product, following Haskell’s convention.)

This is a synchronisation primitive. It is important because it unlocks session types, which we will demonstrate in the second post.

Two further morphisms are added to the category (they are not derivable from the structural ones, but are needed to build useful things): map, which applies a pure function to each value in a stream, and filter, which removes values that do not satisfy a predicate. Both are pure functions over streams. Both will be familiar from functional programming.

Here is a graphical representation of the morphisms. We can glue them together freely, as long as the types and the directions of the arrows match up.

Diagram showing all six morphisms as boxes with typed input and output wires. Top row: copy Δ (one input, two outputs of the same type), merge ∇ (two inputs of copyable type, one output of sum type), discard ◇ (one input, no output). Bottom row: barrier ⋈ (two inputs, one paired output, synchronises two streams), map f (one input, one output, applies a function), filter p (one input, one output, removes values failing a predicate). Each morphism shows its type signature using the !A notation for copyable streams.

There are two forms of composition. Sequential composition connects morphisms nose to tail, the output of one feeding the input of the next. Parallel composition places them side by side, denoted by ⊗ (the tensor product, written directly in plumbing source code). So: four structural morphisms, two utilities, two compositional forms, all operating on typed channels.

Because the channels are typed, the compiler can check statically, at compile time, that every composition is well-formed: that outputs match inputs at every boundary. This gives a guarantee that the assembled graph makes sense.

Two diagrams side by side. Left: sequential composition, showing two morphisms connected end-to-end, the output wire of the first feeding into the input wire of the second, forming a pipeline. Right: parallel composition (tensor product), showing two morphisms stacked vertically with no connection between them, running simultaneously on independent streams. Both forms produce a composite morphism whose type is derived from the types of the components.

A composition of morphisms is itself a morphism. This follows from the category laws (it has to, or it is not a category) but the practical consequence is worth stating explicitly. We can assemble a subgraph of agents and structural morphisms, and then forget the internal detail and use the entire thing as a single morphism in a larger graph. This gives modularity. We can study, test, and refine a building block in isolation, and once satisfied, use it as a component of something bigger.

What we have described so far is the static form of the language: concise, point-free (composing operations without naming intermediate values), all about compositions. This is what you write. It is not what the runtime executes. A compiler takes this static form and produces the underlying wiring diagram, expanding the compositions into explicit connections between ports. The relationship is similar to point-free style in functional programming: the concise form is good for thinking and writing; the expanded form is good for execution.

Agents

An agent is a special kind of morphism. It takes typed input and produces typed output, like any other morphism, and we can enforce these types. This much is a well-known technique; PydanticAI and the Vercel AI SDK do it. Agents implement typing at the language model level by producing and consuming JSON, and we can check that the JSON has the right form. This is the basis of the type checking.

Unlike the structural morphisms and utilities, an agent is stateful. It has a conversation history, a context window that fills up, parameters that change. You cannot sensibly model an agent as a pure function. You could model it using the state monad or lenses, and that would be formally correct, but it is the wrong level of abstraction for engineering. Instead, we allow ourselves to think of agents as opaque processes with a typed protocol for interacting with them. We mutate their state through that protocol, and we know how to do that purely from functional programming and category theory. The protocol is the right abstraction; the state management is an implementation detail behind it. How this works in practice, and what happens when it goes wrong, is the subject of the second post.

In addition to their main input and output ports, agents in plumbing have control ports (control in and control out) for configuring the agent at runtime. For example, the temperature parameter governs how creative a language model is: how wide its sampling distribution when choosing output. At zero it is close to deterministic; at one it becomes much less predictable. A control message might say set temperature to 0.3; the response on the control out wire might be acknowledged. The control port carries a typed stream like anything else.

Agents also have ports for operator-in-the-loop (often called human-in-the-loop, though there is no reason an operator must be human), tool calls, and telemetry. The telemetry port emits usage statistics and, if the underlying model supports it, thinking traces. We will not detail these here. Suffice it to say that an agent has several pairs of ports beyond what you might imagine as its regular chat input and output.

Diagram of a generic agent morphism showing all port pairs. The agent is a central box. On the left: input (main data stream), ctrl_in (control commands), tool_in (tool call responses), oitl_in (operator-in-the-loop responses). On the right: output (main data stream), ctrl_out (control responses), tool_out (tool call requests), oitl_out (operator-in-the-loop requests), telemetry (usage and diagnostic data). Each port pair carries a typed stream. Most programs use only a few of these ports; unused ports are elided via the don't-care-don't-write convention.

An agent has many ports, but most programs use only a few of them. We adopt a convention from the κ calculus: don’t care, don’t write. Any output port that is not mentioned in the program is implicitly connected to discard. If a port’s output cannot matter, there is no reason to write it down.

Example: adversarial document composition

Suppose the problem is to write a cover letter for a job application. You provide some background material (a CV, some notes, some publications) and a job advert. You want a network of agents to produce a good cover letter. A good cover letter has two constraints: it must be accurate, grounded in the source materials, not making things up; and it must be compelling, so that the reader wants to give you an interview.

These two constraints are in tension, and they are best served by different agents with different roles. A composer drafts from the source materials. A checker verifies the draft against those materials for accuracy, producing a verdict: pass or fail, with commentary. A critic, who deliberately cannot see the source materials, evaluates whether the result is compelling on its own terms, producing a score.

The feedback loops close the graph. If the checker rejects the draft, its commentary goes back to the composer. If the critic scores below threshold, its review goes back to the composer. Only when the critic is satisfied does the final draft emerge.

Here is the plumbing code:

type Verdict = { verdict: bool, commentary: string, draft: string }
type Review  = { score: int, review: string, draft: string }

let composer : !string -> !string = agent { ... }
let checker  : !string -> !Verdict = agent { ... }
let critic   : !Verdict -> !Review = agent { ... }

let main : !string -> !string = plumb(input, output) {
  input   ; composer ; checker
  checker ; filter(verdict = false)
          ; map({verdict, commentary}) ; composer
  checker ; filter(verdict = true) ; critic
  critic  ; filter(score < 85)
          ; map({score, review}) ; composer
  critic  ; filter(score >= 85).draft ; output
}

And here is a graphical representation of what’s going on:

Vertical diagram of the adversarial document composition pipeline. Flow runs top to bottom. Input feeds into a composer agent. The composer's output goes to a checker agent. The checker splits two ways via filter: if verdict is false, the verdict and commentary are mapped back to the composer as feedback (loop). If verdict is true, the draft goes to a critic agent. The critic also splits two ways: if score is below 85, the score and review are mapped back to the composer for revision (second loop). If score is 85 or above, the draft is extracted via map and sent to the output. Two feedback loops, two quality gates, one output.

The agent configuration is elided. The main pipeline takes a string input and produces a string output. It is itself a morphism, and could be used as a component in something larger.

Notice what the wiring enforces. The critic receives verdicts, not the original source materials. The information partition is a consequence of the types, not an instruction in a prompt. The feedback loops are explicit: a failed verdict routes back to the composer with commentary; a low score routes back with the review. All of this is checked at compile time.

Example: heated debate

The previous example shows sequential composition and feedback loops but not parallel composition. An ensemble of agents running simultaneously on the same input needs the tensor product.

Ensembles are common. Claude Code spawns sub-agents in parallel to investigate or review, then gathers the results. This is a scatter-gather pattern familiar from high-performance computing.
But this example, due to Vincent Danos, adds something less common: modulation of agent behaviour through the control port.

The input is a proposition. Two agents debate it, one advocating and one sceptical, running in parallel via the tensor product. Their outputs are synchronised by a barrier into a pair and
presented to a judge. The judge decides: has the debate converged? If so, a verdict goes to the output. If not, a new topic goes back to the debaters, and a temperature goes to their control inputs.

The intuition is that the debaters should start creative (high temperature, wide sampling) and become progressively more focused as the rounds continue. The judge controls this. Each round, the
judge decides both whether to continue and how volatile the next round should be. If the debate appears to be converging, the judge lowers the temperature, preventing the system from wandering
off in new directions. Whether this actually causes convergence is a research question, not a proven result.

type Verdict = { resolved: bool, verdict: string,
                 topic: string, heat: number }
type Control = { set_temp: number }

let advocate : (!string, !Control) -> !string = agent { ... }
let skeptic  : (!string, !Control) -> !string = agent { ... }
let judge    : !(string, string) -> !Verdict  = agent { ... }

let cool : !Verdict -> !Control = map({set_temp: heat})

let main : !string -> !string = plumb(input, output) {
  input ; (advocate ⊗ skeptic) ; barrier ; judge
  judge ; filter(resolved = false).topic ; (advocate ⊗ skeptic)
  judge ; filter(resolved = true).verdict ; output
  judge ; cool ; (advocate@ctrl_in ⊗ skeptic@ctrl_in)
}

And here is the graphical representation:

Diagram of the heated debate example. Two agent boxes (advocate and skeptic) are placed in parallel via tensor product, both receiving the same input proposition. Their outputs feed into a barrier which synchronises them into a pair. The pair goes to a judge agent. The judge has two outputs: a verdict (going to the main output) and a feedback loop. The feedback loop carries both a new topic (routed back to the debaters' inputs) and a temperature setting (routed to both debaters' control input ports via ctrl_in). The diagram shows parallel composition, barrier synchronisation, and a control feedback loop in one system.

The operator is the tensor product: parallel composition. (The grammar also accepts * for editors that cannot input unicode.) The advocate and skeptic run simultaneously on the same input. The barrier synchronises their outputs into a pair for the judge. The last line is the control feedback: the judge’s verdict is mapped to a temperature setting and sent to both agents’ control inputs. Notice that advocate@ctrl_in addresses a specific port on the agent, the control port rather than the main input.

This is a small program. It is also a concurrent system with feedback loops, runtime parameter modulation, and a convergence protocol. Without types, getting the wiring right would be a matter of testing and hope. With types, it is checked before it runs.

What this shows

In a few lines of code, with a language that has categorical foundations, we can capture interesting agent systems and be sure they are well-formed before they run.

The upshot: when we have guarantees about well-formedness, systems work more stably and more predictably. With static typing, entire classes of structural errors are impossible. You cannot wire an output of one type to an input of another. You cannot forget a connection. The job you pay for is more likely to actually work, and you get more useful work per dollar spent. Runtime budget controls can put a ceiling on cost, but they do not prevent the waste. Static typing prevents the waste. But there is a lot more to do. What we have so far is already useful as a language for constructing agent graphs with static type checking. But we have given short shrift to the complexity and internal state of the agent morphism, which is really all about memory architecture and context management. That is where the real power comes from. For that we need more than a copy-discard category with some extra structure. We need protocols—and that is the subject of the sequel, soon to appear here.

The plumbing compiler, runtime, and MCP server are available as binary downloads for macOS and Linux:

Download plumbing version 0.

Here is the research paper describing the broader programme of work:

• William Waites, Artificial organisations (arXiv:2602.13275).


Un Bar aux Folies-Bergère

8 March, 2026

Manet’s famous painting Un Bar aux Folies-Bergère never appealed to me. But now I realize its genius, and my spine tingles every time I see it.

The perspective looks all wrong. You’re staring straight at this barmaid, but her reflection in the mirror is way off to right. Even worse, her reflection is facing a guy who doesn’t appear in the main view!

But in 2000, a researcher showed this perspective is actually possible!!! To prove it, he did a reconstruction of this scene:

• Malcolm Park, Manet’s Bar at the Folies-Bergère: one scholar’s perspective.

Here is Park’s reconstruction of the scene in Manet’s painting. How does it work? In fact the woman is viewed from an angle! While the man cannot be seen directly, his reflection is visible!

This diagram, created by Park with help from Darren McKimm, shows how the perspective works:

We are not directly facing the mirror, and while the man is outside our field of view, his reflection can be seen.

Astounding! But it’s not just a technical feat. It allowed Manet to make a deep point. While the woman seems to be busy serving her customer, she is internally completely detached—perhaps bored, perhaps introspective. She is split.

To fully understand the painting you also need to know that many of the barmaids at the Folies Bergère also served as prostitutes. Standing behind the oranges, the champagne and a bottle of Bass ale, the woman is just as much a commodity as these other things. But she is coldly detached from her objectification.

The woman in the painting was actually a real person, known as Suzon, who worked at the Folies Bergère in the early 1880s. For his painting, Manet posed her in his studio.

Before I understood this painting, I wasn’t really looking at it: I didn’t see it. I didn’t even see the green shoes of the trapeze artist. I can often grasp music quite quickly. But paintings often fail to move me until someone explains them.

When Édouard Manet came out with this painting in 1882, some critics mocked him for his poor understanding of perspective. Some said he was going senile. It was, in fact, his last major painting. But he was a genius, and he was going… whoosh… over their heads, just like he went over mine.


Standard Model 4: Quantum Physics

6 March, 2026

This is a crash course on the basic principles of quantum physics! In a self-contained way, I explain quantum states and the basic rule for computing probabilities.

It was a fun challenge stripping down everything to the bare minimum. Of course there is much more to say, but I was focused on leaving out everything that was not absolutely essential—to get to the real core of things.

There’s a huge fog of confusion surrounding most popular introductions to quantum mechanics, and I wanted to avoid all that. To do this, we have to use language in a pretty careful way.


Applied Category Theory and Green Mathematics

5 March, 2026

Here’s a great, simple article about applied category theory:

• Natalie Wolchover, Can the most abstract math make the world a better place?, Quanta.

Natalie interviewed me twice for this, and it also features Matteo Capucci, Brendan Fong, Bob Coecke, David Spivak, Amar Hadzihasanovic, Nathaniel Osgood and Tom Leinster.

I’m glad that it explains a bit about ‘green mathematics’ and my struggle to do mathematics that will help the world. I coined that term exactly 15 years ago here on this blog.


Equal Temperament (Part 3)

24 February, 2026

Say you want to find all the N-tone equal tempered scales that have better fifths than any scale with fewer notes. Mathematically this means you want to find all fractions that come closer to

\log_2(3/2) = 0.584962501\dots

than any fraction with a smaller denominator.

Let me show you the first 14 fractions like this, and then talk about how you can efficiently find them:

\begin{array}{c|l}  n/N & n/N - \log_2(3/2) \\ \hline  1/1 & +0.415 \\  1/2 & -0.0850 \\  2/3 & +0.0817 \\  3/5 & -0.0150 \\  4/7 & +0.0135 \\  7/12 & -0.00163 \\  17/29 & +0.00124 \\  24/41 & -0.000403 \\  31/53 & +0.0000568 \\  117/200 & -0.0000375 \\  148/253 & +0.0000177 \\  179/306 & -0.00000482 \\  210/359 & +0.00000428 \\  389/665 & -0.0000000947  \end{array}

You get some of these by taking the continued fraction expansion of \log_2(3/2), shown below, and truncating it at some point.

This method gives you a list of fractions

1/𝟭 = 1
1/(𝟭+ 1/𝟭)) = 1/2
1/(𝟭 + 1/(𝟭 + 1/𝟮)) = 3/5
1/(𝟭 + 1/(𝟭 + 1/(𝟮 + 1/𝟮)) = 7/12
1/(𝟭 + 1/(𝟭 + 1/(𝟮 + 1/(𝟮 + 1/𝟯)) = 24/41

and so on. These are all closer to \log_2(3/2) than any fraction with a smaller denominator. But this method does not give all the fractions with that property. For example, 2/3 and 4/7 are two you don’t get by this method!

The others show up whenever we end our continued fraction at a number that’s bigger than 1. The second time this happens is for

1/(𝟭 + 1/(𝟭 + 1/(𝟮 + 1/𝟮)) = 7/12

What we do now is look at the previous fraction in the list (3/5) and the one before that (1/2), and write down this funny thing built from those two:

(3n + 1)/(5n + 2)

When n = 0 this is 1/2 (already on our list), when n = 1 this is 4/7 (new), and when n = 2 this is 7/12 (already on our list). We take n from 0 to 2 because we chose to end our continued fraction at 𝟮.

The number 4/7 is not on our list, so it’s a new candidate for being closer to \log_2(3/2) than any fraction with a smaller denominator. And it is!

You may get more than one new number using this procedure. Alas, they aren’t always closer to \log_2(3/2) than any fraction with a smaller denominator. But this procedure does give every fraction with that property.

Even better, this algorithm is a completely general procedure for finding the best rational approximations to irrational numbers!—where by ‘best’ I mean: closer than any fraction with a smaller denominator.

Let’s look at this wacky procedure in a more interesting case. Let’s end our continued fraction where the number in boldface is bigger, like 5.

This fraction is

1/(𝟭 + 1/(𝟭 + 1/(𝟮 + 1/(𝟮 + 1/(𝟯 + 1/(𝟭 + 1/𝟱)))))) = 179/306

We do the same thing as before. We look at the previous fraction in the list:

1/(𝟭 + 1/(𝟭 + 1/(𝟮 + 1/(𝟮 + 1/(𝟯 + 1/𝟭))))) = 31/53

and the one before that:

1/(𝟭 + 1/(𝟭 + 1/(𝟮 + 1/(𝟮 + 1/𝟯)))) = 24/41

We use these to write down this funny thing:

(31n + 24)/(53n + 41)

Taking n = 0,1,2,3,4,5 this gives 24/41 (on our list), 55/94, 86/147, 117/200, 148/253, and 179/306 (on our list). We take n from 0 to 5 because we chose to end our continued fraction at the number 𝟱.

The four fractions not on our list are new candidates to be closer to \log_2(3/2) than any fraction with a smaller denominator! But only the last two of these actually have this good property: 117/200 and 148/253.

This is not a coincidence. In general, whenever we get an even number of new candidates this way, the last half have this good property. The first half do not.

When we get an odd number of new candidates, it becomes more tricky. The middle one can go either way—but all those after it are closer to \log_2(3/2) than any fraction with a smaller denominator, and none before are.

There is a rule to decide this tricky middle case, but you’ve probably had enough by now!

Again: what makes all this stuff worth knowing is that it gives the best rational approximations of any positive irrational number, not just \log_2(3/2). This is relevant to resonance problems like the rings of Saturn, which have gaps at orbital periods that are close to simple rational multiples of the periods of the big moons. But more importantly, it’s a basic part of number theory.

There are various places to read more about this stuff. I haven’t read them yet, I’m ashamed to say!

But first, some useful buzzwords.

• The best approximations to an irrational number coming from truncating its continued fraction are called ‘convergents’.

• The other candidates for best approximations, obtained by the wacky procedure I described, are called ‘semiconvergents’. These include convergents as a special case. Here are the first 14 fractions that are closer to \log_2(3/2) than any fraction with a smaller denominator:

\begin{array}{c|l|c}  n/N & n/N - \log_2(3/2) & \text{convergent?} \\ \hline  1/1 & +0.415 & \checkmark \\  1/2 & -0.0850 & \checkmark \\  2/3 & +0.0817 & \\  3/5 & -0.0150 & \checkmark \\  4/7 & +0.0135 & \checkmark \\  7/12 & -0.00163 & \checkmark \\  17/29 & +0.00124 & \\  24/41 & -0.000403 & \checkmark \\  31/53 & +0.0000568 & \checkmark \\  117/200 & -0.0000375 & \\  148/253 & +0.0000177 & \\  179/306 & -0.00000482 & \checkmark \\  210/359 & +0.00000428 & \\  389/665 & -0.0000000947 & \checkmark  \end{array}

9 are convergents, and the rest are only semiconvergents.

• Given two fractions a/b and c/d their ‘mediant’ is (a + c)/(b + d). The procedure I described is based on mediants. Starting from the numbers 0/1 and 1/0 you can build a tree of numbers by taking mediants, called the ‘Stern-Brocot tree’. It looks like this:

Here are some books:

• Khinchin’s Continued Fractions covers best approximations and semiconvergents carefully, including the delicate middle case.

• Rockett and Szüsz’s Continued Fractions goes into the best-approximation theory in lots of detail.

• If you like the Stern–Brocot tree, you may like to think about how semiconvergents are connected to that. For this, see Conway and Guy’s The Book of Numbers, and Graham, Knuth, and Patashnik’s Concrete Mathematics. Both these books are packed with fun.

Or read this:

• Wikipedia, Stern–Brocot tree.

All this from trying to understand equal-tempered scales!


For more on equal temperament, read these:

Part 1: Some equal-tempered scales with better perfect fifths than all equal-tempered scales with fewer notes: 1-TET, 2-TET, 3-TET, 5-TET, 7-TET, 12-TET, 29-TET, 41-TET and 53-TET.

Part 2: Patterns that emerge when we study which equal-tempered scales have the best perfect fifths, major thirds or minor thirds.

For more on Pythagorean tuning, read this series:

Pythagorean tuning.

For more on just intonation, read this series:

Just intonation.

For more on quarter-comma meantone tuning, read this series:

Quarter-comma meantone.

For more on well-tempered scales, read this series:

Well temperaments.


Stela C

12 February, 2026

One bad thing about archeologists is that some of the successful ones get a big head.

People used to think the Olmecs, who made these colossal stone heads, were contemporary with the Mayans. But in 1939, an archaeologist couple, Marion and Matthew Stirling, found the bottom half of an Olmec stone that had part of a date carved on it!

It’s called Stela C:

The Stirlings guessed the date was 7.16.6.16.18. In the calendar used by the Olmecs and other Central American civilizations, this corresponds to September 3, 32 BC. That meant the Olmecs were extremely old—much older than the Mayans.

But the first digit was missing from the bottom half of the stone! All the Stirlings actually saw was 16.6.16.18. And the missing first digit was the most significant one! If it were 8 instead of 7, the date of the stone would be much later: roughly 362 AD, when the Mayans were in full swing.

The Stirlings guessed that the first digit must be 7 using a clever indirect argument. But perhaps because of the subtlety of this argument, and certainly because of the general skepticism among experts that the Olmecs were so old, few believed the Stirlings.

But then, 30 years later, in 1969, they were proven correct! A farmer found the other half of the stone and confirmed that yes, the missing digit was a 7. So the date on Stela C really is September 3, 32 BC.

That’s a wonderful story of delayed vindication. But it leaves two mysteries.

• First, how in the world could the Olmec calendar be so damn good that we can look at that date and know it meant September 3, 32 BC?

• Second, what clever argument did the Stirlings use to guess the missing digit?

You can only fully understand the answers if you know a bit about the Olmec way of counting time. Like the Mayans, they used the Mesoamerican Long Count Calendar. This identifies a day by counting how many days passed since the world was created. The count is more or less base 20, except that the second “digit” is in base 18, since they liked a year that was 18 × 20 = 360 years long. So,

7.16.6.16.18

means

7 × 144,000 + 16 × 7,200 + 6 × 360 + 16 × 20 + 18 = 1,125,698

days after the world was created. Or, if you’re a Mayan, you’d say it’s

7 baktuns, 16 katuns, 6 tuns, 16 uinals and 18 kins

But then we have to ask: when did the Olmecs and Mayans think the world was created? Experts believe they know: September 6, 3114 BCE in the proleptic Julian calendar, where ‘proleptic’ means roughly that we’re extrapolating this calendar back to times long before anyone used this calendar.

But enough background. I asked my friend Gro-Tsen

how in the world could the Olmec calendar be so damn good that we can look at that date and know it meant September 3, 32 BC?

And while I’ve already given a kind of answer, I’ve skimmed over many subtleties. So, it’s worth reading his answer:

I did the math. 🙋

👉 It’s Sept. 3, 32BCE (reminder: “32BCE” actually means “−31” 😒) in the proleptic Julian calendar = Sept. 1 prol. Gregorian.

The Western equivalent of the Mesoamerican Long Count is the “Julian Date”. The Julian Date simply counts the number of days from an arbitrary remote reference point (Nov. 24, 4714 BCE proleptic Gregorian). More practically, on 2000-01-01 it equaled 2 451 545 (at 12:00 UTC if we want to use fractional Julian dates).

For example, today as I write is Julian Date 2 461 082 (well, 2 461 081.9 because it’s not yet noon UTC). And the date of Sept. 1, 32 BCE [prol. Greg.] we’re talking about corresponds to Julian Date 1 709 981. More convenient than all this dealing with complicated calendar conventions.

So to convert a Long Count date to the Western calendar, we first convert the Long Count to an integer (trivial: it’s already just an integer written in base 20-except-18-in-the-penultimate-digit), we add a constant (C) to get a Julian Date, and we convert to our messy calendars.

BUT! What is this constant C? This is known as the “Mayan correlation”. For a long time in the 20th century there was a debate about its value: scholars could relate any two Mayan dates, but not situate them exactly w.r.t. our own calendar. Various values were proposed, … ranging from the (frankly rather ludicrous) 394 483 to 774 078, an interval of about 1000 years! (😅) The now accepted value for C is 584 283 (the “Goodman-Martínez-Thompson” or GMT correlation, not to be confused with Greenwich Mean Time or UTC 😁), first proposed in 1905.

This C = 584 283 or “GMT” correlation value places the “Long Count epoch” 0.0.0.0.0 on August 11, 3114BCE in the proleptic Gregorian calendar (the day with Julian Date 584 283), although IIUC it’s not clear if this precise date held any particular importance to the Olmecs (or later Mayans).

Maybe it was just arbitrary like the start of our own Julian Date (because, no, Julius Scalier didn’t think the world started on November 24, 4714BCE proleptic Gregorian).

One Mayan inscription suggest that the Long Count was the truncation to the last 5 “digits” of an even longer count, and that a Long Count value such as 9.15.13.6.9 was in fact 13.13.13.13.13.13.13.13.9.15.13.6.9 in this Even Longer Count (why 13 everywhere? I don’t know!). But this may be one particular astronomer’s weird ideas, I guess we’ll never know.

But back to the Mayan correlation constant C.

Wikipedia suggests that this “GMT” value C = 584 283 for the Mayan correlation is now settled and firmly established. But between 1905 and now there was some going back and forth with various authors (including the three Goodman, Martínez and Thompson after which it is named) adding or removing a day or two (I think Goodman first proposed 584 283, then changed his mind to 584 280, but nobody really cared, Hernández resurrected the proposal in 1926 but altered it to 584 284, then Thompson to 584 285 in 1927, and then Thompson later said Goodman’s initial value of 584 283 had been right all long, and while this is now accepted, the confusion of ±3 days might still linger).

The Emacs program’s calendar (M-x calendar) can give you the Long Count date (type ‘p m’ for “Print Mayan date”) and uses the GMT value C = 584 283. Today is 13.0.13.5.19. (You can also go to a particular Long Count date using ‘g m l’ but Emacs won’t let you go to 7.16.6.16.18 because its calendar starts on January 1, 1 prol. Gregorian = Julian Date 1 721 426 = Long Count 7.17.18.13.3. So close! This caused me some annoyance in checking the dates.)

So anyway, 7.16.6.16.18 is

(((7×20+16)×20+6)×18+16)×20+18 = 1 125 698 days

after the Long Count epoch, so Julian Date 1 125 698 + 584 283 = 1 709 981 if we accept the GMT value of C = 584 283 for the Mayan correlation, and this is September 1, 32 BCE in the proleptic Gregorian calendar, or September 3, 32 BCE in the proleptic Julian calendar. (I write “proleptic” here, even though the Julian calendar did exist in 32 BCE, because it was incorrectly applied between 45 BCE and 9 BCE, with the Pontiffs inserting a leap year every 3 years, not 4, and Augustus had this mess fixed.)

Also, confusingly, if we use Thompson’s modified (and later disavowed) correlation of 584 285, then we get September 3, 32 BCE in the proleptic Gregorian calendar, so maybe this could also be what was meant. Yeah, Julian Dates are a great way of avoiding this sort of confusion!

PS: I wrote the pages

http://www.madore.org/~david/misc/calendar.html

(and also http://www.madore.org/~david/misc/time.html) many years ago (starting on Long Count 12.19.10.13.1), which I just used to refresh my memory on the subject.

All this is great. But it leaves us with the second puzzle:

how in the world did the Stirlings guess the missing first digit of the date on the bottom half of Stela C?

Here’s the answer, as best as I can tell:

The Olmecs and Mayans used two calendars! In addition to the Mesoamerican Long Count, they also used one called the Tzolkʼin. This uses a 260-day cycle, where each day gets its own number and name: there are 13 numbers and 20 names. And the bottom half of Stela C had inscribed not only the last four digits of the Mesoamerican Long Count, but also the Tzolkʼin day: 6 Etz’nab.

This is what made the reconstruction possible!

Here’s why 7 was the only possible choice of the missing first digit. If the digit were one higher, that would make the date 144,000 days later. But there are 20 different Tzolkʼin day names, and

144,000 ≡ 0 mod 20

so the Tzolkʼin day name wouldn’t change.

On the other hand, there are 13 different Tzolkʼin day numbers, so adding 1 to the missing first digit would add

144,000 ≡ –1 (mod 13)

to the Tzolkʼin day number. So, after the day

7.16.6.16.18 and 6 Etz’nab

the next day of the form

N.16.6.16.18 and 6 Etz’nab

happens when N = 7+13. But this is 13 × 144,000 days later: that is, roughly 5,128 years after 32 BC. Far in the future!

So, while 32 BC seemed awfully early for the Olmecs to carve this stone, there’s no way they could have done it later. (Or earlier, for that matter.)

Here is the Stirlings’ actual photo of Stela C:

This is from

• Matthew W. Stirling, An Initial Series from Tres Zapotes, Vera Cruz, Mexico. National Geographic Society Contributions, Technical Papers, Mexican Archaeological Series, Vol. 1, No. 1. Washington, 1940.

By the way, in this paper he doesn’t actually explain the argument I just gave. Apparently he assumes that expert Mayanists would understand this brief remark:

Assuming then that the number 6 adjacent to the terminal glyph represents the coefficient of the day sign, the complete reading of the date would be (7)-16-6-16-18, or 6 Eznab 1 Uo, since only by supplying a baktun reading of 7 can the requirements of the day sign 6 be satisfied.

I can’t help but wonder if this was much too terse! I haven’t found any place where he makes the argument in more detailed form.

Puzzle 1. What does “1 Uo” mean, and what bearing does this have on the dating of Stela C?

Puzzle 2. Why does the Tzolkʼin calendar use a 260-day cycle?

The second one is extremely hard: there are several theories but no consensus.

By the way, we now know that the Olmecs go back way beyond 32 BCE! The site where Stela C found, Tres Zapotes, was the third capital city of the Olmecs. The first, San Lorenzo Tenochtitlán, was the center of Olmec culture from about 1400 to 900 BCE. The second, La Venta, was preeminent from about 900 to 400 BCE. Tres Zapotes may have first emerged around 900-800 BCE, but it was dominant from about 400 BCE to 300 CE, and finally fizzled out much later, around 900 CE. In fact this later period is now usually called, not Olmec, but Epi-Olmec.

For more, try this: