← Analysis Reports index · Main report

Raw data: the 200-word description experiment

Everything the main report (Sec. 1 to Sec. 6) is computed from: the exact prompts, example task inputs, every description, every attempt and final message of each reconstruction and control session, token usage per run, per-example disagreements on the four dataset tasks, and the full metric table. Programs themselves are files in the repo: experiments/nl-bottleneck/originals/, recon_final/, recon_control_final/; compressed agent transcripts in recon/*/agent_log.jsonl.gz and recon_control/*/. Contents: Sec. 1 prompts, Sec. 2 example inputs, Sec. 3 per-task descriptions and runs, Sec. 4 per-example disagreements, Sec. 5 tokens, Sec. 6 full metrics.

1. Prompts, verbatim

Placeholders in angle brackets are filled per task. All calls used claude -p --model claude-opus-5-5 --restricted --strict-mcp-config; the summarizer had --tools Read only; reconstructors and controls had Read, Edit, Write, Glob, Grep and Bash, with Bash allowed only for the scoring command.

Summarizer prompt
Below is a task, and a file model.py in your working directory. model.py is a small transformer whose weights were written by hand (no training) to solve the task; the file builds the model and sets every weight in closed form.

TASK: <task name>
<task docstring from src/task.py>
Prompts are fed token by token (character level); the model generates the answer autoregressively with greedy decoding and is scored by exact match.

Read model.py. Then write a description of what this model learned/implements, for an engineer who will have to rebuild a model with similar accuracy AND similar mistakes from scratch, without ever seeing model.py or its numbers. Rules:
- At most 200 words, plain prose, no code blocks, no pasted tensors.
- Describe the algorithm the network implements (what attention does, what the MLP or readout does, how the answer is produced), not the raw weights. Practical hints are welcome (e.g. "look up bigrams", which features or words matter, thresholds, scale constants that matter, how many layers/heads).
- Say where the model is known to be imperfect or approximate, if it is, so its error pattern can be matched.
Output only the description.

If over 200 words, a follow-up in the same session: "That is N words. Rewrite it in at most 190 words, keeping the most important information. Output only the description."

Reconstructor prompt (with description)
You are rebuilding a hand-written transformer. An earlier agent wrote, with no training, the weights of a small transformer for the task `<task>`. You will not see that model. All you get is the description below, written by another engineer who did read it. Your job is to hand-write a transformer that implements the same algorithm, so that its accuracy AND its mistakes resemble the original's.

TASK:
<task docstring>
Prompts are fed token by token (character level); the model generates the answer autoregressively with greedy decoding; score = exact-match accuracy on 200 fixed examples (seed 0). The original model scored <original visible accuracy> on these 200 examples and has <original params> parameters.

DESCRIPTION OF THE ORIGINAL MODEL (<n> words):
"""
<description>
"""

Your working folder has interpretable_transformer.py (a template transformer; edit the architecture and `write_weights(model, task)` so every parameter is set directly in closed form), src/task.py and src/eval.py (read-only task and scoring code), and results/.
Rules:
- NO training of any kind: no gradient steps, optimizers, fitting loops, least squares or searches over weights. Parameters are set in closed form from the algorithm.
- Do not compute the answer with Python outside the transformer; the network must produce it autoregressively. Do not use pretrained weights.
- Do not fit weights, lexicons or thresholds to examples or labels from the dataset (for dataset tasks write any word lists from your own knowledge, guided by the description). Do not read anything outside this folder.
- Only edit interpretable_transformer.py and write snapshots; do not modify src/.
Procedure: implement your approach, then score it with exactly `<venv python> interpretable_transformer.py --task <task>` (set `model_shorthand_name` and `model_description` in the file before each run; each run appends a row to results/overall_results.csv). Run at most 6 experiments in total. After each run save a copy of the file to interpretable_transformers_lib/<model_shorthand_name>.py with the Write tool. Use the experiments to fix bugs and improve the model. Do not tune it to beat the original: the goal is faithfulness, so do not 'fix' limitations the description says the original has. Finish with a one-line statement of which snapshot is your final model.
Control prompt (no description)
You are hand-writing the weights of a small transformer, with no training, for the task `<task>`.

TASK:
<task docstring>
Prompts are fed token by token (character level); the model generates the answer autoregressively with greedy decoding; score = exact-match accuracy on 200 fixed examples (seed 0). Goal: as high an accuracy as you can get within 6 experiments.

Your working folder has interpretable_transformer.py (a template transformer; edit the architecture and `write_weights(model, task)` so every parameter is set directly in closed form), src/task.py and src/eval.py (read-only task and scoring code), and results/.
Rules:
- NO training of any kind: no gradient steps, optimizers, fitting loops, least squares or searches over weights. Parameters are set in closed form from the algorithm.
- Do not compute the answer with Python outside the transformer; the network must produce it autoregressively. Do not use pretrained weights.
- Do not fit weights, lexicons or thresholds to examples or labels from the dataset (for dataset tasks write any word lists from your own knowledge). Do not read anything outside this folder.
- Only edit interpretable_transformer.py and write snapshots; do not modify src/.
Procedure: implement your approach, then score it with exactly `<venv python> interpretable_transformer.py --task <task>` (set `model_shorthand_name` and `model_description` in the file before each run; each run appends a row to results/overall_results.csv). Run at most 6 experiments in total. After each run save a copy of the file to interpretable_transformers_lib/<model_shorthand_name>.py with the Write tool. Use the experiments to fix bugs and improve the model. Finish with a one-line statement of which snapshot is your final model.
Task statement as inserted (sort-five-digits example)
Example task statement (sort-five-digits), as inserted into the prompts:
Sort a string of 5 digits in ascending order.

prompt: "DDDDD="     (6 tokens)
answer: "SSSSS"      (5 tokens; sorted ascending)

Example: "31415=" -> "11345"

2. Example task inputs

Five visible examples per task (seed 0), as the model sees them (prompt, then the target answer). For the dataset tasks the sentences are lowercased and padding underscores are collapsed; labels are shown by name here, but the model sees the digit.

addition-five-digits
'50494+99346='  ->  '149840'
'55125+05306='  ->  '060431'
'33936+67013='  ->  '100949'
'63691+53075='  ->  '116766'
'39755+62468='  ->  '102223'
boolean-circuit-5-bits
'1|1|0|1|1='  ->  '1'
'0^0|1^0^0='  ->  '1'
'0|1^0&0|1='  ->  '1'
'1^1|0&1^1='  ->  '1'
'0|0&1^0|1='  ->  '1'
date-normalization
'7/2/2094_______________________='  ->  '2094-07-02'
'aug. 13, 2030__________________='  ->  '2030-08-13'
'jun. 19, 2022__________________='  ->  '2022-06-19'
'the 10th of march, 2029________='  ->  '2029-03-10'
'20 february 2093_______________='  ->  '2093-02-20'
decimal-to-binary-8bit
'197='  ->  '11000101'
'215='  ->  '11010111'
'020='  ->  '00010100'
'132='  ->  '10000100'
'248='  ->  '11111000'
digit-counting-10
'66048764759='  ->  '00'
'38242194892='  ->  '02'
'41157815659='  ->  '00'
'38778408016='  ->  '00'
'09753513933='  ->  '03'
edit-distance-8
'ddacddcd,dbdbcdaa='  ->  '5'
'acdcbddc,acdcbddc='  ->  '0'
'aadadcbc,aadbadcb='  ->  '2'
'bdaacdac,abbacbda='  ->  '5'
'dcbbdcdd,cdccbdbd='  ->  '4'
gcd-three-digits
'865,395='  ->  '005'
'777,912='  ->  '003'
'431,042='  ->  '001'
'266,989='  ->  '001'
'524,498='  ->  '002'
linear-interpolation-two-points
'0,06;4,22;8='  ->  '38'
'4,22;7,34;5='  ->  '26'
'8,43;2,13;4='  ->  '23'
'9,19;4,09;8='  ->  '17'
'4,22;1,07;1='  ->  '07'
multiplication-five-digits
'50494*99346='  ->  '5016376924'
'55125*05306='  ->  '0292493250'
'33936*67013='  ->  '2274153168'
'63691*53075='  ->  '3380399825'
'39755*62468='  ->  '2483415340'
nli-snli
'in a bowling alley a man holding a green bowling ball looks |a man is bowling ='  ->  'entailment'
'three girls on an amusement ride enjoying themselves |the girls are at home ='  ->  'contradiction'
'a man and two women sitting on a bench |a few people share a bench ='  ->  'entailment'
'three young children consisting of two girls and a boy who i|a woman drinks wine ='  ->  'contradiction'
'two people wearing blue clothing are making hand gestures ne|two people are next to each other ='  ->  'entailment'
paraphrase-mrpc
'i do not wish to be present during this witness he told stan|yes i do not wish to be present during this witness peterson='  ->  'not paraphrase'
'prince saud said the kingdom of saudi arabia has been wrongf|the kingdom of saudi arabia has been wrongfully and morbidly='  ->  'paraphrase'
"but while microsoft 's share of the low end server software |microsoft 's share of the low end server software market has="  ->  'paraphrase'
'the benchmark year note us yt rr slipped in price sending it|the yield on the year treasury note rose to from late thursd='  ->  'not paraphrase'
'the impact of increased prices has only begun to be seen in |the impact of increased prices has only begun to be seen in ='  ->  'paraphrase'
parity-upto10-bits
'___1011111='  ->  '0'
'____001001='  ->  '0'
'_010011011='  ->  '1'
'____011100='  ->  '1'
'________10='  ->  '1'
sentiment-sst2
'with fewer deliberate laughs more inadvertent ones and stunningly trite songs ='  ->  'negative'
'k the widowmaker is a great yarn ='  ->  'positive'
"it 's got all the familiar bruckheimer elements and schumacher does probably as ="  ->  'positive'
'beautifully filmed ='  ->  'positive'
'this cinema verite speculation on the assassination of john f kennedy may have b='  ->  'positive'
smallest-prime-factor
'64507='  ->  '00251'
'86171='  ->  '86171'
'90347='  ->  '00167'
'98333='  ->  '00107'
'20261='  ->  '20261'
sort-five-digits
'66048='  ->  '04668'
'76475='  ->  '45677'
'93824='  ->  '23489'
'21948='  ->  '12489'
'92411='  ->  '11249'
trec-coarse
'what year did apartheid start ='  ->  'NUM'
'what language is mostly spoken in brazil ='  ->  'ENTY'
'what african country is governed from ouagadougou ='  ->  'LOC'
'what sport features snatches and clean jerks ='  ->  'ENTY'
'who created the character of scrooge ='  ->  'HUM'
word-reversal-3x3
'myn_biq_pmz='  ->  'pmz_biq_myn'
'jpl_sgq_eje='  ->  'eje_sgq_jpl'
'ydt_zir_wzt='  ->  'wzt_zir_ydt'
'ejd_xcv_kpr='  ->  'kpr_xcv_ejd'
'dln_ktu_grp='  ->  'grp_ktu_dln'

3. Per task: description, reconstructions and control

For every task: the selected original, the exact description the reconstructors received, and for each of two reconstructions and the control the attempts they logged, their final message and usage.

addition-five-digits

Original: run addition-five-digits-opus55-s2, program RingLinearTokens (experiments/nl-bottleneck/originals/addition-five-digits.py), visible 100.0%, hidden 100.00%, 27 params.

The original agent's own description of this program: Back in float64, with token embeddings replaced by digit * one scalar and the head by a scalar turn and phase, leaving the 12-row position table as the only table.

Description given to the reconstructors (198 words):

This model adds in one cross-attention read, treating the sum as an angle on a ten-sector ring. The only table is a 2-D position embedding. Digit values are the digit times one scalar, and the readout is a cosine.

Attention: every query reads only the 11 prompt positions, never its own outputs. Digits a_c and b_c share the key for place value c: (1, −c·ln10). Output step i (0–5) has query (ln t_i, 1), so the score is log(t_i·10^−c) and softmax weights are proportional to place value. The '+' key is at the origin, scores 0 and has value 0, acting as a sink. t_i is set so the output equals 1e-5·(A+B)/10^(5−i), shifting the decimal point so the target digit becomes the units digit.

The value scale of 2π/10/1e-5 makes z = 2π·x/10, so higher digits complete whole turns and vanish, and carries come for free.

Readout: digit d's logit is cos(z − 2π/10·(d + 0.5 − 5e-6)), so each digit owns a sector and the tiny shift keeps exact integers off boundaries. Non-digit logits are masked.

Errors: the model needs float64, where it is essentially exact. In float32, rounding near sector boundaries drops accuracy to about 0.5%.
Reconstruction r1: visible 100.00 / hidden 100.00 / extra 100.00; 27 params; 14 turns, $0.51

Final program: experiments/nl-bottleneck/recon_final/addition-five-digits-r1.py (snapshot RingAngleCrossAttn27). Tokens: 13,572 output (2,563 thinking), 24,545 cache write, 228,613 cache read, 24 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
RingAngleCrossAttn27 (final)100.027yesSingle cross-attention read over prompt positions; 2-D pos-embedding keys (1,-c ln10), queries (ln t_i,1), '+' sink; value scale 2pi/10/1e-5 turns the shifted sum into an angle; cosine sector readout; float64, 27 params.

Agent's final message:

The rebuilt model scores 100.0% (200/200) on the first scored run. That matches the original, and it has the same 27 parameters. I used 1 of the 6 allowed runs.

It follows the description directly, with every weight set by a formula:
- **Parameters:** a 13×2 position table plus one value scale (26 + 1 = 27). The description doesn't say how the original gets its table down to that size. I have digits of the same place value share a row, one row for the `+` sink, six rows for the output steps, and one row for the last position, which is never used.
- **Attention:** every query reads only the 11 prompt positions. Place value c has key (1, −c·ln10), with c = 0 for the leading digit. Output step i has query (ln tᵢ, 1). The `+` key is at the origin with value 0, so it acts as the sink. tᵢ = r/(1e-4 − 2.2222·r), where r = 10^(i−10). This puts the attention output at exactly 1e-5·(A+B)/10^(5−i), as described.
- **Value and readout:** digit value = digit × 2π/10/1e-5. The digit logits are cos(z − 2π/10·(d + 0.5 − 5e-6)), and the non-digit tokens are masked. Everything runs in float64.

I haven't checked the description's float32 claim (accuracy dropping to about 0.5%). The script to test it was blocked pending your approval, so I skipped it. The model fits the description as written, and I didn't make it more robust than the original.

Final model: `interpretable_transformers_lib/RingAngleCrossAttn27.py`.
Reconstruction r2: visible 100.00 / hidden 100.00 / extra 100.00; 25 params; 11 turns, $0.45

Final program: experiments/nl-bottleneck/recon_final/addition-five-digits-r2.py (snapshot RingAdder_xattn_cos_f64). Tokens: 12,414 output (2,509 thinking), 20,961 cache write, 172,494 cache read, 20 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
RingAdder_xattn_cos_f64 (final)100.025yesOne cross-attention read over the 11 prompt positions with 2-D place-value keys (1,-c ln10) and queries (ln t_i,1), '+' as zero sink; digit*2pi/10/1e-5 values give an angle; cosine readout over 10 sectors with 5e-6 shift; float64.

Agent's final message:

I rebuilt the model from the description. It got 200/200 (100.0%) on the first run, the same as the original, so I stopped after one of the six allowed experiments.

How it works, all set directly in closed form:
- **Attention:** each output step makes one attention read over the 11 prompt positions only. It never reads the `=` sign or its own earlier outputs.
- **Keys:** the two digits in the same place share a key, (1, −c·ln10), where c=0 is the leftmost (most significant) digit. The `+` key is at the origin with value 0, so it acts as a sink.
- **Queries:** output step i has query (ln tᵢ, 1). I set tᵢ = r/(1 − 2·1.1111·r), with r = 10^(i−6). This cancels the softmax denominator exactly, so the read gives 1e-5·(A+B)/10^(5−i).
- **Values:** each digit is multiplied by 2π/10/1e-5, which turns the read into an angle z = 2π·x/10. Higher digits make whole turns and drop out, so carries come for free.
- **Readout:** digit d's logit is cos(z − 2π/10·(d + 0.5 − 5e-6)). The logits for `+` and `=` are masked out. Everything runs in float64.

Two differences from the original:
- **Parameter count:** mine has 25 parameters (a 12×2 position table plus one value scale), against the original's 27. The description doesn't say what the other two were, so I didn't guess.
- **Float32 behaviour:** I didn't run the model in float32. The description says the original falls to about 0.5% there because of rounding near sector boundaries. My model follows the same arithmetic, so it should fail the same way, but I haven't checked.

Final model: `interpretable_transformers_lib/RingAdder_xattn_cos_f64.py`
Control (no description) r1: visible 100.00 / hidden 100.00 / extra 100.00; 19,744 params; 15 turns, $0.65

Final program: experiments/nl-bottleneck/recon_control_final/addition-five-digits-r1.py (snapshot CarryLookaheadExact). Tokens: 16,893 output (5,647 thinking), 34,213 cache write, 206,301 cache read, 20 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
CarryLookaheadExact (final)100.019,700yes2-layer no-LN transformer: heads gather a_i+b_i per place, MLP computes carries via exact integer threshold sum t_i 10^i >= 10^j, layer-2 MLP selects place by position and applies mod 10, quadratic readout.

Agent's final message:

The hand-written model got all 200 test examples right (accuracy 1.0000) on the first run, so I used only 1 of the 6 experiments. Every weight is set in closed form, with no training or fitting.

**How it works:** it's a 2-layer causal transformer with 5 heads, a model width of 40 and an MLP width of 32. I removed LayerNorm so the hidden values stay exact whole numbers.
1. **Layer-1 attention:** each of the 5 heads looks only at the two digits in one place value, aᵢ and bᵢ, and averages them. The output is scaled by 2, so it stores tᵢ = aᵢ + bᵢ.
2. **Layer-1 MLP:** it works out every carry at once. The carry into place j is 1 exactly when the sum of tᵢ·10ⁱ over the lower places i < j is at least 10ʲ. Two ReLUs per carry implement this test, and it is exact because the values are whole numbers below about 2×10⁵, which float32 holds exactly.
3. **Layer-2 MLP:** it picks the place value for the current output position using the position one-hot. It then computes u = tⱼ + cⱼ and takes u mod 10 as u − 10·[u ≥ 10], using 3 ReLUs per place.
4. **Readout:** the score for each digit d is 2d·s − d², which ranks digits the same way as −(s − d)². So greedy decoding emits exactly the digit s computed in step 3. The `+` and `=` tokens get a score of −10⁴ so they are never chosen.

Because all the arithmetic is exact, the model should be correct on every pair of 5-digit inputs, not just the 200 scored examples. I only measured the 200.

Final model: `interpretable_transformers_lib/CarryLookaheadExact.py` (identical to `interpretable_transformer.py`), 200/200 correct.

boolean-circuit-5-bits

Original: run boolean-circuit-5-bits-opus55-s2, program MinCA_noWk_beta200 (experiments/nl-bottleneck/originals/boolean-circuit-5-bits.py), visible 100.0%, hidden 100.00%, 51 params.

The original agent's own description of this program: Scalar-state CA without W_k: keys are raw positions and W_q is a scaled rotation, sharpness beta=200.

Description given to the reconstructors (186 words):

A tiny recurrent "cellular automaton" transformer. Each position holds one scalar state, the expression value so far, stored centred as S − 0.5. One tied block (one attention head, 2-neuron ReLU MLP) is applied 10 times; each pass moves information one cell right, enough to reach '='.

Attention has no key matrix: positions are sinusoidal angles at 30° per step, and the query is the position rotated back one step, scaled by 200, giving near-hard attention to the previous cell (position 0 attends to itself). Values copy that cell's AND/XOR flags and put its state on the bit channel; each cell adds its own embedding. Op tokens carry their flag plus identity bit (& 1, | and ^ 0), so op cells and '=' (zero embedding) pass the state through.

The MLP does one fold step: new state = relu(b + S − A) − relu(2(b + S) + X − A − 2), giving OR with no flags, AND with A, XOR with X. State starts at 0; readout logits are ±4 times the centred state.

Errors: expected none (attention leakage ≈ e^−27), though untested.
Reconstruction r1: visible 100.00 / hidden 100.00 / extra 100.00; 49 params; 11 turns, $0.56

Final program: experiments/nl-bottleneck/recon_final/boolean-circuit-5-bits-r1.py (snapshot CA_recurrent_fold_v1). Tokens: 16,065 output (4,726 thinking), 26,293 cache write, 165,107 cache read, 18 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
CA_recurrent_fold_v1 (final)100.049yesRecurrent cellular-automaton transformer: scalar centred state per cell, one tied block (keyless 30deg sinusoidal prev-cell attention x200, 2-ReLU fold MLP) applied 10 times; op cells carry flags+identity bit; readout +-4*state.

Agent's final message:

My rebuilt model scored 100% (200/200) on the first run, matching the original, and I didn't need further experiments (1 of 6 used). All weights are set directly from the algorithm; nothing was trained.

How it follows the description:
- **State:** each position holds one scalar, the expression value so far minus 0.5. It starts at 0 (−0.5 centred).
- **Loop:** one shared block with a single attention head and a 2-neuron ReLU MLP runs 10 times. Each pass moves information one cell to the right, so the value reaches '=' by the last pass.
- **Attention:** there is no key matrix. Positions are fixed sinusoids at 30° per step, and the query is the position rotated back one step and scaled by 200. Each cell therefore attends almost entirely to the cell before it, with leakage around e^−27; position 0 attends to itself.
- **Values:** attention copies the previous cell's AND/XOR flags and puts its state on the bit channel. Each cell then adds its own embedding. '&' carries its AND flag plus an identity bit of 1, '|' carries nothing, '^' carries its XOR flag, and '0' and '=' have zero embeddings. Operator cells and '=' therefore just pass the state through.
- **Readout:** the logits for '1' and '0' are +4 and −4 times the centred state.

Two places where I had to depart from the description:
1. **MLP constants.** Taken literally, the description's formula gives the wrong answer for OR when both inputs are 1. I kept its shape and fixed the constants: the new state is 2·(relu(b+s−A) − relu(2(b+s) + X/2 − A − 2)) − 0.5, where s is the centred state. I checked this against every operator and input combination by hand. It gives OR with no flags, AND with A and XOR with X.
2. **Parameter count.** My model has 49 parameters, not 51. The other two probably sit in some bookkeeping detail I can't recover from the description, and I didn't pad to reach 51.

Like the original, it should make no errors on this test set.

Final model: `interpretable_transformers_lib/CA_recurrent_fold_v1.py`.
Reconstruction r2: visible 100.00 / hidden 100.00 / extra 100.00; 57 params; 19 turns, $0.81

Final program: experiments/nl-bottleneck/recon_final/boolean-circuit-5-bits-r2.py (snapshot RecurrentCA_v2). Tokens: 22,663 output (6,883 thinking), 34,214 cache write, 395,963 cache read, 32 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
RecurrentCA_v148.557yesRecurrent cellular-automaton transformer: 1 tied block (keyless sinusoidal prev-cell attention, 2-ReLU fold MLP) applied 10x; centred scalar state; readout +/-4*state.
RecurrentCA_v2 (final)100.057yesRecurrent cellular-automaton transformer: 1 tied block (keyless sinusoidal prev-cell attention, 2-ReLU fold MLP) applied 10x; centred scalar state; readout +/-4*state.

Agent's final message:

The rebuilt model scores 100% (200/200) on the 200 fixed examples, the same as the original. It took 2 of the 6 allowed runs.

**How it follows the description:**
- **State:** each position holds one scalar, the expression value so far, stored as S − 0.5. It starts at S = 0.
- **Recurrence:** one tied block (one attention head and a 2-neuron ReLU MLP) is applied 10 times, which is just enough for the answer to reach '='.
- **Attention:** there is no key matrix. Positions are sinusoidal angles at 30° per step, and the query is the position rotated back one step and scaled by 200. Each cell attends almost entirely to the previous cell, and position 0 attends to itself. Leakage to other cells is about e^−27.
- **Values:** the attended cell's AND/XOR flags are copied, and its state goes onto the bit channel. Each cell then adds its own embedding, with `&` carrying identity bit 1 and `|`/`^` carrying 0, so op cells and '=' pass the state through.
- **MLP:** with u = bit + S (both 0/1), new S = relu(u − A) − relu(2u + X − A − 3). This gives OR with no flags, AND with A and XOR with X.
- **Readout:** the logits for '1' and '0' are ±4 times the centred state.

**Where it departs from the description:**
- **MLP formula:** the formula as written doesn't give correct AND/OR/XOR, whether S is read centred or uncentred. That made run 1 score 48.5%. My version matches its structure, and its second term equals the written one when S is centred; the first term has an extra +0.5 offset.
- **Parameter count:** it has 57 parameters versus the original's 51, because I used full 3×3 value and output matrices rather than more compact ones.

**Snapshots:** `RecurrentCA_v1.py` is a trimmed copy of run 1 (same model and weights, with comments and some harness text removed), not a byte-for-byte copy. `RecurrentCA_v2.py` is an exact copy of the final file.

Final model: `interpretable_transformers_lib/RecurrentCA_v2.py`.
Control (no description) r1: visible 100.00 / hidden 100.00 / extra 100.00; 11,376 params; 15 turns, $0.65

Final program: experiments/nl-bottleneck/recon_control_final/boolean-circuit-5-bits-r1.py (snapshot GatherThenChainMLP). Tokens: 18,569 output (1,773 thinking), 28,930 cache write, 259,184 cache read, 26 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
GatherThenChainMLP (final)100.011,400yesNo-LN; 9-head positional gather attention copies bits/ops to the '=' slot, then 4 ReLU MLPs each compute r_i=op_i(r_{i-1},b_i) via 12 combo-detector units.

Agent's final message:

The model scored 100% (200/200) on the first run, so I used 1 of the 6 experiments and stopped there.

**How it works.** Every weight is set by hand and nothing is trained. I removed LayerNorm so the values inside the network stay exactly 0 or 1.
- **Embeddings:** each token is a one-hot vector, plus a constant-1 dimension and a one-hot position.
- **Attention (one layer, 9 heads):** head *j* looks only at position *j* (score 60 there, 0 elsewhere). At the `=` token, this copies each of the 5 bits and the 4 operators into its own slot.
- **Four ReLU MLPs, applied in sequence:** MLP *i* computes rᵢ = opᵢ(rᵢ₋₁, bᵢ), starting from r₀ = b₀. It has 12 hidden units, one for each (operator, previous result, bit) combination. A unit fires with value 1 only on an exact match, and it writes 1 into rᵢ when the truth table gives 1. Doing the steps in order gives the strict left-to-right evaluation.
- **Output:** the score for `1` is 10·r₄ and the score for `0` is 10·(1−r₄). The other tokens score 0, so they never win.

The model has about 2.3k parameters. The result is logged in `results/overall_results.csv`.

**Leftover file:** a scratch file, `results/_new_section.py`, is still there because the permission prompt blocked deleting it. You can delete it; it isn't used.

Final model: snapshot `interpretable_transformers_lib/GatherThenChainMLP.py` (100% accuracy).

date-normalization

Original: run date-normalization-opus55-s1, program ptr_v35_noZ3 (experiments/nl-bottleneck/originals/date-normalization.py), visible 100.0%, hidden 100.00%, 345 params.

The original agent's own description of this program: First dot of yyyy.mm.dd found by position (flag O overlaid on i=4) instead of a digit 3 back, dropping the Z3 dim. 25 stream dims used.

Description given to the reconstructors (196 words):

A 2-layer transformer with no LayerNorm, 3 heads per layer, character tokens and positions on a period-48 circle. Layer 1 has attention plus an MLP; layer 2 has attention only.

Layer-1 heads match positions rotary-style to copy features from 1, 2 and 3 tokens back: digit value, is-digit flag and letter codes on a circle. At position 1, the "2 back" head reads itself.

MLP ReLU AND-gates tag the token ending each digit run. Default is DAY; a dot at position 4 is YEAR; a slash at position ≤2 or a dot after a digit is MONTH; padding is half YEAR, half DAY. One unit per month matches letters 2–3, tags MONTH and writes the month's digits. March ("ar") gets half weight so January/February win.

Layer-2 heads attend sharply (about 20·√d) to the token tagged with the current field, reading digits 2 back (tens) and 1 back (ones), or else "=" (zeros). The first year digit is 2 − (digit 3 back)/9; the second is ReLU(18 − 9·first). The readout picks the digit nearest a scalar. Hyphens come from position.

Errors: tied tags (two dots) get averaged; unseen separators or field orders fall back to DAY.
Reconstruction r1: visible 100.00 / hidden 100.00 / extra 100.00; 7,708 params; 11 turns, $1.44

Final program: experiments/nl-bottleneck/recon_final/date-normalization-r1.py (snapshot DateNorm_TagRead_v1). Tokens: 43,882 output (28,336 thinking), 61,617 cache write, 355,554 cache read, 22 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
DateNorm_TagRead_v1 (final)100.07,710yes2-layer no-LN transformer: rotary k-back copy heads, MLP AND-gate field tags (DAY default, dot@4 YEAR, slash<=2/dot MONTH, pad half YEAR/DAY, month letter-pair units, march half), layer-2 sharp tag readers for hundreds/tens/ones, nearest-digit readout.

Agent's final message:

My hand-written transformer scored **100% (200/200)** on the fixed eval set on the first run, the same as the original. I used 1 of the 6 allowed experiments and stopped there, since the goal is faithfulness, not beating the original.

**How it follows the description** (no LayerNorm; 2 layers with 3 heads each; layer 1 has attention plus an MLP, layer 2 attention only):
- **Layer-1 heads:** rotary-style position matching on a period-48 circle. They copy the token 1, 2 and 3 back: digit value, is-digit flag and letter codes on a circle. At position 1 the 2-back head reads itself.
- **Layer-1 MLP:** ReLU AND-gates tag the non-digit token right after each digit run:
  - The default tag is DAY.
  - A dot at position 4 is YEAR.
  - A slash at position ≤2, or a dot after a digit, is MONTH.
  - Padding is half YEAR, half DAY.
- **Month names:** 12 units match letters 2–3 of the name, tag MONTH and write the month's two digits. March ("ar") gets half weight, so January and February win.
- **Layer-2 heads:** one reads the hundreds digit, one the tens, one the ones. Each attends sharply (logit 20·tag) to the token tagged with the current field, or falls back to "=" (zeros) when no tag is present.
- **Year digits:** the first is 2 − (digit 3 back)/9, and the second is ReLU(18 − 9·first).
- **Readout:** picks the digit nearest a scalar. Hyphens come from position.

**Where it departs from the description:**
- **Size:** about 4,000 parameters against the original's 345, because I used ordinary dense matrices that are mostly zeros.
- **Extra position marker:** the 2-back head can't read itself at position 1 using the circle alone, so I added a small marker on positions 0 and 1.
- **Generated hyphens:** I subtract the DAY tag at positions 36 and 39 so the hyphens the model writes itself aren't treated as day separators. The description doesn't say how the original handled this.

**Not checked:** the original's described mistakes (two dots averaging their tags, unseen separators or field orders falling back to DAY) never occur in the 200 examples. A quick test on such inputs needed your approval, so I skipped it. These mistakes are built into the same rules, but I haven't confirmed them by running anything.

Final model: `interpretable_transformers_lib/DateNorm_TagRead_v1.py`
Reconstruction r2: visible 100.00 / hidden 100.00 / extra 100.00; 9,174 params; 26 turns, $2.11

Final program: experiments/nl-bottleneck/recon_final/date-normalization-r2.py (snapshot rotcopy_tags_v2_sharp40). Tokens: 63,765 output (44,930 thinking), 79,630 cache write, 995,614 cache read, 38 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
rotcopy_tags_v199.09,170yes2-layer no-LN transformer: L1 rotary-style copy heads (1/2/3 back, period-48 circle, step 17) + ReLU AND-gate tags (DAY default, dot@4 YEAR, slash<=2/dot MONTH, pad half Y/D, month letter units); L2 tens/ones/high heads attend sharply to field tag or '='; nearest-digit readout.
rotcopy_tags_v2_sharp40 (final)100.09,170yes2-layer no-LN transformer: L1 rotary-style copy heads (1/2/3 back, period-48 circle, step 17) + ReLU AND-gate tags (DAY default, dot@4 YEAR, slash<=2/dot MONTH, pad half Y/D, month letter units); L2 tens/ones/high heads attend sharply to field tag or '='; nearest-digit readout.

Agent's final message:

I rebuilt the model as hand-set weights with no training. It scores **100% (200/200)** on the seed-0 set, the same as the original. I used 2 of the 6 allowed runs.

**What's built, following the description:**
- **Layer 1, three heads:** each copies digit value, is-digit flag and letter code from 1, 2 and 3 tokens back, by matching positions on a period-48 circle. At position 1 the "2 back" head reads itself, which keeps the tens digit of a one-digit month at 0 (e.g. "3/3/2021" gives month 03).
- **Layer 1, ReLU MLP:**
  - **Field tags:** gates tag the token that ends each digit run. Default is DAY; a dot at position 4 is YEAR; a slash at position ≤2 or a dot after a digit is MONTH; padding is half YEAR, half DAY.
  - **Month names:** 12 units match letters 2–3 of a month name, tag MONTH and write the month's digits. March ("ar") has half weight so January and February win.
  - **Second year digit:** one unit computes ReLU(18 − 9·first).
- **Layer 2, three heads, no MLP:** a tens head, a ones head and a head for the first year digit (2 − (digit 3 back)/9). Each attends sharply to the token tagged with the current field, or to "=" (zeros) when it's not its turn.
- **Readout:** picks the digit nearest a scalar; hyphens come from position.

**Things I added or chose that the description doesn't specify:**
- **Position circle:** each position advances 17/48 of a turn instead of 1/48. With plain rotary matching on a 1/48 circle, the "2 back" head at position 1 always reads position 0, never itself. Because of the 17/48 spacing, "slash at position ≤2" uses two exact-position units (positions 1 and 2).
- **Choosing the field:** the layer-2 heads need to know which field and digit to output at each answer position. I added 9 MLP units that each fire at a single answer position to supply this, and two of them place the hyphens.
- **Answer hyphens:** the "-" characters the model writes after digits would otherwise be tagged DAY by default. Two units cancel that tag.
- **Sharpness:** the first run used an effective attention sharpness of 20 and scored 99%. Inactive heads leaked attention onto digit tokens, so two 19xx years came out as 21xx. I raised it to 40, which still fits "about 20·√d".
- **Parameter count:** about 9,170 parameters versus the original's 345, because I used dense named-channel matrices. The algorithm is the same; the size is not.

I didn't test the failure modes the description lists (two dots get averaged, unseen separators fall back to DAY). They aren't in the 200 examples, and running a custom test needs your approval. The mechanism that causes them is in place.

Run 1's snapshot (`rotcopy_tags_v1.py`) isn't a full copy: I saved it late, so it loads v2 and halves the layer-2 sharpness to recreate run 1.

Final model: `interpretable_transformers_lib/rotcopy_tags_v2_sharp40.py`.
Control (no description) r1: visible 100.00 / hidden 100.00 / extra 100.00; 959,616 params; 25 turns, $1.44

Final program: experiments/nl-bottleneck/recon_control_final/date-normalization-r1.py (snapshot date_parser_2L_markers_v2). Tokens: 42,204 output (18,434 thinking), 61,984 cache write, 497,798 cache read, 32 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
date_parser_2L_markers97.5960,000yes2-layer no-LN hand-wired parser: L1 copies prev 4 tokens and detects year/month/day end markers + month trigrams; L2 attends to markers and reads out digits per output position.
date_parser_2L_markers_v2 (final)100.0960,000yes2-layer no-LN hand-wired parser: L1 copies prev 4 tokens and detects year/month/day end markers + month trigrams; L2 attends to markers and reads out digits per output position.

Agent's final message:

The model now gets **200/200 (100%)** on the seed-0 set, after 2 of the 6 allowed runs. Every weight is set by hand in closed form: no training, no fitting, no search.

**How it works:** two layers, with LayerNorm removed so the arithmetic on one-hot vectors stays exact.
- **Layer 1, attention:** four heads copy the tokens 1, 2, 3 and 4 positions back into each position. If that position doesn't exist, the head looks at the current token instead.
- **Layer 1, MLP:** ReLU units fire on simple patterns and mark where each field ends:
  - **Year:** the end of a 4-digit run, or failing that, the first `_` after a digit.
  - **Numeric month:** a `/` at position 2 or earlier, or a `.` at position 7.
  - **Day:** the end of a 1–2 digit run that isn't the month or the two-digit year in `mm/dd/yy`.
  - **Month names:** the first three letters of each of the 12 names, from my own month list.
- **Layer 2, attention:** three heads jump to the year, month and day markers and bring back the digits just before each one, plus the month-name signal.
- **Layer 2, MLP:** for each answer position, it turns the gathered digits into an output score. It fills in the defaults: "20" for two-digit years, a leading 0 for one-digit months and days, and the two-digit number for each month name.

**Runs:**
1. `date_parser_2L_markers`: 97.5%. The 5 failures were all `m/d/yyyy` dates with a one-digit month and day. My day-run test wrongly treated "7/2/" as a long run of digits.
2. `date_parser_2L_markers_v2`: 100%. I fixed that test so a run counts as short unless three digits precede the separator.

**Caveats:**
- The v2 snapshot is an exact copy of the file that scored 100%. The run-1 snapshot is not: the first save failed, so I rebuilt it from the v2 file with the old day-run test and name put back and comments removed. It should score 97.5% again, but I haven't rerun it.
- In v2, the docstring line describing the day-run test is out of date; the code is correct. I left it alone so the snapshot matches the scored file.

Final model: `interpretable_transformers_lib/date_parser_2L_markers_v2.py` (200/200 = 100%).

decimal-to-binary-8bit

Original: run decimal-to-binary-8bit-opus55-s2, program e22_minimal_dof (experiments/nl-bottleneck/originals/decimal-to-binary-8bit.py), visible 100.0%, hidden 100.00%, 89 params.

The original agent's own description of this program: Degrees-of-freedom probe: W_v and W_o replaced by hard-wired read of VAL and two output scales, head reads only MODE. Bespoke rather than a standard transformer.

Description given to the reconstructors (190 words):

The model does exact greedy binary expansion, MSB first: at each output step it computes r = n − (value of bits already emitted) and outputs 1 if r ≥ 2^k, where k is the current bit. It uses one layer, two attention heads and no MLP or LayerNorm. Attention values are a scalar token value: digit face value, 0/1 for emitted bits, 0.5 for "=".

Each position carries a log-place-value feature (ln100, ln10, 0 for digits; (11−p)·ln2 for "=" and fed-back bits), so softmax turns scores into place-value weights.

Head 0 attends from output positions to the digits, weighted 100:10:1, returning n/111. Head 1 attends to "=" and emitted bits, each bit weighted 2^j and "=" weighted 2^(k+1), so weights total 256 and it returns (S + 2^k)/256, S being the emitted bits' value.

Scaling the heads by +222 and −512 and adding +1 gives 1 + 2(r − 2^k), an odd integer, never zero. That is the "1" logit; "0" gets its negative.

A ±6 positional bias separates the heads' regions; the e^−12 leakage shifts logits about 0.01 against a margin near 1. All 256 inputs are correct.
Reconstruction r1: visible 100.00 / hidden 100.00 / extra 100.00; 89 params; 13 turns, $0.63

Final program: experiments/nl-bottleneck/recon_final/decimal-to-binary-8bit-r1.py (snapshot greedy_binexp_2head_89p). Tokens: 17,588 output (6,379 thinking), 28,792 cache write, 228,053 cache read, 22 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
greedy_binexp_2head_89p (final)100.089yes1-layer 2-head attention-only (89 params): head0 log-place weights 100:10:1 -> n/111, head1 bit weights 2^j + '=' 2^(k+1) -> (S+2^k)/256; logit1 = 222h0-512h1+1, logit0 = -logit1; +-6 region bias.

Agent's final message:

I rebuilt the model and it matches the original on the numbers I could measure. It scored **100% (200/200)** on the first run and has exactly **89 parameters**, the same count as the original. I used only that one of the 6 allowed experiments, since nothing needed fixing.

**How it works** (one layer, two attention heads, no MLP or LayerNorm):
- **Parameters:** each token has two values (its face value, with 0.5 for "=", and a flag for "="). Each position has four features. Three numbers combine the heads into the output. That gives 22 + 64 + 3 = 89.
- **Head 0:** looks at the three digits with weights 100:10:1, using the log place-value feature (ln100, ln10, 0), plus a +6 bias toward digits. It returns n/111.
- **Head 1:** looks at "=" and the bits already emitted. Each bit at position p is weighted 2^(11−p), with a +6 bias toward these positions. It returns (S + 2^k)/256.
- **"=" weighting:** "=" sits at position 3 with feature 8·ln2. A query-side shift of (3−q)·ln2 turns its weight into 2^(k+1).
- **Output:** the "1" logit is 222·h0 − 512·h1 + 1 = 1 + 2(r − 2^k), and the "0" logit is its negative. All other tokens get 0.

**One part I had to interpret:** the description says "=" has feature (11−p)·ln2 but also weight 2^(k+1). Those only agree when the extra query-dependent shift above is added, so the 4th position feature I used may not be exactly how the original did it.

**Not checked:** I didn't confirm the description's claim that all 256 inputs are correct or measure the margins, because the command for that check was declined. By the arithmetic, the logit is always an odd integer (at least 1 in size) and the leakage is about 0.01, so every input should be decoded correctly.

Final model: `interpretable_transformers_lib/greedy_binexp_2head_89p.py`
Reconstruction r2: visible 100.00 / hidden 100.00 / extra 100.00; 432 params; 12 turns, $0.60

Final program: experiments/nl-bottleneck/recon_final/decimal-to-binary-8bit-r2.py (snapshot GreedyBinExp_2head_logplace). Tokens: 15,761 output (3,584 thinking), 29,491 cache write, 228,378 cache read, 22 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
GreedyBinExp_2head_logplace (final)100.0432yes1-layer 2-head attn-only: head0 place-value softmax over digits -> n/111, head1 over '=' and emitted bits -> (S+2^k)/256; logit1 = 1+222a0-512a1 = 2(r-2^k)+1; +-6 positional region bias.

Agent's final message:

I built the model from the description and it scored **100% (200/200)** on the first run, the same as the original. I used 1 of the 6 allowed experiments.

What I built, in `interpretable_transformer.py`, with every weight set by hand:
- **Structure:** one layer, two attention heads, no MLP and no LayerNorm. Each token carries a single value: the digit's face value, 0 or 1 for an emitted bit, and 0.5 for "=".
- **Place-value feature:** each position gets ln100, ln10 or 0 for the three digits, and (11−p)·ln2 for "=" and the fed-back bits, so the softmax turns scores directly into place-value weights. One extra query-side term, (3−t)·ln2, lifts the weight on "=" to 2^(k+1) at the step that emits bit k.
- **Head 0** reads the digits with weights 100:10:1 and returns n/111.
- **Head 1** reads "=" and the bits already emitted (bit j weighted 2^j, total weight 256) and returns (S + 2^k)/256, where S is the value of those bits.
- **Output:** the "1" logit is 1 + 222·(head 0) − 512·(head 1), which equals 2(r − 2^k) + 1 with r = n − S. It is always an odd integer, so never zero. The "0" logit is its negative.
- **Region separation:** a ±6 positional bias keeps each head on its own positions. Some attention still leaks onto the other region (about e^−12, as in the description), and I left that as it is.

**Parameter count:** mine has 368 parameters, not the original's 89. Its layer is dense, while the original was probably sparse. This changes the size only, not the algorithm or its behaviour.

**Not checked:** I only scored the 200 examples. I did not run all 256 inputs or measure the logit margins, so the description's "all 256 correct" is not confirmed for my version.

Final model: `interpretable_transformers_lib/GreedyBinExp_2head_logplace.py`.
Control (no description) r1: visible 100.00 / hidden 100.00 / extra 100.00; 150,976 params; 14 turns, $0.63

Final program: experiments/nl-bottleneck/recon_control_final/decimal-to-binary-8bit-r1.py (snapshot DigitGatherStepBits). Tokens: 13,004 output (2,158 thinking), 39,790 cache write, 277,871 cache read, 20 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
DigitGatherStepBits (final)100.0151,000yes1-layer no-LN transformer: 3 attention heads gather digits into N=100a+10b+c; position-gated ReLU step pairs compute bit_(10-pos)(N) as alternating sum of thresholds.

Agent's final message:

The model got 200/200 (accuracy 1.0000) on its first scored run. That used one of the six experiments, and I didn't run any more because there was nothing left to fix.

Every weight is set directly from the algorithm, and the network produces the answer itself, one token at a time:

- **Architecture:** a single transformer layer with LayerNorm removed, so the numbers stored in the residual stream stay exact integers. Other settings: d_model 64, 4 heads, feed-forward width 1024.
- **Attention:** three heads each lock onto one of the three prompt digits (positions 0, 1 and 2). Their outputs are scaled by 100, 10 and 1 and added together, which gives the decimal number N.
- **Feed-forward layer:** at sequence position p (3 to 10), the next token is bit 10−p of N. That bit is an alternating sum of step functions with thresholds at multiples of 2^b. Each step is made exactly from two ReLUs, using the fact that N is an integer. A large negative bias switches a neuron off except at its own position. This uses 1004 neurons.
- **Output:** the bit is read out as a logit of ±20 for the tokens '0' and '1'.

To save the snapshot I wrote the file out with the Write tool rather than `cp`. One line came out wrong in the copy and I corrected it, so the logic in the snapshot is the same as the file that was scored. I didn't re-run the snapshot to confirm. In both files, the docstring of `SimpleTransformer` still calls it "3-layer"; that's out of date but changes nothing.

Final model: `interpretable_transformers_lib/DigitGatherStepBits.py` (200/200).

digit-counting-10

Original: run digit-counting-10-opus55-s2, program MetricAttention (experiments/nl-bottleneck/originals/digit-counting-10.py), visible 100.0%, hidden 100.00%, 75 params.

The original agent's own description of this program: Attention is a bilinear metric per layer, score = x diag(m) x', with the value fixed to the flag channel: 6 attention numbers in total, in score units. d_model=3.

Description given to the reconstructors (200 words):

An attention-only 2-layer transformer: one head per layer, no MLP, no LayerNorm, width 3. Two channels hold the digit on the unit circle (angle 2πd/10). The third is a signed flag: +1 at the query digit (position 10), −1 at '=' and the generated tens digit, 0 on input digits. Attention writes only into the flag channel.

Layer 1 matches same digits. The score is the circle dot product, scaled so a match beats the nearest non-match by about 40 logits. The query attends evenly to its k matches plus itself, so its flag becomes 1 + 1/(k+1). Softmax does the counting. '=' and the tens position sit off the unit circle, so they attend only to themselves and their flag becomes −2.

In layer 2 (score about −60·f_q·f_k), negative-flag positions attend to the query and add its flag, leaving ρ−1, where ρ = 1/(k+1).

The readout is a linear nearest-prototype classifier on ρ: logit_u = 2ρ/(u+1) − 1/(u+1)². The circle channels supply the constants. They also suppress digits above 1 for the tens digit, place the tens threshold between k=9 and 10, and force units "0" after tens "1".

Counts 0–10 are exact. There are no known systematic errors.
Reconstruction r1: visible 100.00 / hidden 100.00 / extra 100.00; 150 params; 15 turns, $0.78

Final program: experiments/nl-bottleneck/recon_final/digit-counting-10-r1.py (snapshot CircleFlagCount_v1). Tokens: 20,244 output (6,927 thinking), 39,284 cache write, 310,240 cache read, 26 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
CircleFlagCount_v1 (final)100.0150yes2-layer attn-only width-3: circle digit matching counts via softmax into flag (1+1/(k+1)); layer2 copies query flag to '='/tens (rho-1); nearest-prototype linear readout, circle channels give constants.

Agent's final message:

I rebuilt the model from the description, and it scored **100% (200/200)** on the first scored run. That matches the original, which has no known errors. I used 1 of the 6 allowed runs.

How it follows the description:
- **Shape:** two attention-only layers with one head each, width 3, no MLP and no LayerNorm. Two channels hold the digit on the unit circle at angle 2πd/10, and the third is the flag. The flag is +1 at the query, −1 at `=` and at the generated tens digit, and 0 elsewhere.
- **Layer 1:** matches equal digits, with the scale set so a match beats the nearest non-match by 40 logits. It writes only into the flag, so the query's flag becomes 1 + 1/(k+1). `=` (radius 2) and the tens position (shifted by (2,0)) sit off the circle, attend only to themselves, and end with flag −2.
- **Layer 2:** the score is −60·f_q·f_k, so `=` and the tens position copy the query's flag and end at ρ−1, where ρ = 1/(k+1).
- **Readout:** a linear nearest-prototype rule, logit_u = 2ρ/(u+1) − 1/(u+1)², with the constants coming from the circle channels. When writing the tens digit, digits 2–9 are pushed down, and the 0/1 cut-off sits between k=9 and k=10. After a tens digit of "1", the units digit is forced to "0". I set these constants by exactly inverting a 2×2 matrix, not by fitting.

Two things you should know:
- **Size:** my model has 150 parameters against the original's 75. The extra come from a full 16-position embedding table and from storing each layer's attention as two full 3×3 matrices. The computation is the same, but the parameter layout is not.
- **Counts 9 and 10 not checked separately:** I tried to test every count from 0 to 10, but that command wasn't approved, so it never ran. Counts of 9 or 10 are rare in the 200 test examples, so the 100% result alone doesn't prove those cases work. They should hold on paper, since ρ is 0.1 at k=9, 0.0909 at k=10, and the cut-off is 0.0955.

Final model: `interpretable_transformers_lib/CircleFlagCount_v1.py`
Reconstruction r2: visible 100.00 / hidden 100.00 / extra 100.00; 186 params; 15 turns, $0.73

Final program: experiments/nl-bottleneck/recon_final/digit-counting-10-r2.py (snapshot CircleFlagSoftmaxCount). Tokens: 19,778 output (6,607 thinking), 35,604 cache write, 248,487 cache read, 22 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
CircleFlagSoftmaxCount (final)100.0186yes2-layer attn-only width-3: circle digit channels + signed flag; L1 softmax counts matches into query flag 1+1/(k+1); L2 copies it to '='/tens giving rho-1; linear nearest-prototype readout on rho with circle-channel constants.

Agent's final message:

The rebuilt model scored **100.0% (200/200)** on the first run, the same as the original. I used one of the six allowed runs.

**What I built** (`interpretable_transformer.py`): a 2-layer transformer with attention only (no MLP, no LayerNorm), width 3 and one head per layer. Every weight is set directly by formula.
- **Channels:** two channels place each digit on the unit circle (angle 2πd/10). The third is a signed flag: +1 at the query, −1 at '=' and at the generated tens digit, 0 on the input digits. '=' and the tens position are pushed off the unit circle (radius above 1).
- **Layer 1:** matches the query to equal digits using the circle dot product, scaled so a match beats the nearest non-match by 40 logits. The value is the flag, so the query's flag becomes 1 + 1/(k+1). '=' and the tens position attend only to themselves, so their flag becomes −2.
- **Layer 2:** the score is −60·f_q·f_k. The '=' and tens positions attend to the query and add its flag, leaving ρ−1 with ρ = 1/(k+1).
- **Readout:** a linear nearest-prototype classifier, logit_u = 2ρ/(u+1) − 1/(u+1)². The flag weight for digit u is 2/(u+1). The constant terms come from the circle channels, through an exact 2×2 solve written out by hand. At '=' those constants suppress digits above 1 and put the tens threshold halfway between ρ = 1/10 and 1/11 (k = 9 and k = 10). After a tens digit of "1" they force the units digit to "0".

So counts 0–10 come out exactly and there's no systematic error, which matches the description.

**Parameter count:** the template uses full 3×3 attention matrices and a 16-row position table that I'm not allowed to change. That gives 186 parameters, most of them zero, against the original's 75. The algorithm is the same; only the parameter count differs.

Final model: `interpretable_transformers_lib/CircleFlagSoftmaxCount.py`.
Control (no description) r1: visible 100.00 / hidden 100.00 / extra 100.00; 102,408 params; 16 turns, $0.64

Final program: experiments/nl-bottleneck/recon_control_final/digit-counting-10-r1.py (snapshot QueryCopyHistRamp). Tokens: 15,399 output (2,830 thinking), 34,756 cache write, 268,969 cache read, 26 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
QueryCopyHistRamp (final)100.0102,000yes2-layer no-LN: L1 attn copies query digit to answer positions; L2 attn averages digit one-hots over positions 0-9; L2 MLP selects query bin and decodes count via relu ramps gated by position.

Agent's final message:

The hand-written model scored **100% (200/200)** on the first run, so I used 1 of the 6 experiments. The snapshot is identical to the scored file.

How it works: I removed the LayerNorms so the hand-set values stay exact. It's a 2-layer model with one attention head per layer, a 64-wide residual stream, and every weight set directly in `write_weights`:

1. **Layer 1 attention:** the two positions that predict the answer (11 and 12) attend sharply to position 10, the query digit, and copy it in as a one-hot vector.
2. **Layer 2 attention:** the same two positions spread attention evenly over the 10 digit positions (0–9). Averaging the digits' one-hot vectors gives a histogram, `count_d / 10` for each digit d.
3. **Layer 2 MLP:** each hidden unit computes `relu(count - k)` for digit d. Large gate terms switch it off unless d is the query digit and the unit belongs to the current output position. Summing neighbouring units as `relu(c-k+1) - 2·relu(c-k) + relu(c-k-1)` gives exactly 1 when the count equals k and 0 otherwise. Position 11 writes the tens digit ('1' only when the count is 10) and position 12 writes `count % 10`, straight into logit slots that the output head reads.

Final model: `interpretable_transformers_lib/QueryCopyHistRamp.py` (100% accuracy).

edit-distance-8

Original: run edit-distance-8-opus55-s1, program ScalarValues_d7 (experiments/nl-bottleneck/originals/edit-distance-8.py), visible 100.0%, hidden 100.00%, 507 params.

The original agent's own description of this program: Each head returns one scalar value, shrinking W_v and W_o from 7x6 to 7x2; shared cell, d=7, ff=3.

Description given to the reconstructors (200 words):

This untrained transformer runs the Levenshtein DP table: one shared block applied 9 times (width 7, no LayerNorm, 2 single-scalar heads, 3-unit ReLU MLP), one DP row per pass.

The comma and eight second-string tokens hold columns 0–8 as W[l] = D[i][l] − l, initialised to 0; first-string tokens start at W = 16 so they never win a minimum. Letters are unit-ring points (a, b, c, d at 90° steps). Each token keeps a countdown Y = position − pass and Y², frozen at the comma.

Head 1 is a soft minimum over strictly earlier tokens (softmax of −4·W, averaging W). Head 2 selects the first-string letter with zero countdown via a −32·Y² peak, scoring +8 on match and −8 on mismatch against 0 for the comma, yielding a soft match bit. The MLP sets W ← min(W+1, softmin − match) with one ReLU and advances the countdown.

At "=" the answer is W + 8, read out with parabolic digit logits 2d·v − d², i.e. round(v).

Errors: the soft minimum averages, biasing W upward about 0.018 per near-tied column one above the minimum; accumulated over passes, this occasionally rounds up by one, so mistakes should be rare overestimates.
Reconstruction r1: visible 100.00 / hidden 100.00 / extra 100.00; 507 params; 18 turns, $2.34

Final program: experiments/nl-bottleneck/recon_final/edit-distance-8-r1.py (snapshot ed8_softmin_dp_v1). Tokens: 73,459 output (59,054 thinking), 90,983 cache write, 728,017 cache read, 30 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
ed8_softmin_dp_v1 (final)100.0507yesShared 7-wide block x9 (no LN, 2 scalar heads, 3 ReLU): W=D-l columns, head1 softmin(-4W) over earlier tokens, head2 countdown -32Y^2 letter match vs comma, MLP W<-min(W+1,S-M); '=' reads W+8 with parabolic digit logits.

Agent's final message:

My rebuild scored **100% (200/200)** on the first run and has **507 parameters**, both matching the original. I used 1 of the 6 allowed runs and stopped there, since further changes would only be tuning.

**How it follows the description:**
- **Structure:** one shared block applied 9 times, width 7, no LayerNorm. It has 2 attention heads, each with a 3-dim query/key and a single scalar value, and a 3-unit ReLU MLP. That layout is what makes the count come out at exactly 507.
- **What each token holds:** the comma and the 8 second-string tokens hold columns 0–8 as W = D[i][l] − l, starting at 0. First-string tokens start at W = 16. Letters are points on a unit ring. Each token also carries a countdown Y and Y².
- **Head 1:** a soft minimum over strictly earlier tokens, softmax(−4·W) averaging W.
- **Head 2:** a −32·Y² peak picks out the current first-string letter. It scores +8 on a match and −8 on a mismatch, against 0 for the comma, which gives a soft match bit.
- **MLP:** `W ← W + 1 − relu(W + 1 − (softmin − match))`, which is min(W+1, softmin − match). It also advances the countdown and clears a scratch slot.
- **Readout:** at "=", the digit logits are 2d·v − d² with v = W + 8, which picks round(v).
- **Errors:** the upward bias from the soft minimum (e^−4 ≈ 0.018 per near-tied column one above the minimum) is left in, as the description says the original has it.

**Things the description doesn't say, and how I resolved them:**
- **Why 9 passes:** taken literally, the update has no within-row horizontal step, so on its own it would get the last DP row wrong. I treat "=" as a 10th column whose update takes a soft minimum over all columns. After the 9th pass it holds min over l of (D[8][l] − l), which supplies those missing last-row moves. That explains the 9 passes and the "+8" in the readout.
- **Fitting the countdown into 3 MLP units:** on first-string tokens the min unit always outputs exactly 2, so I reuse it to drive the countdown, alongside one Y-linear unit and one unit that clears the scratch slot. To keep that working, the second-string tokens and "=" have their countdown parked at −100 rather than literally "position − pass". The comma's countdown is frozen at 0.

This is a reconstruction of behaviour, not a weight-level copy: it can't be compared to the unseen original beyond accuracy and parameter count. Since it scored 100%, these 200 examples show none of the rare over-by-one mistakes the description predicts, so they give no evidence about whether its mistakes match the original's elsewhere.

Final model: `interpretable_transformers_lib/ed8_softmin_dp_v1.py`.
Reconstruction r2: visible 100.00 / hidden 100.00 / extra 100.00; 471 params; 12 turns, $1.41

Final program: experiments/nl-bottleneck/recon_final/edit-distance-8-r2.py (snapshot EditDistDP_softmin_v1). Tokens: 45,245 output (29,477 thinking), 53,395 cache write, 385,958 cache read, 24 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
EditDistDP_softmin_v1 (final)100.0471yesWidth-7 shared block x9, one Levenshtein DP row per pass: head1 softmin(-4W) over strictly earlier tokens, head2 countdown-selected match bit (+-8 vs comma), MLP W<-min(W+1,S-m), parabolic readout of W+8 at '='.

Agent's final message:

The rebuilt model scored **100.0% (200/200)** on the first scored run, which matches the original. I used 1 of the 6 allowed runs and made no further changes. It has 471 parameters against the original's 507, because a few of my architecture details differ from the original's (listed below).

**How it works.** Every parameter is written directly in `write_weights`, with no training. One shared block (width 7, no LayerNorm) runs 9 times:
- **Starting values:** the comma and the eight second-string tokens hold the DP columns as W = D − l, starting at 0. First-string tokens start at W = 16 so they never win a minimum. Letters are points on a unit ring, 90° apart.
- **Head 1** takes a soft minimum over strictly earlier tokens (scores −4·W, averaging W).
- **Head 2** finds the first-string letter whose countdown Y is 0, using a −32·Y² term. It scores +8 on a match and −8 on a mismatch, against 0 for the comma, which gives a soft match bit.
- **MLP (3 ReLUs):** one unit does W ← min(W+1, softmin − match). The other two advance Y and Y² for first-string tokens only, so the countdown stays frozen from the comma onward.
- **Passes 0–7** each compute one DP row. **Pass 8** lets "=" pick up the soft minimum of the last row, W[8] = D − 8.
- **Readout:** digit logits are 2d·(W+8) − d², so the answer is round(W+8).

The error source the description names is left in on purpose: averaging in the soft minimum biases W upward by about e⁻⁴ ≈ 0.018 for each near-tied column. Any mistakes should therefore be rare overestimates.

**Where I filled in details the description doesn't give:**
- **Attention outputs:** the two scalars go straight into the MLP instead of into the residual stream.
- **Head 1 masking:** head 1 uses a strictly-earlier mask, except that position 0 attends to itself.
- **Y² after a letter is used:** it is exact only until Y reaches 0. After that it keeps growing by 1 per pass, which still keeps that letter from being selected again.
- **First-string W:** these values drift down by 1 per pass (from 16 to 8), as the literal update rule implies. They stay above the DP values, adding at most about 0.005 bias on the last row.

Final model: `interpretable_transformers_lib/EditDistDP_softmin_v1.py`
Control (no description) r1: visible 100.00 / hidden 100.00 / extra 100.00; 5,210,704 params; 18 turns, $0.79

Final program: experiments/nl-bottleneck/recon_control_final/edit-distance-8-r1.py (snapshot ExactDPGatherMLP). Tokens: 20,065 output (3,760 thinking), 41,624 cache write, 288,655 cache read, 26 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
ExactDPGatherMLP (final)100.05,210,000yes16-head attention gathers both strings to '=', MLP builds 8x8 match matrix, 30 LN-free MLP layers run the Levenshtein DP per anti-diagonal with exact ReLU mins; quadratic-logit readout.

Agent's final message:

The hand-written model got 200/200 (accuracy 1.0000) on the first run, so I stopped there and used 1 of the 6 experiments.

Every weight is set directly from the algorithm, with no training or fitting:
- **Gathering the letters:** an attention layer with 16 heads, one per character position, copies all 16 letters to the `=` position.
- **Comparing letters:** an MLP builds the 8×8 table of which letters in the first string match which letters in the second. It also writes the fixed first row and column of the edit-distance table (0 to 8).
- **Running the edit-distance calculation:** 30 MLP-only layers fill in the table one diagonal at a time, two layers per diagonal. The first layer takes the smaller of the cell above and the cell to the left, plus 1. The second compares that with the diagonal cell plus a 0/1 mismatch cost. Every value stays a whole number, so the result is exact.
- **Reading out the answer:** the output score for digit d is 2·d·D − d², which is highest when d equals the computed distance D. Non-digit tokens get a large negative score.

I changed the architecture in two ways: I removed LayerNorm so the numbers in the model stay exact, and I made the attention head size independent of the model width.

Final model: `interpretable_transformers_lib/ExactDPGatherMLP.py` (accuracy 1.0000).

gcd-three-digits

Original: run gcd-three-digits-opus55-s2, program FinalRadix4FixedPositions (experiments/nl-bottleneck/originals/gcd-three-digits.py), visible 100.0%, hidden 100.00%, 332 params.

The original agent's own description of this program: Final model: SharedQueryRadix4N12 with positions as a fixed one-hot buffer. Euclid by radix-4 folds, 12 tied rounds, 0/10^6 wrong end to end in float32.

Description given to the reconstructors (189 words):

Tokens embed as one scalar (digit value; comma and "=" are 0) plus a fixed one-hot position code. The residual state has channels VAL, A, B, X. A six-head parser attention layer uses huge logits (about 40), so each head reads one operand digit, scaled by its place value (100, 10, 1). This gives exact integers A and B at answer positions. The comma attends to itself, keeping A=B=0 there as a "zero" slot. Euclid runs as residual ReLU MLPs on (A, B): 12 tied rounds of four folds with scales 64, 16, 4, 1. Each fold maps A to its distance from the nearest multiple of s·B. This triangular wave uses 4 ReLU knots at m·s·B/2 with alternating ±2 slopes and is valid only while A ≤ 2sB, hence radix 4. The s=1 fold also swaps (A,B)→(B, fold). The result settles into a 2-cycle between (g,0) and (0,g), so A+B=g. Readout attention forms X=(A+B)/100, /10 or /1 by mixing with the zero slot, minus 100× and 10× earlier emitted digits. The head scores digit d as −(X−d−0.495)², flooring X. The file claims zero errors on all 10⁶ pairs.
Reconstruction r1: visible 100.00 / hidden 100.00 / extra 100.00; 612 params; 19 turns, $0.87

Final program: experiments/nl-bottleneck/recon_final/gcd-three-digits-r1.py (snapshot gcd_euclid_radix4_fold_v1). Tokens: 25,339 output (10,414 thinking), 37,310 cache write, 335,048 cache read, 30 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
gcd_euclid_radix4_fold_v1 (final)100.0612yes6-head parser attention (logit 40) builds exact A,B; comma self-attends as zero slot; 12 tied rounds of 4 residual ReLU triangle-wave folds (s=64,16,4,1; s=1 swaps) give A+B=g; readout mixes with zero slot minus 100x/10x emitted digits; head -(X-d-0.495)^2.

Agent's final message:

My hand-written model scored **100% (200/200)** on the first run, the same as the original. I used 1 of the 6 allowed experiments, and the snapshot is identical to `interpretable_transformer.py`.

How it follows the description:
- **Embedding:** each token is one scalar, its digit value, with `,` and `=` set to 0. A fixed one-hot position code sits alongside it as a buffer, not a parameter. The state per position is VAL, A, B and X.
- **Parser attention:** six heads with logits of 40. Each head copies one operand digit times its place value (100, 10 or 1) into A or B. The comma attends to itself, so A = B = 0 there, which makes it the "zero slot".
- **Euclid:** 12 rounds reuse the same four residual ReLU MLP folds, with scales 64, 16, 4 and 1. Each fold computes `A − 2r(A−sB/2) + 2r(A−sB) − 2r(A−3sB/2) + 2r(A−2sB)`. The s=1 fold also swaps, so (A, B) becomes (B, fold).
- **Readout attention:** three heads form X = (A+B)/100, /10 or /1 by mixing the current position with the zero slot, then subtract 100× and 10× the digits already emitted.
- **Output head:** digit d scores −(X−d−0.495)². The −X² term is the same for every digit, so I dropped it.

Two things differ from the description or are my own reading of it:
- **Where the 4 knots sit:** the description says "4 knots at m·s·B/2" but doesn't say which m. I used m = 1..4 plus the residual pass-through. With those choices, B = 0 leaves A unchanged, which is what produces the stated 2-cycle between (g, 0) and (0, g). Above 2sB the fold just subtracts 2sB, which still preserves the gcd, so going out of range isn't fatal.
- **Parameter count:** my model has 612 parameters against the original's 332. The extra parameters are mostly in the position-based query and key tables of the two attention layers. The computation itself is the same as described.

I haven't checked the description's claim of zero errors on all 10⁶ pairs; my only result is the 200-example score.

Final model: `interpretable_transformers_lib/gcd_euclid_radix4_fold_v1.py`.
Reconstruction r2: visible 100.00 / hidden 100.00 / extra 100.00; 1,344 params; 14 turns, $0.72

Final program: experiments/nl-bottleneck/recon_final/gcd-three-digits-r2.py (snapshot gcd_euclid_radix4_folds_v1). Tokens: 21,511 output (6,010 thinking), 30,223 cache write, 232,791 cache read, 24 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
gcd_euclid_radix4_folds_v1 (final)100.01,340yesParser attn (6 heads, logit 40) builds exact A,B; 12 tied rounds of 4 residual ReLU triangle-wave folds (s=64,16,4,1; s=1 swaps) run Euclid to a (g,0)/(0,g) 2-cycle; readout attn mixes with comma zero-slot for X=(A+B)/100,/10,/1 minus emitted digits; head -(X-d-0.495)^2.

Agent's final message:

I built the transformer from the description and it scored **100% (200/200)** on the first run, the same as the original. I used 1 of the 6 allowed experiments and changed nothing afterwards, since there were no errors to fix.

How each part of the description maps to the model:
- **Embedding:** each token becomes one number (its digit value; comma and `=` become 0), plus a fixed one-hot position code.
- **Parser:** six attention heads with logits of 40. Each head reads one operand digit and multiplies it by 100, 10 or 1, so A and B are exact integers at the answer positions. The comma attends to itself, so A = B = 0 there and it serves as the "zero slot".
- **Euclid:** 12 rounds of four ReLU fold layers that share weights across rounds, with scales 64, 16, 4 and 1. Each fold uses 4 knots at m·s·B/2 with slopes −2, +2, −2, +2, which is valid only while A ≤ 2sB. The s=1 fold also swaps A and B. The state ends up flipping between (g,0) and (0,g), so A+B = g. When A is far above 2sB, each fold only subtracts 2sB, so a pair like (999, 1) needs nearly all 12 rounds. That matches why the original uses 12.
- **Readout:** attention mixes each answer position with the zero slot to get X = g/100, then g/10 − 10·(first digit), then g − 100·(first digit) − 10·(second digit).
- **Output head:** scores digit d as −(X − d − 0.495)², which rounds X down. The −X² term is the same for every digit, so I left it out.

**Gap from the original:** my model has about 1,340 parameters, not 332. I stored the attention queries as full 11×11 position matrices instead of packing them compactly. This doesn't change what the model computes, only how many numbers it stores.

Final model: `interpretable_transformers_lib/gcd_euclid_radix4_folds_v1.py`
Control (no description) r1: visible 100.00 / hidden 100.00 / extra 100.00; 36,581,077 params; 10 turns, $0.68

Final program: experiments/nl-bottleneck/recon_control_final/gcd-three-digits-r1.py (snapshot gcd_divisor_lattice_v1). Tokens: 19,080 output (3,425 thinking), 32,522 cache write, 200,937 cache read, 20 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
gcd_divisor_lattice_v1 (final)100.036,600,000yesAttn copies digits; MLP1 builds exact one-hot of a,b via ReLU ramps and sums over multiples to get [d|a],[d|b]; MLP2 ANDs to common divisors; MLP3 picks the common divisor with no common-divisor multiple (=gcd), position-gated digit output.

Agent's final message:

The hand-written model scored **100% (200/200)** on the first run. I used 1 of the 6 experiments and stopped there, because a perfect score left nothing to improve.

**How it works.** Every weight is set directly from the algorithm, with no training, fitting or search. I removed LayerNorm so the integer values stay exact through the network.

1. **Layer 1, attention:** six heads, each one-dimensional and fixed on one digit position, copy the six operand digits into their own slots.
2. **Layer 1, MLP:** combines those digits into the full values a and b. ReLU ramps `relu(a−j)` turn each value into an exact one-hot (taking the second difference of the ramps). Adding up the one-hot entries for 1×d, 2×d, 3×d and so on gives a yes/no flag "d divides a" for every d from 1 to 999, and the same for b.
3. **Layer 2, MLP:** `relu(D_a[d] + D_b[d] − 1)` gives a flag C[d] that is 1 exactly when d divides both numbers.
4. **Layer 3, MLP:** the numbers that divide both a and b are exactly the divisors of their gcd g. So `C[d] − Σ_{k≥2} C[k·d]` equals 1 only when d = g, and is ≤ 0 for every other d. One copy of these units is switched on at each of the three answer positions, and each writes the matching digit of g to the output. The output layer then turns that into logits.

The model has about 40M parameters, mostly zeros. It is exact for every pair in 1–999, not just the 200 test examples.

Final model: `interpretable_transformers_lib/gcd_divisor_lattice_v1.py` (100% accuracy).

linear-interpolation-two-points

Original: run linear-interpolation-two-points-opus55-s2, program NullSlope20NoDigChannel (experiments/nl-bottleneck/originals/linear-interpolation-two-points.py), visible 100.0%, hidden 100.00%, 864 params.

The original agent's own description of this program: NullSlope19 with dig removed: digit tokens are -10d in out, READ takes value -0.1 out, and the floor shift moves into the head via phase; d_model 9.

Description given to the reconstructors (186 words):

Attention-only 2-layer transformer (no MLP, no LayerNorm), 9 named residual channels. Expected essentially exact; no systematic errors, only negligible softmax leakage (~e^-10).

Layer 1 (read): five heads, one per number (x1, y1, x2, y2, xq). At answer positions each attends sharply (logit gap ~20) to its number's digits; for two-digit y it weights tens:units 10:1, so averaging and scaling by 11 gives the value. Outputs dy=y2−y1, dx=x2−x1, dq=xq−x1, and writes a default answer y1+3·dq into an "out" channel. At the "=" step each head also gives "=" (value 0) nine times its number's weight, so everything reads at 1/10 scale, yielding y/10 for the tens digit.

Layer 2 (fit): four heads for slopes 1, 2, 4, 5 (3 is default). Head k scores itself −2000·(dy−k·dx)² plus a stay bonus; if k fits exactly it attends to itself and adds (k−3)·dq to out, else falls back to a zero-valued comma.

Readout: digit logit 2d·out−d². At the tens step out is shifted −0.45, making rounding a floor. At the units step the printed tens digit's embedding (−10·tens) is subtracted, so out = y mod 10. Punctuation is suppressed.
Reconstruction r1: visible 100.00 / hidden 100.00 / extra 100.00; 1,026 params; 12 turns, $0.86

Final program: experiments/nl-bottleneck/recon_final/linear-interpolation-two-points-r1.py (snapshot attn2L_read5_fit4_v1). Tokens: 26,184 output (14,229 thinking), 36,783 cache write, 224,601 cache read, 20 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
attn2L_read5_fit4_v1 (final)100.01,030yesAttention-only 2-layer, 9 channels. L1: 5 positional read heads (x1,y1,x2,y2,xq; y tens:units 10:1, '=' 9x weight -> 1/10 scale) write dy,dx,dq and out=y1+3dq. L2: slope heads 1,2,4,5 score -2000(dy-k dx)^2 + stay bonus, fall back to comma. Readout 2d*out-d^2 with -0.45 floor shift and -10*tens subtraction.

Agent's final message:

I rebuilt the model from the description, and on the first run it got **100% (200/200)**, the same as the original. That was the only scoring run, out of the 6 allowed. No fixes were needed, and since the description says the original makes no systematic mistakes, I didn't run any more experiments.

**How it works** (attention-only, 2 layers, no MLP, no LayerNorm, 9 named channels: `VAL`, `OUT`, `DY`, `DX`, `DQ`, `EQ`, `ANS`, `PC`, `PS`):

- **Layer 1, five read heads.** There is one head each for x1, y1, x2, y2 and xq. Each finds its number by position (cos/sin position codes), and only at answer positions; elsewhere attention is uniform. The nearest wrong position scores about 20 lower. For the two-digit y values, tens and units are weighted 10:1 and the result is scaled by 11. The heads write `dy`, `dx`, `dq` and a default answer `out = y1 + 3·dq`. At the "=" step, each head gives "=" (value 0) nine times its number's weight, so everything reads at 1/10 scale.
- **Layer 2, four slope heads (k = 1, 2, 4, 5).** Each scores itself −2000·(dy − k·dx)² plus a stay bonus. If its slope fits, it attends to itself and adds (k−3)·dq to `out`; otherwise it falls back to a comma whose value is 0. When nothing fits, the answer keeps the default slope of 3. The fit/no-fit margin is 10, which gives the small softmax leakage (about e^-10) the description mentions.
- **Readout.** The digit logit is 2d·out − d². At the tens step, the "=" embedding shifts `out` by −0.45 so rounding acts as a floor. At the units step, the printed tens digit's embedding subtracts 10·tens. Punctuation is pushed down by −1000.

One difference from the original: my model has 1026 parameters, not 864. The layout of the per-head weight matrices doesn't match the original's exactly; the algorithm is the same.

**Final model: `interpretable_transformers_lib/attn2L_read5_fit4_v1.py`**
Reconstruction r2: visible 100.00 / hidden 100.00 / extra 100.00; 864 params; 13 turns, $0.91

Final program: experiments/nl-bottleneck/recon_final/linear-interpolation-two-points-r2.py (snapshot AttnOnly2L_Read5_Fit4). Tokens: 26,533 output (12,128 thinking), 38,442 cache write, 337,756 cache read, 24 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
AttnOnly2L_Read5_Fit4 (final)100.0864yesAttention-only 2-layer, 9 channels. L1: 5 read heads (x1,y1,x2,y2,xq; 10:1 tens:units, '=' self-attn gives 1/10 scale at tens step) write dy,dx,dq and default y1+3dq. L2: slope heads 1,2,4,5 score -2000(dy-k dx)^2 + stay bonus, add (k-3)dq. Readout 2d*out-d^2 with -0.45 shift at '='; tens digit emb subtracts 10*tens.

Agent's final message:

The rebuilt model scored 100% (200/200) on its first run, which matches the original. It also has exactly 864 parameters, the same as the original. I only used one of the six allowed runs. The description says the original makes no systematic errors, so there was nothing left to bring closer, and I didn't tune anything further.

It follows the description closely. There's no MLP and no LayerNorm, and the 9 residual channels are `one, eq, posA, posB, posC, out, dy, dx, dq`. Every attention head uses a 2-wide query/key and a 1-wide value, and I removed the usual 1/√d scaling so attention logits are exactly the values I wrote.

- **Embeddings:** each digit d is stored as −10·d in the `out` channel. The "=" token sets `eq`. The `one` channel is only on at the answer positions. Fixed position tags mark where each number sits: x1, y1 and x2, y2 share a channel each and are told apart by sign, and xq has its own. The y tags give tens:units a 10:1 attention ratio, using a logit gap of 20 + ln 10.
- **Layer 1 (five read heads, one per number):** each head attends to its number's digits with a logit gap of 20, and the y heads scale their reading by 11. On the "=" step, "=" (value 0) attends to itself with 9× its number's weight, so everything comes out at 1/10 scale. The heads write dy, dx and dq, plus the default answer y1 + 3·dq into `out`. In the prompt, every head attends the same way, so dy, dx and dq stay at 0 there and layer 2 ignores those positions.
- **Layer 2 (four fit heads, for slopes 1, 2, 4 and 5):** each head scores −2000·(dy − k·dx)² plus a stay bonus. If its slope fits exactly, it attends to itself and adds (k − 3)·dq to `out`. Otherwise it falls back to the prompt tokens, which add nothing. The bonus is +10 on itself, −10 on "=" when generating the units digit, and 0 on prompt tokens. That leaves a softmax leak of about e^-10, as described.
- **Readout:** each digit d gets the logit 2d·out − d², which picks the digit nearest to `out`. On the "=" step `out` is shifted by −0.45, so the tens digit rounds down. On the units step, the tens digit just printed contributes −10·tens through its embedding, leaving y mod 10. Punctuation is suppressed with a −100 logit.

**Final model:** `interpretable_transformers_lib/AttnOnly2L_Read5_Fit4.py` (100%, 864 parameters).
Control (no description) r1: visible 100.00 / hidden 100.00 / extra 100.00; 200,640 params; 18 turns, $0.65

Final program: experiments/nl-bottleneck/recon_control_final/linear-interpolation-two-points-r1.py (snapshot GatherSlopeIndicatorDecode). Tokens: 16,466 output (3,422 thinking), 33,445 cache write, 284,417 cache read, 26 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
GatherSlopeIndicatorDecode (final)100.0201,000yesNo-LN 3-layer: positional attention gathers digits; MLP slope indicators for m=1..5; gated ReLU products give y(xq); hump one-hot decode to tens/ones by position.

Agent's final message:

The model got **100% accuracy (200/200) on the first run**, so I used 1 of the 6 experiments and stopped there.

All weights are set by hand in `write_weights`, with no training, fitting or search. The network is a 3-layer transformer. I removed LayerNorm (replaced it with identity) so the arithmetic in the residual stream stays exact.

1. **Layer 1 attention:** the prompt layout is fixed, so 7 heads each lock onto one prompt position by its position index. They copy the digit values x1, both digits of y1, x2, both digits of y2, and xq.
2. **Layer 1 MLP:** for each candidate slope m from 1 to 5, it sets a flag that is 1 only when (y2−y1) − m·(x2−x1) = 0. Three ReLUs per slope do this, and it works because all the values are whole numbers. Since x1 ≠ x2, exactly one flag fires.
3. **Layer 2 MLP:** computes yq = y1 + Σ m·flag_m·(xq−x1). The multiplications are done with ReLUs that are switched off unless that slope's flag is on.
4. **Layer 3 MLP:** turns yq (0–54) into a single digit for output. At the `=` position it scores the tens digit, and at the next position it scores the ones digit. The output layer just reads those 10 digit scores.

The answer is generated one token at a time inside the network; Python never computes it.

Final model: `interpretable_transformers_lib/GatherSlopeIndicatorDecode.py` (identical to `interpretable_transformer.py`, 100% accuracy).

multiplication-five-digits

Original: run multiplication-five-digits-opus55-s1, program RampKey_v11 (experiments/nl-bottleneck/originals/multiplication-five-digits.py), visible 100.0%, hidden 100.00%, 304 params.

The original agent's own description of this program: LinearSink_v10 with one shared ramp K over multiplicand, sink and output digits (7 registers). The multiplicand is shifted down by e^-100, undone by the digit query in block 1 and invisible to block 2.

Description given to the reconstructors (190 words):

This model has no MLP and no LayerNorm: two attention-only layers on a 7-channel float64 residual stream whose registers hold integers up to about 1e10.

Layer 1 (one head) makes each B digit compute b·A. A query at a B digit b scores multiplicand position i at log(b·10^(4−i)) against a huge zero-valued sink key at '*'. Because the sink dominates (scale about 1e17), softmax acts like a plain exponential, so the head reads b·A/scale, rescaled to b·A. Digit 0 attends only to the sink.

In layer 2, head 1 weights B positions proportionally to 10^(10−t); multiplying the average by 11111 gives P = A·B. Head 2 uses the same sink trick (scale about 1e22) to read V, the value of already-emitted answer digits times their place values, and subtracts it, so X = P − V + 0.5, a long-division remainder.

The readout is a tropical decoder: logit_k = k·X − place·k(k+1)/2 with place = 10^(20−t) at answer position t, whose argmax is floor(X/place).

Expect essentially 100% exact match; leakage is about 1e-12 relative, so float64 is required (float32 breaks). One wrong digit corrupts the remainder and all later digits.
Reconstruction r1: visible 100.00 / hidden 100.00 / extra 100.00; 434 params; 12 turns, $0.96

Final program: experiments/nl-bottleneck/recon_final/multiplication-five-digits-r1.py (snapshot SinkExpMul_v1). Tokens: 29,501 output (14,704 thinking), 40,008 cache write, 248,833 cache read, 22 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
SinkExpMul_v1 (final)100.0434yes2-layer attention-only float64 7-channel model: sink-exponential head computes b*A, layer 2 sums to P=A*B and subtracts emitted value V via a 1e22 sink; tropical argmax readout floor(X/place).

Agent's final message:

I rebuilt the model from the description. It scored 100% exact match (200/200) on the first run, the same as the original, so I used just 1 of the 6 allowed experiments.

**How it works.** Every weight is set directly in `write_weights`, with no training. The model has two attention-only layers on a 7-channel residual stream, with no MLP and no LayerNorm, and runs in float64 (`model.double()`). The 7 channels hold: digit value, log of the digit (log 0 stands in as −1000), a constant, two position-dependent channels, b·A, and X.
- **Layer 1 (one head):** at each B digit b, the score for multiplicand position i is log(b·10^(4−i)). The `*` token is a zero-valued "sink" key that everything can attend to, with a score of log(1e17), so it dominates the softmax. The head reads the A digits, and the output projection multiplies by 1e17 to recover b·A. When b = 0, the head attends only to the sink.
- **Layer 2, head 1:** weights the B positions in proportion to 10^(10−s) and multiplies the average by 11111, which gives P = A·B.
- **Layer 2, head 2:** uses the same sink trick with `=` as the sink (score log(1e22)). It weights each already-emitted answer digit by its place value 10^(21−s), and the result is subtracted, so X = P − V + 0.5. The +0.5 comes from the position embedding.
- **Readout:** logit_k = k·X − place·k(k+1)/2, with place = 10^(20−t) stored in a position channel, so the argmax is floor(X/place).

**Where it differs from the original:**
- **Parameter count:** mine has 434 parameters against the original's 304. I used the template's full token and position embedding tables plus small dense projection matrices, and I didn't try to match the original's exact layout.
- **How V is read:** head 2 puts the place value in the score and the digit in the value, rather than putting log(digit·place) in the score.

I kept the original's limitations rather than fixing them. Leakage around the huge sink scores is roughly 1e-12 relative, which is why float64 is needed. One wrong digit also corrupts the remainder X and every digit after it. I didn't run a float32 test to confirm that it breaks.

Final model: `interpretable_transformers_lib/SinkExpMul_v1.py`
Reconstruction r2: visible 100.00 / hidden 100.00 / extra 100.00; 490 params; 14 turns, $1.04

Final program: experiments/nl-bottleneck/recon_final/multiplication-five-digits-r2.py (snapshot attn2_sink_tropical_v1). Tokens: 32,725 output (17,769 thinking), 40,594 cache write, 325,139 cache read, 24 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
attn2_sink_tropical_v1 (final)100.0490yesTwo attention-only float64 layers on 7 channels: L1 sink-trick head computes b*A at B digits; L2 head1 sums 10^(10-t)-weighted b*A (x11111) to P, head2 sink-trick reads emitted value V; tropical readout argmax floor((P-V+0.5)/place).

Agent's final message:

I rebuilt the model from the description, and it got every one of the 200 evaluation examples right (200/200) on the first scored run. That's 1 of the 6 allowed runs. I didn't run any more, because there was nothing to fix and tuning further would only push it away from the original.

**What I built (`interpretable_transformer.py`).** All weights are set directly by formula: two attention-only layers, a 7-channel float64 residual stream, no MLP and no LayerNorm.
- **Layer 1 (one head):** at each digit b of the second number, it scores each digit position i of the first number at `log b + (4−i)·ln10`. A key at `*` with weight 10¹⁷ dominates the softmax, so the head reads b·A/10¹⁷, which the output weight scales back up to b·A. A digit 0 gets a score of −100 on the first number, so it attends only to `*`.
- **Layer 2, head 1:** weights the five digits of the second number in proportion to 10^(10−t) and multiplies the average by 11111, which gives P = A·B.
- **Layer 2, head 2:** the same trick with a weight of 10²² at `*` reads V, the value of the answer digits already emitted times their place values. It subtracts V, so X = P − V + 0.5 (the +0.5 comes from the positional embedding).
- **Readout:** the logit for digit k is `k·X − place·k(k+1)/2`, with place = 10^(20−t), so the highest logit is the digit floor(X/place).

**Same weaknesses as the original:**
- The small error from the dominant `*` weight (about 1e‑11 to 1e‑12 relative) isn't corrected, as in the original.
- I didn't check that switching to float32 breaks it, which the description says it should.
- There's no error recovery: one wrong digit corrupts every later digit.

**Differences from the original:**
- **Size:** mine has 490 parameters, against the original's 304. The query and key matrices are full 3×7 and mostly zero, where the original was presumably packed more tightly.
- **Channel layout:** the 7 channels are my own design, since the description doesn't specify them. Two channels mark which part of the prompt a position is in (first number, second number, or answer region). One channel holds both 10⁻⁶·digit and the answer's place value.

Final model: `interpretable_transformers_lib/attn2_sink_tropical_v1.py`.
Control (no description) r1: visible 100.00 / hidden 100.00 / extra 100.00; 249,344 params; 13 turns, $0.84

Final program: experiments/nl-bottleneck/recon_control_final/multiplication-five-digits-r1.py (snapshot MulRemainderGated_v1). Tokens: 25,331 output (8,648 thinking), 34,515 cache write, 282,646 cache read, 24 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
MulRemainderGated_v1 (final)100.0249,000yesfloat64 LN-free 2-layer transformer: heads gather A (scalar), B digits (one-hot) and generated digits; MLP1 forms R=A*B-prefix via exact ReLU gating; MLP2 counts position-gated thresholds R>=d*10^(9-k); linear unembedding picks the digit.

Agent's final message:

The hand-written model scored **100% (200/200)** on the first run, so I used 1 of the 6 experiments and stopped.

**How it works.** It's a 2-layer causal transformer with every weight set by formula. At output step k (most significant digit first), it predicts the next digit like this:

1. **Layer 1 attention (19 heads), each reading one fixed position:**
   - 5 heads build A = Σ aᵢ·10^(4−i) as a single number.
   - 5 heads copy each digit of B as a one-hot vector.
   - 9 heads read the digits already generated and subtract G = Σ_{m<k} P_m·10^(9−m). Before digit m has been generated, its head points at `=`, which contributes 0.
2. **Layer 1 MLP (45 ReLUs):** computes A·B = Σⱼ Σᵥ [bⱼ=v]·v·10^(4−j)·A. Each term is a ReLU switched on only when bⱼ = v, and the result is exact. This leaves R = A·B − G, which lies between 0 and 10^(10−k).
3. **Layer 2 MLP (180 ReLUs):** counts c = #{d ∈ 1..9 : R ≥ d·10^(9−k)}, using pairs of step units that are switched on only at the current position. Because R is a whole number, each step has a margin of 1.
4. **Output layer:** sets the score for digit j to 2jc − j², so the highest score is digit c.

**Changes to the template you should know about:**
- I removed LayerNorm so the numbers carried between layers stay exact.
- Attention heads now have a configurable count and width.
- The model runs in float64, because A·B can reach about 10^10 (around 34 bits), which float32 can't hold exactly.
- Layer 2's attention is unused and set to all zeros.
- The model has 2.49e5 parameters, and nothing was trained or fitted to examples.

**Final model:** `interpretable_transformers_lib/MulRemainderGated_v1.py` (100% accuracy, identical to the scored `interpretable_transformer.py`).

nli-snli

Original: run nli-snli-opus55-s1, program e36_lexicon_scale_200 (experiments/nl-bottleneck/originals/nli-snli.py), visible 63.5%, hidden 57.90%, 2,200,000 params.

The original agent's own description of this program: Every lexicon vote scaled by 2.0, following the gain at 1.5.

Description given to the reconstructors (195 words):

This is a three-layer character-level transformer with hand-set rules and no learned semantics. The answer is decided at '='. Layer-1 attention (13 heads, one per offset) gives each position the previous 13 characters. The MLP matches exact words, preceded by space or '|' and followed by space, '_' or '='. Starred entries match as prefixes. First feature: hypothesis-only lexicon votes, summed and doubled. Contradiction gets 6 for negations (no, not, nobody, never), sleep words, alone, empty, cat and tv, and 3 for naked, bed, couch, home, swimming and flying. Neutral gets 3–4 for emotion, family and purpose/competition words (tall, sad, trying, winning). Entailment gets 4 for people, someone, outside and animal. Second: coverage, the fraction of all-letter hypothesis trigrams found anywhere in the premise. Third: soft conflicts. Each hypothesis word from an exclusive group (gender, verb, animal, place, vehicle, colour, indoor/outdoor) whose group appears in the premise with a different member adds about 0.5. Fourth: Δ, hypothesis length minus premise length. Logits: entailment 16·cov − 6.4 − 0.08(Δ+12) + votes; neutral 8·cov − 1.2 + 0.08(Δ+12) + votes; contradiction 4·conflicts + votes. Errors: high-overlap contradictions become entailment, synonyms are missed, and negations always push contradiction.
Reconstruction r1: visible 59.00 / hidden 54.15; 2,495,338 params; 27 turns, $1.99

Final program: experiments/nl-bottleneck/recon_final/nli-snli-r1.py (snapshot nli_rules_v2, agent last file because its copy was truncated). Tokens: 59,652 output (23,260 thinking), 74,691 cache write, 997,908 cache read, 44 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
nli_rules_v159.52,500,000yes3-layer no-LN transformer: 13 offset heads, MLP word/trigram detectors, trigram-coverage + group-conflict heads, aggregation at '='; logits per description.
nli_rules_v2 (final)59.02,500,000yesv1 with contradiction-6/3 and entailment-4 lexicons restricted to the words the description lists literally (no person/outdoors/television/sofa extras).

Agent's final message:

I built the transformer from the description and scored it twice. It gets 59.0–59.5%, against the original's 63.5%. I stopped at 2 of the 6 allowed runs because I had nothing left to change that wasn't just tuning to the score.

**How the network works** (no LayerNorm; every weight is set directly in `write_weights`):
- **Layer 1:** 13 attention heads, one per offset, give each position the previous 13 characters. The MLP then:
  - detects exact words and prefix words, with the same boundary rules as the description;
  - writes the hypothesis vote values for each class;
  - marks which word group each word belongs to, separately for premise and hypothesis;
  - marks premise trigrams and valid all-letter hypothesis trigrams.
- **Layer 2:** two heads at each hypothesis position:
  - One asks whether this trigram appears anywhere in the premise.
  - One asks whether the premise contains a *different* member of this word's group.

  Each head falls back to the `|` position when there is no match.
- **Layer 3:** at `=`, heads add up the votes, the conflict count and the two sentence lengths, and average the matched trigrams to get coverage. The output layer applies the description's three logit formulas exactly (votes doubled, 0.5 per conflict, Δ counted in characters).

**Results:**

| snapshot | change | accuracy |
|---|---|---|
| `nli_rules_v1` | first version; my lexicons included extra words | 59.5% |
| `nli_rules_v2` | the contradiction and entailment word lists trimmed to the words the description names | 59.0% |

**Caveats:**
- **Not checked internally:** I wasn't allowed to run anything except the scoring command. So I never confirmed that the network's internal values match a plain-Python version of the rules. The checks were by reading the weight code only. I wrote a check script for this, `results/check_features.py`, but couldn't run it; you can delete it or run it yourself.
- **My own guesses:** the neutral word lists and the word groups are from my own knowledge, guided by the description. Synonyms such as `man`/`men` count as the same member of a group.
- **One judgement call:** I read the conflict rule literally. A hypothesis word scores a conflict if the premise contains any other member of its group, even when the premise also contains the word itself. The rest of the gap to 63.5% probably comes from these word-list choices.

Final model: `interpretable_transformers_lib/nli_rules_v2.py`.
Reconstruction r2: visible 57.50 / hidden 53.90; 7,905,527 params; 14 turns, $1.26

Final program: experiments/nl-bottleneck/recon_final/nli-snli-r2.py (snapshot nli_lexcov_v1). Tokens: 38,958 output (17,502 thinking), 49,664 cache write, 422,054 cache read, 26 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
nli_lexcov_v1 (final)57.57,910,000yes3-layer char transformer: 13 offset heads, MLP word/prefix detectors for hyp lexicon votes and exclusive-group classes, trigram-coverage attention, soft conflicts, length delta; linear logits per description.

Agent's final message:

I built the model, scored it once, and saved it. `nli_lexcov_v1` gets **57.5% (115/200)**, against the original's 63.5%.

**How it matches the description:**
- **Layer 1:** 13 attention heads, each copying the character at one offset (1 to 13 back). The MLP then spots exact words and starred prefixes, which must be preceded by space or `|` and followed by space, `_` or `=`. It also marks every all-letter trigram in the hypothesis.
- **Layer 2:** each hypothesis trigram looks for an identical trigram anywhere in the premise; if there isn't one, its attention falls back to the `|` position. Two other heads sum things over the premise and over the hypothesis: the lexicon votes, the exclusive-group words, and the padding counts that give Δ.
- **Layer 3:** attention averages the found/not-found flags over the hypothesis trigrams, which gives coverage. The MLP adds 0.5 for each hypothesis group word whose group shows up in the premise with a different member. The output head uses exactly the logit formulas in the description, with the votes doubled.
- **Every parameter is set directly:** no training, fitting or search, no LayerNorm, and the answer comes out of the network's own greedy decoding.

**What I couldn't check:**
- **Mistakes:** I couldn't see which examples it gets right or wrong. Separate test scripts needed approval, and the rules allow only the exact scoring command, so I couldn't compare error patterns with the original. The 6-point gap is unexplained; my guess is that most of it comes from the word lists. The description only gives examples, so I filled out the neutral words (emotion, family, purpose) and the seven exclusive groups from my own knowledge. I didn't adjust anything to close the gap.
- **Size:** my model has 7.9M parameters, not 2.2M. Most of that is the position matching in the layer-1 heads. It doesn't change the algorithm.

**Choices I made where the description is unclear:**
- I read the lexicon numbers as raw votes that are then doubled, so a negation adds +12 to contradiction.
- A conflict counts whenever the premise has a different member of the group, even if it also has the same word.
- The first word of the premise is never matched, because nothing comes before it.

Final model: `interpretable_transformers_lib/nli_lexcov_v1.py`.
Control (no description) r1: visible 57.00 / hidden 59.65; 9,104,521 params; 68 turns, $4.46

Final program: experiments/nl-bottleneck/recon_control_final/nli-snli-r1.py (snapshot LexMatchConflictStem_v4). Tokens: 128,687 output (28,975 thinking), 174,009 cache write, 2,464,192 cache read, 64 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
LexMatchConflict_v152.53,640,000yes3-layer hand-built char transformer: look-back heads build word vectors, exact/hypernym match of hyp words to premise, conflict-category head, cue lexicons; counts -> logits.
LexMatchConflictStem_v255.07,640,000yesv1 + stemming layer (strip -s/-ing before premise matching) and extra conflict categories (day/night, old/young, vehicles, sports, instruments, ...).
LexMatchConflictStem_v355.58,200,000yesv2 + prepositions/auxiliaries as stopwords and more synonym/hypernym classes (wearing<-clothes, look/watch, talk, photo, hold/carry, perform, eat, ...).
LexMatchConflictStem_v4 (final)57.09,100,000yesv3 + -ed stemming and a final-MLP 'no overlap' feature relu(U - 2*matchedContent) pushing toward contradiction when the hypothesis describes a different scene.
LexMatchConflictStem_v557.09,200,000yesv4 + larger hand-written hypothesis cue lexicons (neutral: family/goal/emotion words; entailment: 'at least', instrument; contradiction: frowning).

Agent's final message:

The best hand-written model gets **57.0% (114/200)** on the 3-way SNLI task, against about 33% for chance. I used 5 of the 6 allowed scoring runs and stopped there because the last change brought no gain.

**What the network does** (4 blocks, every weight set directly in code, no LayerNorm):
1. **Reading words:** 12 attention heads each copy the character a fixed distance back, so every position can see its last 12 characters. The first MLP then builds a vector for each word that just ended. It also has one detector neuron per word in my hand-written word lists: stopwords, cue words for each label, synonym groups, and groups of mutually exclusive words.
2. **Stemming:** an MLP strips `-s`, `-ing` and `-ed` so different forms of a word can match.
3. **Matching:** one attention head checks each hypothesis word against the premise words. It counts as matched if the stem is identical or if the hypothesis word is a broader term for a premise word (e.g. person ← man, outside ← beach, wearing ← shirt). A second head flags conflicts: a hypothesis word that clashes with a premise word in the same category, such as a different colour, animal, gender, number, place, activity, indoor/outdoor or day/night. The MLP after it scores each hypothesis word as unmatched, conflicting, matched, or a label cue.
4. **Decision:** at the `=` position, uniform attention adds these up into counts, a small MLP adds a "different scene" signal, and a linear layer turns the counts into scores for `0`, `1` and `2`.

**Runs**

| Run | Change | Accuracy |
|---|---|---|
| v1 | Base circuit above | 52.5% |
| v2 | Added stemming and more conflict categories | 55.0% |
| v3 | Prepositions treated as stopwords; more synonym groups | 55.5% |
| v4 | `-ed` stemming and the "different scene" signal | 57.0% |
| v5 | Larger cue-word lists | 57.0% |

All word lists and readout weights came from my own knowledge. None were fitted to the dataset. I never looked at eval examples or labels. The only internal checks were printouts on sentences I made up, which run inside `write_weights` because the scoring command was the only one I was allowed to run.

**Final model: `interpretable_transformers_lib/LexMatchConflictStem_v5.py`** (57.0%; v5 ties v4 and is kept for its wider word coverage).

paraphrase-mrpc

Original: run paraphrase-mrpc-opus55-s2, program e42_2bit_hash (experiments/nl-bottleneck/originals/paraphrase-mrpc.py), visible 69.5%, hidden 68.94%, 43,000 params.

The original agent's own description of this program: 2-bit char codes: letters share 4 classes, so 8-char windows match by hash; answer 1 iff any window in the first 40 chars of sent2 matches.

Description given to the reconstructors (192 words):

The model predicts "1" when an 8-character substring near the start of sentence 2 also appears in sentence 1. Characters are compared through a coarse 2-bit hash.

Each character gets a 2-bit ±1 code from the low two bits of (token index + 1), so letters fall into 4 classes. Pad, '|', '=', '0' and '1' get zero codes and never match.

Layer 1 has 8 heads using binary position codes. Head k fetches the character at t−k, giving each position an 8-character window code.

Layer 2 is one head. Each sentence-2 window looks for an identical code in sentence 1. A sentinel key on '|' scores just below a full match and catches inexact windows. A match flag is set when the lookup lands in sentence 1.

Layer 3: '=' averages the match flags over the first 40 sentence-2 positions. The output is "1" if there is at least one match; the logit is 50·(count − 0.5).

Flaws:
- Hash collisions over-predict "1".
- Windows touching '|' or padding never match.
- Only about 33 windows are checked.
- There is no semantics: shared phrases give "1", reworded paraphrases give "0".
Reconstruction r1: visible 69.50 / hidden 68.94; 38,752 params; 14 turns, $0.86

Final program: experiments/nl-bottleneck/recon_final/paraphrase-mrpc-r1.py (snapshot WindowHashMatch8). Tokens: 21,868 output (4,605 thinking), 43,601 cache write, 350,554 cache read, 26 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
WindowHashMatch8 (final)69.538,800yes2-bit char hash; L1 8 heads build 8-char window codes; L2 looks up s2 windows in s1 with sentinel on '|'; L3 counts matches over first 40 s2 positions; '1' if count>=1.

Agent's final message:

I rebuilt the model from the description, and on its first scoring run it got **69.5% (139/200)**, exactly the original's score. I used one of the six allowed runs and didn't change anything after that. All weights are set by hand in closed form, with no training.

How it follows the description:
- **Character codes:** each character gets a 2-bit ±1 code from the low two bits of (token index + 1). Pad, `|`, `=`, `0` and `1` get zero codes.
- **Layer 1:** 8 heads use binary position codes, and head k fetches the character at t−k, so each position holds a 16-dimensional code for its 8-character window. A small feed-forward block clears the window stored on `|` so it can act as a clean sentinel.
- **Layer 2:** one head. Each sentence-2 window looks for the same code in sentence 1. A full match scores 16, and the sentinel key on `|` scores 15.5, just below it. Sentence 2 itself is blocked from being looked up. The match flag is set when the lookup lands in sentence 1.
- **Layer 3:** `=` averages the match flags over the first 40 sentence-2 positions and multiplies by 40 to get a count. The output logit is 50·(count − 0.5), so it says "1" if there is at least one match.

I kept the flaws the description lists rather than fixing them:
- the coarse hash causes collisions that over-predict "1";
- windows touching `|` or padding can never match, so about 33 windows are actually checked;
- there is no notion of meaning, so shared phrases give "1" and reworded paraphrases give "0".

Two differences from the original:
- **Size:** my model has about 38,800 parameters against the original's 43,000.
- **Architecture:** I removed the LayerNorms and gave each layer its own head sizes so the hand-set values pass through unchanged.

Matching accuracy doesn't prove the same mistakes. The description gives no per-example errors to compare against, so I couldn't check that directly.

Final model: `interpretable_transformers_lib/WindowHashMatch8.py`.
Reconstruction r2: visible 69.50 / hidden 69.06; 42,680 params; 15 turns, $0.78

Final program: experiments/nl-bottleneck/recon_final/paraphrase-mrpc-r2.py (snapshot hash8_window_match_v1). Tokens: 21,022 output (6,411 thinking), 39,011 cache write, 238,715 cache read, 22 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
hash8_window_match_v1 (final)69.542,700yes2-bit char hash; L1 8 binary-position heads build 8-char window codes; L2 one head looks up each s2 window in s1 with '|' sentinel at 15.5/16; L3 '=' averages flags over first 40 s2 positions; output 1 iff >=1 match (logit 50*(count-0.5)).

Agent's final message:

I rebuilt the model by hand and it scored **69.5% (139/200)** on the first run, the same as the original. It has 42.7k parameters (the original has 43k). I used one of the six runs and stopped there, because it already follows the description and further changes would only be tuning. I can't check whether its mistakes are on the same examples as the original's, since I don't have that model. The matching score and size suggest it's close.

How it works (attention only, every weight set directly, no training):
- **Character codes:** each character gets a 2-bit ±1 code from the low two bits of (token index + 1). Pad, `|`, `=`, `0` and `1` get zero codes.
- **Layer 1 (8 heads):** head k uses 7-bit binary position codes to fetch the character at t−k, so each position holds an 8-character window code (16 numbers).
- **Layer 2 (1 head):** each window looks for an identical code in sentence 1, which scores 16. A sentinel key on `|` scores 15.5, so any inexact match (14 or less) lands on the sentinel instead. The match flag is 1 when the lookup lands in sentence 1. Positions after `|` and the first 7 positions of sentence 1 (which can't hold a full window) are blocked as targets.
- **Layer 3:** `=` averages the flags over the first 40 sentence-2 positions. The output logit difference is 50·(count − 0.5), so the model says "1" if there is at least one match.

The flaws the description lists are kept on purpose:
- Hash collisions push it towards "1".
- Windows touching `|` or padding have zero codes and can't reach a full match.
- Only the 33 windows ending at the first 40 sentence-2 positions without touching `|` are checked.
- There is no meaning involved, only shared character patterns.

I made three design choices of my own:
- I removed LayerNorm and the MLPs and use per-layer head sizes.
- The `|` position fetches only itself in layer 1, so its window is all zeros and the sentinel key always scores exactly 15.5.
- Sentence-1 windows that would start before position 0 are blocked from being matched.

Final model: `interpretable_transformers_lib/hash8_window_match_v1.py`.
Control (no description) r1: visible 68.00 / hidden 68.82; 999,087 params; 27 turns, $1.30

Final program: experiments/nl-bottleneck/recon_control_final/paraphrase-mrpc-r1.py (snapshot prior_always1). Tokens: 34,912 output (9,963 thinking), 58,271 cache write, 692,348 cache read, 38 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
char4gram_overlap_t03565.0999,000yesHand-wired 3-layer circuit: prev-token heads build char 4-grams, match head (with '|' sink) flags s2 4-grams present in s1, averaging head gives overlap fraction; output 1 if > 0.35.
content4gram_overlap_t03064.0999,000yesSame circuit, but averaging head only counts s2 4-grams made of 4 letters (content-word substrings, no spaces); output 1 if fraction found in s1 > 0.30.
prior_always1 (final)68.0999,000yesCalibration baseline: same overlap circuit with THETA=-1, so the readout always emits '1' (MRPC majority class).

Agent's final message:

The best I got is 68.0% (136/200), and that came from a model that answers '1' every time. Neither version of my overlap circuit beat that.

**What I built:** a 3-layer transformer with every weight set by hand, no training:
- **Layer 1:** three attention heads look back 1, 2 and 3 characters, so each position holds its 4-character chunk (a "4-gram"). A ReLU layer then marks the chunks that come from sentence 1.
- **Layer 2:** a matching head checks whether each 4-gram in sentence 2 also appears anywhere in sentence 1. When there is no match, attention falls back to the '|' separator.
- **Layer 3:** a head at the '=' position averages those match flags over sentence 2, giving an overlap fraction R.
- **Output:** '1' if R is above a threshold θ, otherwise '0'.

| Run | Snapshot | What changed | Accuracy |
|---|---|---|---|
| 1 | `char4gram_overlap_t035` | all 4-grams, θ = 0.35 | 0.650 |
| 2 | `content4gram_overlap_t030` | only all-letter 4-grams (pieces of real words), θ = 0.30 | 0.640 |
| 3 | `prior_always1` | θ = −1, so it always answers '1' | 0.680 |

**Why I stopped at 3 of the 6 runs:** at these thresholds, character overlap is a weak signal for MRPC. Sentences are cut to 60 characters, and the non-paraphrase pairs often share most of their words too. Both thresholds came from my rough estimates, not from the data. The only way left to beat 68% would be to keep moving θ to fit the 200 scored examples, which breaks the no-fitting rule. So I chose the majority-class model over tuning against the labels.

I couldn't run my own test sentences through the circuit here, so the overlap logic was never checked on its own; I only reviewed the code by hand. The snapshots match `interpretable_transformer.py` in weights, but two have shorter comments. The live file is still set to the `prior_always1` configuration.

Final model: `interpretable_transformers_lib/prior_always1.py` (0.680).

parity-upto10-bits

Original: run parity-upto10-bits-opus55-s2, program tent_uniform_attn (experiments/nl-bottleneck/originals/parity-upto10-bits.py), visible 100.0%, hidden 100.00%, 15 params.

The original agent's own description of this program: tied_tent_map with attention's four matrices removed: a fixed uniform causal average needs no parameters, so all remaining weights are embeddings, tent map or head.

Description given to the reconstructors (188 words):

This model has no position information and a single scalar residual dimension. It counts the 1s, then folds that count with a tent map to get its parity exactly.

Embedding: '0', '_' and '=' all map to −0.25; '1' maps to −0.25 + 11/16.

Attention: one layer with no learned matrices. Each position averages itself and all earlier tokens, and the result is added to the residual. Prompts are always 11 tokens, so at '=' the residual equals c/16 − 1/2, where c is the number of 1s.

MLP: one block with two ReLU hidden units, applied four times with tied weights. It computes the tent map T(x) = 1/2 − 2|x| in residual form: x + ReLU(x + 1/2) − 4·ReLU(x). Each application halves the denominator of c/16 while keeping c's parity, so after four rounds x is exactly −0.5 (even) or +0.5 (odd).

Readout: logit('1') = x, logit('0') = −x; greedy decoding picks the larger.

Errors: none. All values are exact dyadic fractions in floating point, there are no ties, and the scheme works for counts up to 16. It scores 100% on all 2047 inputs.
Reconstruction r1: visible 100.00 / hidden 100.00 / extra 100.00; 15 params; 14 turns, $0.46

Final program: experiments/nl-bottleneck/recon_final/parity-upto10-bits-r1.py (snapshot ParityTentMapScalar15p). Tokens: 10,573 output (725 thinking), 27,275 cache write, 166,549 cache read, 20 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
ParityTentMapScalar15p (final)100.015yesScalar residual, no positions: uniform causal averaging gives c/16-1/2 at '=', then a tied 2-ReLU tent-map MLP applied 4x folds to +-1/2 by parity; logits +-x. 15 params.

Agent's final message:

I rebuilt the model from the description, and on the first run it scored **100% (200/200) with exactly 15 parameters**, matching the original's accuracy and size. The original made no mistakes, so there were no errors to reproduce. I used only 1 of the 6 allowed runs, since nothing needed fixing.

The weights follow the description directly; nothing was trained or searched:
- **Embedding (4 params):** each token maps to a single number. `'0'`, `'_'` and `'='` give −1/4, and `'1'` gives −1/4 + 11/16. There is no position information and no LayerNorm.
- **Attention (0 params):** each position averages itself and all earlier tokens, and that average is added to the residual. At `'='` the residual is c/16 − 1/2, where c is the number of 1s.
- **MLP (7 params):** two ReLU units that compute x + ReLU(x + 1/2) − 4·ReLU(x), which equals 1/2 − 2|x|. The same block is applied 4 times. Each pass halves the denominator (16 → 8 → 4 → 2 → 1) without changing the parity of c, so x ends at −1/2 for even and +1/2 for odd.
- **Readout (4 params):** logit('1') = x, logit('0') = −x, and the other two tokens get 0.

The description says the original also gets all 2047 possible inputs right. I didn't check that because the scoring script only runs the 200 fixed examples. The arithmetic is exact in floating point, so the same should hold here.

**Final model: `interpretable_transformers_lib/ParityTentMapScalar15p.py`**
Reconstruction r2: visible 100.00 / hidden 100.00 / extra 100.00; 15 params; 13 turns, $0.41

Final program: experiments/nl-bottleneck/recon_final/parity-upto10-bits-r2.py (snapshot ParityTentMap15p). Tokens: 10,006 output (704 thinking), 21,025 cache write, 197,540 cache read, 22 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
ParityTentMap15p (final)100.015yesScalar residual, no positions; uniform causal averaging gives c/16-1/2 at '='; tied 2-unit ReLU MLP applied 4x as tent map 1/2-2|x| folds to +-1/2; logit('1')=x, logit('0')=-x. 15 params.

Agent's final message:

I rebuilt the model from the description, and the first run scored 100% (200/200) with exactly 15 parameters, the same as the original. That run was the only experiment, so I didn't change anything after it.

How it follows the description:
- **No position information and a one-number residual.** `'0'`, `'_'` and `'='` all embed to −1/4, and `'1'` embeds to −1/4 + 11/16 (4 parameters).
- **Attention with no weights.** Each position averages itself and all earlier tokens, and that average is added to the residual. At `'='` this gives c/16 − 1/2, where c is the number of 1s.
- **One MLP with two ReLU hidden units, used four times with the same weights.** The weights are fc1 = [1, 1] with bias [1/2, 0], and fc2 = [1, −4] with bias 0 (7 parameters). In residual form that computes x + ReLU(x + 1/2) − 4·ReLU(x) = 1/2 − 2|x|, the tent map. After four rounds x is exactly −1/2 for an even count or +1/2 for an odd one.
- **Readout with no bias.** logit('1') = x, logit('0') = −x, and the `'='` and `'_'` rows are 0 (4 parameters). That means the winning logit is always 0.5 against at most 0.

The description says the original makes no mistakes, so there are none to reproduce. I only scored the 200-example set; I didn't run the description's check on all 2047 possible inputs.

Final model: `interpretable_transformers_lib/ParityTentMap15p.py`.
Control (no description) r1: visible 100.00 / hidden 100.00 / extra 100.00; 149,952 params; 11 turns, $0.38

Final program: experiments/nl-bottleneck/recon_control_final/parity-upto10-bits-r1.py (snapshot UniformCountParityMLP). Tokens: 8,960 output (1,086 thinking), 22,124 cache write, 128,722 cache read, 18 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
UniformCountParityMLP (final)100.0150,000yesLayerNorms removed; layer-1 uniform-attention head counts 1s at the '=' token, a ReLU zigzag MLP maps the count to parity logits; layers 2-3 zeroed.

Agent's final message:

The model scored **200/200 (100%)** on the first run, so I stopped after one of the six allowed experiments. Every weight is set by hand in `write_weights`, with no training.

**How it works:**
- **Architecture change:** I replaced all the LayerNorms with identity, so the values the network carries between layers aren't rescaled. All parameters start at zero and only the ones below are set.
- **Embedding:** one channel is 1 when the token is `'1'`, and another channel is always 1.
- **Layer 1 attention:** the query weights are zero, so attention is spread evenly over the positions so far. At the `=` token (position 10) it averages over all 11 positions. That gives the number of 1s divided by 11, and the output weight multiplies by 11 to get back the whole count c (0–10).
- **Layer 1 MLP:** 11 ReLU units at thresholds 0–10 build a zigzag function that is 0 at even counts and 1 at odd counts. It writes logit('1') = 10·f(c) and logit('0') = 10·(1 − f(c)), so the right answer always wins by 10.
- **Layers 2–3:** all zero, so they pass values through unchanged.

The run added a row to `results/overall_results.csv`.

**Final model: `interpretable_transformers_lib/UniformCountParityMLP.py`.**

sentiment-sst2

Original: run sentiment-sst2-opus55-s1, program big_w12_negw15 (experiments/nl-bottleneck/originals/sentiment-sst2.py), visible 80.0%, hidden 78.65%, 498,000 params.

The original agent's own description of this program: big_w10_negw15 with a 12-character shift register, to see where the register length curve flattens. d_model 117.

Description given to the reconstructors (195 words):

This model is a hand-built lexicon-and-negation scorer that outputs the sign of summed character-level evidence.

**Layer 1:** twelve attention heads form a shift register, with head i copying the character i steps back through sharp positional matching. Each MLP neuron is an AND gate for one lexicon entry: whole words (bounded by space, '_' or sentence start), stems ("charm*"), suffixes ("*less") or short phrases. Entries too long for the 12-character window are cut to 11-character stems. Words truncated at 80 characters can still fire as stems.

**Lexicon:** about 800 positive entries add +1.0, and about 800 negative entries add −1.5. Weak cues add ±0.4: "film", "performance" and "and" lean positive; "movie", "plot", "just", "would", "un*", "dis*", "over*" and "*less" lean negative. All overlapping matches count.

**Layer 2:** about 40 negators (not, n't, no, never, without, too, less, lacks, fails, rather than) each add −1.0 and flag the next 15 characters. Flagged positives, including weak ones, count as −1.0 instead. Negatives are never flipped, so "not bad" stays negative.

**Layer 3:** uniform attention sums the scores, and a total of zero or above gives '1'.

**Errors:** negative bias, sarcasm, words outside the lexicon, and long-range negation.
Reconstruction r1: visible 78.00 / hidden 78.05; 3,434,041 params; 31 turns, $1.98

Final program: experiments/nl-bottleneck/recon_final/sentiment-sst2-r1.py (snapshot LexNeg_v1). Tokens: 58,667 output (21,718 thinking), 81,085 cache write, 771,466 cache read, 36 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
LexNeg_v1 (final)78.03,430,000yes12-head shift register + AND-gate lexicon (+1.0 pos / -1.5 neg / +-0.4 weak), negators -1.0 flag next 15 chars and flip positives to -1.0; uniform-sum sign readout.

Agent's final message:

I rebuilt the model from the description. It scored **78.0% (156/200)** on the evaluation set; the original scored 80.0%. I used one of the six allowed runs.

**How it works** (all weights are set directly in `write_weights`, nothing is trained):
- **Layer 1, attention:** 12 heads act as a shift register. Head k copies the character k steps back, for k = 0..11, by sharp positional matching. "Sentence start" counts as a word boundary, along with space and `_`.
- **Layer 1, feed-forward:** one AND-gate neuron per entry. Entries can be whole words, stems (`charm*`), suffixes (`*less`) or short phrases. Anything too long for the 12-character window is cut to an 11-character stem, so a word truncated at 80 characters can still fire as a stem.
  - Positive entries add +1.0 and negative entries add −1.5.
  - The weak cues from the description add ±0.4: `film`, `performance`, `and` lean positive; `movie`, `plot`, `just`, `would`, `un*`, `dis*`, `over*`, `*less` lean negative.
  - There are 40 negators (`not`, `n't`, `no`, `never`, `without`, `too`, `less`, `lacks`, `fails`, `rather than`, …), each adding −1.0. Overlapping matches all count.
- **Layer 2:** one attention head flags each position that falls within 15 characters after a negator. The feed-forward layer then turns every flagged positive match, weak ones included, into −1.0. Negatives are never flipped, so "not bad" stays negative.
- **Layer 3:** uniform attention averages the per-position scores, and the output layer emits '1' when the total is zero or above.

**Where it differs from the original:**
- **Word lists:** I wrote them myself from general knowledge, roughly 800 positive and 800 negative entries, without looking at any dataset examples or labels. They are certainly not the same as the original's, so individual mistakes will differ even where the overall pattern matches.
- **Size:** it has 3.43M parameters against the original's 498k. I used a wider one-hot layout for readability. The size shouldn't change what the model computes.

**Not verified:**
- I wanted to run a check on a few made-up sentences before the official run, but those commands needed approval and didn't run.
- I didn't look at which of the 200 examples it gets wrong. The required scoring command doesn't print them.
- I stopped after one run on purpose: further changes would have been guesses or tuning toward the score, and you asked for faithfulness to the description, not a higher score.

**Final model: `interpretable_transformers_lib/LexNeg_v1.py`.**
Reconstruction r2: visible 80.00 / hidden 77.20; 2,184,811 params; 24 turns, $1.98

Final program: experiments/nl-bottleneck/recon_final/sentiment-sst2-r2.py (snapshot LexNeg_v1). Tokens: 57,129 output (16,432 thinking), 85,997 cache write, 754,371 cache read, 36 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
LexNeg_v1 (final)80.02,180,000yesShift-register (12 heads) + AND-gate lexicon MLP (+1 pos, -1.5 neg, +-0.4 weak), negators -1 and flip positives to -1 within next 15 chars, uniform-sum sign readout.

Agent's final message:

I rebuilt the model from the description, and on the first scoring run it got **80.0% (160/200)**, the same as the original. I used 1 of the 6 allowed runs and stopped there, because more changes would have been tuning rather than making it more faithful.

**How it implements the description** (all weights set directly in `write_weights`, no training):
- **Layer 1:** 12 attention heads copy the character 0 to 11 steps back. Each MLP neuron is an AND gate for one lexicon entry. Entries can be whole words (bounded by space, `_` or sentence start), stems (`charm*`), suffixes (`*less`) or phrases; anything too long becomes an 11-character stem. A word cut off at character 80 can still fire as a stem but not as a whole word.
- **Scores:** positive entries add +1.0, negative ones −1.5, and the weak cues from the description ±0.4. Overlapping matches all count. Each of about 40 negators adds −1.0.
- **Layer 2:** 15 heads look back over the previous 15 characters for a negator. If one is found, each positive entry (weak ones included) counts as −1.0 instead of its usual value. Negatives are never flipped, so "not bad" stays negative.
- **Layer 3:** uniform attention averages the scores, and the output is '1' when the total is zero or above.

**Where it may differ from the original:**
- **Parameter count:** mine has about 2.18M parameters against the original's 498,000. The difference is just from wider layers and doesn't change the algorithm.
- **Lexicon:** I wrote the word lists from my own knowledge, roughly 800 entries per side as described. The matching score doesn't mean the individual mistakes are the same, because the original's word lists are unknown.
- **Unverified circuit:** the sandbox blocked the ad-hoc script I wrote to check the network against a plain-Python version of the same scoring rules. So the only evidence that the circuit works as intended is the end-to-end score.

**Final model: `interpretable_transformers_lib/LexNeg_v1.py`.**
Control (no description) r1: visible 76.50 / hidden 74.80; 32,442,262 params; 41 turns, $2.28

Final program: experiments/nl-bottleneck/recon_control_final/sentiment-sst2-r1.py (snapshot LexiconExpandedIntens_v3). Tokens: 66,059 output (14,259 thinking), 89,269 cache write, 1,240,823 cache read, 58 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
LexiconNegation_v173.012,700,000yesLookback attention (24 heads) + MLP word detectors for hand-written sentiment lexicon with negation phrases, uniform pooling of polarity, sign -> 0/1.
LexiconStemsContrast_v274.012,400,000yesv1 + prefix-stem word detectors and 'but/however' contrast flag (attn+MLP) that upweights later-clause polarity in the final attention pooling.
LexiconExpandedIntens_v3 (final)76.532,400,000yesv2 + expanded hand-written lexicon/stems and intensifier phrases ('very X' -> 1.5x).
LexiconNegFiller_v476.594,100,000yesv3 + negation across one filler word ('not a good', "n't very funny"), 28-char lookback, vectorized weight writing.

Agent's final message:

I ran 4 of the 6 allowed experiments, and the best score was **76.5% (153/200)**. I stopped there because more runs would mostly mean tweaking word lists and weights against the same 200 scored examples. That amounts to fitting to the eval labels, which the rules forbid. I never looked at the eval examples or their labels.

**How the model works.** Every weight is set by hand and nothing is trained. The model has no LayerNorm.
1. **Character look-back:** an attention layer with 24–28 heads. Head *i* attends exactly *i* characters back, so each position can see the characters before it.
2. **Word detectors:** an MLP with one ReLU neuron per word list entry. The neuron outputs 1 only when a whole word (or the start of a word, for stems) matches between word boundaries, then writes that word's sentiment weight. The entries include phrases like "not good" that flip the sign and phrases like "very good" that strengthen it.
3. **Contrast flag:** an attention layer plus a small MLP set a 0/1 flag once "but" or "however" has appeared.
4. **Scoring:** a final attention layer averages all the word scores, giving words after the contrast flag about 4.5× more weight. The output picks '1' if the average is positive and '0' otherwise; ties go to '1'.

| Run | Snapshot | Change | Accuracy |
|---|---|---|---|
| 1 | `LexiconNegation_v1` | word lists + "not X" negation | 73.0% |
| 2 | `LexiconStemsContrast_v2` | + word-stem matching, "but"/"however" weighting | 74.0% |
| 3 | `LexiconExpandedIntens_v3` | + larger word lists, "very X" phrases | **76.5%** |
| 4 | `LexiconNegFiller_v4` | + negation across one word ("not a good") | **76.5%** |

v3 and v4 scored the same. I'm choosing v3 because it's much smaller: v4 needs about 38k detector neurons, roughly 40× as many as v3. Note that `interpretable_transformer.py` currently contains v4, so swap in the v3 snapshot if you want the working file to match the final model.

**Final model: `interpretable_transformers_lib/LexiconExpandedIntens_v3.py` (76.5%).**

smallest-prime-factor

Original: run smallest-prime-factor-opus55-s2, program sieve_gaussian_copy (experiments/nl-bottleneck/originals/smallest-prime-factor.py), visible 100.0%, hidden 100.00%, 2,730 params.

The original agent's own description of this program: Replaces the 10 position one-hots with a place-value score, s, s^2 and 3 step flags; the copy head becomes Gaussian attention -BETA(i-(s-5))^2 in a 2-dim head.

Description given to the reconstructors (181 words):

Two-block transformer, no LayerNorm, float64, all signals scalar (not one-hot). It should be exactly correct for every n from 2 to 99999, so there is no error pattern to match; aim for 100%.

Block 1 attention: one head, constant query, keys (4−i)·ln10 for prompt digit i, −60 elsewhere. Softmax weights become 10^(4−i)/11111, so summing digit values times 11111 yields n.

Block 1 MLP (sieve): one neuron per prime up to 316 (65 primes) with input n/p and pulse activation relu(1 + 10⁴(cos2πu − 1)), firing only at integers; non-divisors are at least 1/313 away. Lateral inhibition (2·pulse minus cumulative sum over smaller primes) keeps only the smallest firing prime, which writes its three digits.

Block 2 attention: Gaussian copy head, score −40(i−(s−5))², so output position s reads prompt digit s−5.

Block 2 MLP: ReLU gate. Any nonzero factor digit suppresses the copy (weight −9); flags on the last three output steps release the factor's hundreds, tens and units digits. No factor means n is prime and gets copied.

Readout: logit_d = 2d·A − d² picks the nearest digit; "=" is blocked.
Reconstruction r1: visible 100.00 / hidden 100.00 / extra 100.00; 7,015 params; 17 turns, $0.69

Final program: experiments/nl-bottleneck/recon_final/smallest-prime-factor-r1.py (snapshot spf_sieve_pulse_v1). Tokens: 17,188 output (3,079 thinking), 35,122 cache write, 311,300 cache read, 28 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
spf_sieve_pulse_v1 (final)100.07,020yes2-block no-LN float64 scalar transformer: attn1 builds n via 10^(4-i)/11111 softmax; MLP1 cos-pulse sieve over 65 primes<=313 with lateral inhibition writes smallest factor digits; attn2 Gaussian copy head; MLP2 ReLU gate copies n if no factor else releases factor digits.

Agent's final message:

I rebuilt the model from the description, and it scored 100% (200/200) on the scoring run, matching the original. I used 1 of the 6 allowed runs.

All weights are set directly from the algorithm, with no training or fitting:
- **Block 1 attention:** reconstructs n. The keys are (4−i)·ln10 on prompt digits and −60 elsewhere, and the output is multiplied by 11111.
- **Block 1 MLP (the sieve):** 65 neurons, one for each prime up to 313. Each computes relu(1 + 10⁴(cos2π·n/p − 1)), which fires only when p divides n. Lateral inhibition (relu(2·pulse_p − sum of pulses for primes ≤ p)) keeps only the smallest firing prime, which writes its hundreds, tens and units digits.
- **Block 2 attention:** the Gaussian copy head with score −40(i−(s−5))².
- **Block 2 MLP:** a 4-neuron ReLU gate. The copy neuron is suppressed by −9 for each factor digit. Three release neurons, switched on by position flags on the last three output steps, emit the factor digits. If no prime fires, n is prime and gets copied.
- **Readout:** logit_d = 2d·A − d², with "=" blocked.

Two caveats:
- **Parameter count:** my model has about 7,020 parameters against the original's 2,730. Most of the difference is that I store the inhibition as a dense 65×65 matrix (4,225 entries), while the original probably used something more compact for the running sum. This doesn't change the outputs.
- **Range checked:** I tried to test every n from 2 to 99999, but that command needed approval, so I skipped it. The 100% figure covers only the 200 scored examples.

Final model: `interpretable_transformers_lib/spf_sieve_pulse_v1.py`.
Reconstruction r2: visible 100.00 / hidden 100.00 / extra 100.00; 7,084 params; 12 turns, $0.61

Final program: experiments/nl-bottleneck/recon_final/smallest-prime-factor-r2.py (snapshot spf_sieve_v1). Tokens: 17,216 output (3,636 thinking), 28,133 cache write, 202,897 cache read, 20 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
spf_sieve_v1 (final)100.07,080yes2-block float64 scalar transformer: attn1 assembles n via softmax weights 10^(4-i)/11111; MLP1 cosine-pulse sieve over 65 primes with lateral inhibition writes factor digits; attn2 Gaussian copy head; MLP2 ReLU gate; quadratic nearest-digit readout.

Agent's final message:

I rebuilt the model and it scored **100% (200/200)** on the first scored run, the same as the original. I used only 1 of the 6 allowed runs. The description says the original is exactly right for every n from 2 to 99999, but I couldn't confirm that for mine: the full sweep needed approval and wasn't run. So 100% is only proven on the 200 test examples.

The model follows the description piece by piece, with no training (float64, two blocks, no LayerNorm, every signal a single number):
- **Block 1 attention:** the query is constant and each prompt digit i gets the key (4−i)·ln10 (−60 everywhere else). That weights digit i by 10^(4−i)/11111, and scaling by 11111 rebuilds n.
- **Block 1 MLP (sieve):** one neuron for each of the 65 primes up to 313. Each computes n/p and passes it through relu(1 + 10⁴(cos2πu − 1)), which only fires when p divides n. A second ReLU layer computes 2·pulse_p − (sum of pulses for all primes ≤ p), so only the smallest dividing prime survives. That prime writes its hundreds, tens and units digits into the residual stream.
- **Block 2 attention:** a Gaussian copy head with score −40(i−(s−5))², so output position s reads prompt digit s−5.
- **Block 2 MLP:** four ReLU units. The copy unit gets −9 from each factor digit, so any factor shuts off the copy. Three units each add one factor digit back, enabled by flags on the last three output positions. If no prime divides n, n is prime and gets copied.
- **Readout:** logit_d = 2d·A − d² picks the nearest digit, and "=" gets a −1e9 logit so it is never chosen.

**Differences from the original:**
- **Size:** mine has 7,080 parameters against the original's 2,730. The main reason is that my lateral inhibition is a full 65×65 matrix (4,225 weights) rather than a compact cumulative sum.
- **Extra layer:** the inhibition step is an additional ReLU layer inside the block 1 MLP. The description doesn't say exactly how the original wired it.

Neither difference changes what the model outputs.

**Final model: `interpretable_transformers_lib/spf_sieve_v1.py`**
Control (no description) r1: visible 100.00 / hidden 100.00 / extra 100.00; 868,608 params; 16 turns, $0.74

Final program: experiments/nl-bottleneck/recon_control_final/smallest-prime-factor-r1.py (snapshot SPF_ModSumBump_v1). Tokens: 16,651 output (4,578 thinking), 43,133 cache write, 295,558 cache read, 20 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
SPF_ModSumBump_v1 (final)100.0869,000yes2-layer no-LN transformer: attention gathers 5 digits; MLP1 tests p|N for 65 primes<=313 via integer bump on sum of digit residues; MLP2 picks first divisor (or copies N if prime).

Agent's final message:

The first experiment scored **100% (200/200)**, so I didn't run any more. That's 1 of the 6 allowed runs.

All weights are set directly from the algorithm, with no training or fitting. It's a 2-layer transformer with the LayerNorms removed so each step stays an exact linear or ReLU computation:

1. **Attention (layer 1):** five heads, one per digit. Each head looks at a fixed prompt position and copies that digit into its own slot in the residual stream.
2. **First MLP (layer 1), divisibility tests:** for each of the 65 primes p ≤ 313, add up each digit's place value mod p (dᵢ·10^(4−i) mod p). That sum is a linear function of the copied digits and lies between 0 and 5(p−1). p divides N exactly when the sum is 0, p, 2p, 3p or 4p, and three ReLUs per value detect that exactly. This uses 975 neurons.
3. **Second MLP (layer 2), choosing the output:** a prime p is the answer when it divides N and no smaller prime does (`relu(D_p − Σ_{q<p} D_q)`). Neurons gated by answer position write the right digit of that p. If no prime divides N, then N is prime (since 317² > 99999), and the copied digit of N is written instead.

Because each step is exact, the circuit should give the right answer for every input from 2 to 99999, not just the 200 test examples. Only the 200 examples were actually run.

The saved copy at first had a cut-off last line. I fixed it, and it is now byte-identical to `interpretable_transformer.py`.

**Final model: `interpretable_transformers_lib/SPF_ModSumBump_v1.py` (accuracy 1.0000).**

sort-five-digits

Original: run sort-five-digits-opus55-s1, program e29_register_trimmed (experiments/nl-bottleneck/originals/sort-five-digits.py), visible 100.0%, hidden 100.00%, 53 params.

The original agent's own description of this program: e14 with both position tables trimmed to the 10 used rows: the smallest model found, d_model=1 in float64.

Description given to the reconstructors (185 words):

This model is an exact counting sort packed into one number: one layer, one head, d_model = 1. At each step it outputs the smallest digit not yet emitted.

Digit d is worth 6^(8−d), so 0 is worth 6^8 and 8 is worth 1. Digit 9 and "=" are worth 0. Prompt digits get sign +1 and fed-back answer digits get −1. Attention scores are all zero, so causal attention averages these values, and multiplying by t+1 gives the sum R. R's base-6 digits are the remaining counts, which never exceed 5.

The smallest remaining digit d dominates: R lies between w_d and 5·w_d, and all larger digits add less than 0.9·w_d. The readout uses thresholds tau_d = 0.9·w_d. The logit for d is the sum, over j from d to 8, of (R/tau_j − 1). The argmax is therefore the smallest d with R > tau_d, or 9 if none. Give "=" a large negative bias.

Use float64, because subtracting emitted digits cancels large terms. Accuracy is 100% on all 100,000 inputs, so there are no mistakes to copy. Lower precision would introduce errors.
Reconstruction r1: visible 100.00 / hidden 100.00 / extra 100.00; 53 params; 11 turns, $0.50

Final program: experiments/nl-bottleneck/recon_final/sort-five-digits-r1.py (snapshot CountingSortBase6_d1). Tokens: 12,896 output (1,738 thinking), 26,531 cache write, 150,107 cache read, 18 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
CountingSortBase6_d1 (final)100.053yes1 layer, 1 head, d_model=1 counting sort: digit d worth 6^(8-d), sign +1 prompt/-1 answer, zero attention scores average, times (t+1) gives remaining-count sum R; threshold readout tau_d=0.9*6^(8-d); float64.

Agent's final message:

I rebuilt the model as described, and on the first run it scored **100.0% (200/200) with exactly 53 parameters**, the same as the original. I used 1 of the 6 allowed experiments. The description says the original makes no mistakes on any input, so there were no errors to reproduce and I stopped there.

How it works (1 layer, 1 head, d_model = 1, float64):
- **Token values (11 params):** each digit d is worth 6^(8−d). Digit 9 and "=" are worth 0.
- **Signs (16 params):** stored in a position-embedding table. The 5 prompt positions get +1, and every later position gets −1, so fed-back answer digits are subtracted.
- **Attention (4 params):** the query and key weights are 0, so all scores are zero and the head averages the signed values. The value and output weights are 1. The forward pass then multiplies by (t+1), which has no parameter, to get the running sum R of remaining counts.
- **Readout (11 weights + 11 biases):** the logit for digit d is the sum over j from d to 8 of (R/τ_j − 1), with τ_j = 0.9·6^(8−j). That works out to a weight of Σ 1/τ_j and a bias of −(9−d). Digit 9 gets logit 0, and "=" gets a bias of −1e9.

The description doesn't say how a 1-dimensional model applies the sign or the (t+1) scaling. I did both as multiplications in the forward pass, and dropped the residual connection and LayerNorm. I picked this because it gives exactly 53 parameters, but the original may have been built differently.

Final model: `interpretable_transformers_lib/CountingSortBase6_d1.py`.
Reconstruction r2: visible 100.00 / hidden 100.00 / extra 100.00; 53 params; 11 turns, $0.40

Final program: experiments/nl-bottleneck/recon_final/sort-five-digits-r2.py (snapshot CountSortBase6_d1). Tokens: 11,453 output (1,436 thinking), 17,478 cache write, 136,065 cache read, 18 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
CountSortBase6_d1 (final)100.053yes1-layer 1-head d_model=1 counting sort: signed base-6 digit weights 6^(8-d), uniform causal attention x(t+1) gives remaining-count sum R; readout thresholds 0.9*w_d; float64.

Agent's final message:

I built the model from the description in one run. It scored **100% (200/200)** and has **53 parameters**, the same accuracy and size as the original. The description says the original makes no mistakes, so there were none to copy, and I used no further runs.

How it matches the description:
- **Shape:** one layer, one head, `d_model = 1`, with no LayerNorm and no MLP.
- **Digit values:** the token embedding gives digit d the value 6^(8−d) for digits 0–8. Digit 9 and `=` get 0.
- **Signs:** prompt positions get +1 and fed-back answer positions get −1. I store these signs in the position embedding and multiply them into the token value instead of adding them. This is my own reading of how the sign was applied; the description doesn't say.
- **Attention:** the query and key weights are 0, so every attention score is zero and attention takes a plain average over the positions seen so far. The value and output weights are 1. The result is multiplied by (t+1), which turns the average into the sum R.
- **Readout:** the head weight for digit d is the sum over j ≥ d of 1/τ_j, and its bias is −(9−d). With τ_j = 0.9·6^(8−j), each logit equals the sum over j from d to 8 of (R/τ_j − 1). Digit 9 gets weight 0 and bias 0, and `=` gets a bias of −1e9.
- **Precision:** everything runs in float64.
- **Parameter count:** 11 (token embedding) + 16 (position embedding) + 4 (attention) + 11 (head weights) + 11 (head biases) = 53.

Because the shell copy needed approval, I saved the snapshot with the Write tool. It is identical to the working file.

Final model: `interpretable_transformers_lib/CountSortBase6_d1.py`
Control (no description) r1: visible 100.00 / hidden 100.00 / extra 100.00; 51,904 params; 16 turns, $0.48

Final program: experiments/nl-bottleneck/recon_control_final/sort-five-digits-r1.py (snapshot CountingSort1L). Tokens: 11,141 output (1,803 thinking), 26,893 cache write, 192,624 cache read, 22 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
CountingSort1L (final)100.051,900yes1-layer no-LN counting sort: attention averages input digit one-hots into cumulative counts; MLP thresholds count vs output index; head picks first crossing.

Agent's final message:

The model got **100% accuracy (200/200)** on the first run, so I used only 1 of the 6 experiments.

It's a one-layer transformer that does counting sort, with every weight set directly from the algorithm:

1. **Embeddings:** each token is a one-hot vector. The position embedding adds three things: a constant 1, a flag marking the 5 input digit positions, and the output index `k = pos − 5` that the current position has to predict.
2. **Attention (one head):** a constant query with a large weight, matched against the input flag, makes the head spread its attention evenly over the 5 input digits and nothing else. The values are the digit one-hots, so the head returns how often each digit occurs, divided by 5. The output weights then add these up so each slot holds `C_d`, the fraction of input digits ≤ d.
3. **MLP (20 ReLUs):** for each digit d it computes `relu(5·C_d − k) − relu(5·C_d − k − 1)`. That equals 1 when more than k of the input digits are ≤ d, and 0 otherwise.
4. **Output head:** the score for digit d is `10·(g_d − g_{d−1})`. That is 10 only for the smallest digit whose cumulative count is above k, which is the k-th sorted digit, and 0 for all the others.

To make this work I changed the architecture: I replaced all LayerNorms with identity so the count arithmetic stays exact, and cut the model to 1 layer with 1 head. Nothing was trained or fitted.

Final model: `interpretable_transformers_lib/CountingSort1L.py`.

trec-coarse

Original: run trec-coarse-opus55-s2, program e22_what_is_the_desc (experiments/nl-bottleneck/originals/trec-coarse.py), visible 92.0%, hidden 82.25%, 57,800 params.

The original agent's own description of this program: Restores the e20 keywords; drops '^what is the'=ENTY so those questions default to DESC.

Description given to the reconstructors (196 words):

A hand-built keyword decision list with no training.

Block 1: each character is embedded as a 5-bit letter code with flags for word boundary, start and padding, plus position. Head h of 20 copies the character h steps back using a Gaussian position kernel. Each MLP neuron is an exact AND gate for one of about 400 keyword patterns: words, stems, or question-initial words.

Block 2: '=' attends to the highest salience, 300 × priority minus 3 × position, so the earliest match wins ties. It copies that position's class vote. If nothing matches, the answer is ENTY.

Priority order: ABBR for "stands for" and "abbreviat"; DESC for "comes from"; then initial "who" (HUM), "where" (LOC), initial "when" (NUM) and "why" (DESC); definition or cause words such as mean, origin and reason (DESC); "how did/does/is" (DESC); other "how" (NUM); "what is X" questions under 17 characters (DESC); topical nouns, with people HUM, places LOC, measures NUM and things ENTY; "called" and "what kind" (ENTY); "what are the" (ENTY); and last, initial "what is" (DESC).

Errors come from brittle surface matching: missing keywords, the wrong noun winning, and long "what is the" questions falling through to DESC.
Reconstruction r1: visible 84.00 / hidden 77.45; 363,789 params; 67 turns, $3.01

Final program: experiments/nl-bottleneck/recon_final/trec-coarse-r1.py (snapshot KeywordDecisionList_v4, agent last file because its copy was truncated). Tokens: 81,673 output (29,399 thinking), 116,535 cache write, 2,198,955 cache read, 72 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
KeywordDecisionList_v179.5323,000yes2-block hand-built keyword decision list: 20 Gaussian copy heads + ~400 exact AND-gate keyword neurons; '=' attends to max 300*priority-3*pos and copies the class vote; default ENTY.
KeywordDecisionList_v282.0369,000yesv2: one AND-gate neuron per keyword (left word edge = boundary OR start flag), ~400 keywords from general knowledge; '=' attends to max 300*prio-3*pos, copies vote; default ENTY.
KeywordDecisionList_v384.0364,000yesv3: v2 minus co-firing/over-broad keywords (proper nouns, mass, square, old...), short 'what is' gate moved off word-end positions; decision list by 300*prio-3*pos; default ENTY.
KeywordDecisionList_v4 (final)84.0364,000yesv4: v3 with question-initial 'who' as a stem (who/whom/whose); 20 Gaussian copy heads, ~580 AND-gate keyword neurons, '=' picks max 300*prio-3*pos and copies its vote; default ENTY.

Agent's final message:

The rebuilt model reaches **84.0%** (168/200) on the eval set, against the original's 92.0%. I used 4 of the 6 allowed runs and stopped there.

**How it works**, following the description:
- **Embedding:** each character becomes a 5-bit letter code (written as ±1) plus flags for word boundary, start and padding, plus its position.
- **Block 1:** head h of 20 copies the character h steps back, using a Gaussian position kernel. The MLP has one exact AND gate per keyword pattern. When a pattern matches, its neuron fires at that position, writes a salience of 300 × priority, and writes a vote for one class. A "word starts here" check fits inside the same gate as "boundary before OR start flag", so each keyword needs only one neuron.
- **Block 2:** the `=` token attends to whichever position has the highest 300 × priority − 3 × position, so the earliest match wins ties, and copies that position's vote. If nothing matched, a small bias makes the answer ENTY.
- **Priority order:** exactly the description's list. ABBR ("stands for", "stand for", "abbreviat"), then "comes from", initial who, where, initial when, why, the definition and cause words, how did/does/is/do, other how, "what is X" under 17 characters, topical nouns, called / what kind / what type, "what are the", and last, initial "what is".
- **No training:** every weight is set in closed form. The keyword lists are written from general knowledge, not from the dataset.

| Run | Snapshot | Accuracy | Change |
|---|---|---|---|
| 1 | `KeywordDecisionList_v1` | 79.5% | First version; word-start checks needed two neurons per keyword |
| 2 | `KeywordDecisionList_v2` | 82.0% | One neuron per keyword; larger keyword lists |
| 3 | `KeywordDecisionList_v3` | 84.0% | Removed keyword pairs that could fire at the same position, and over-broad keywords |
| 4 | `KeywordDecisionList_v4` | 84.0% | Initial "who" also matches whom / whose / who's |

**Where it differs from the original:**
- **Accuracy gap:** the 8 points are most likely differences in the keyword lists, which I had to write blind. I never saw individual predictions or mistakes, so I can't confirm this. The failure modes the description names (missing keywords, the wrong noun winning, long "what is the" questions falling to DESC) are kept on purpose.
- **Small additions:** I added "stand for", "how do" and "what type", which the description doesn't list.
- **Size:** about 586 keyword neurons, not about 400, and about 364k parameters against the original's 57,800. Each copied character takes 8 dimensions, which makes the model much wider.

**Housekeeping:**
- The snapshots contain the full model code but leave out the unchanged evaluation section at the bottom of the template, because I couldn't copy the file directly.
- I left an unused helper, `results/sanity_check.py`, which never ran because it needed approval.

**Final model: `interpretable_transformers_lib/KeywordDecisionList_v4.py` (84.0%).**
Reconstruction r2: visible 84.50 / hidden 78.15; 272,394 params; 36 turns, $2.15

Final program: experiments/nl-bottleneck/recon_final/trec-coarse-r2.py (snapshot trec_kwlist_v1). Tokens: 64,315 output (22,287 thinking), 78,555 cache write, 1,165,821 cache read, 52 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
trec_kwlist_v1 (final)84.5272,000yes2-block hand-written keyword decision list: 20 Gaussian copy heads + ~400 AND-gate keyword neurons, '=' attends to max 300*priority-3*pos salience and copies class vote; default ENTY.
trec_kwlist_v284.5324,000yes2-block hand-written keyword decision list: 20 Gaussian copy heads + ~400 AND-gate keyword neurons, '=' attends to max 300*priority-3*pos salience and copies class vote; default ENTY.
trec_kwlist_v3_stems84.5307,000yes2-block hand-written keyword decision list: 20 Gaussian copy heads + ~400 AND-gate keyword neurons, '=' attends to max 300*priority-3*pos salience and copies class vote; default ENTY.
trec_kwlist_v4_selftest84.5307,000yes2-block hand-written keyword decision list: 20 Gaussian copy heads + ~400 AND-gate keyword neurons, '=' attends to max 300*priority-3*pos salience and copies class vote; default ENTY.

Agent's final message:

I've built a hand-written transformer that follows the description. It scores **84.5% (169/200)**, against the original's 92.0%. All four runs gave exactly that score.

**How it works** (two blocks, no LayerNorm, no training; every weight is written directly from the rules):
- **Embedding:** each character gets a 5-bit letter code, flags for space, padding `_` and apostrophe, a start-of-question flag, its position, and a salience channel preloaded with −3 × position.
- **Block 1:** 20 heads, where head h copies the character h steps back using a Gaussian position kernel. Each MLP neuron is an exact AND gate for one pattern (a word, a stem, or a question-initial word). It outputs exactly 1 on a match and nothing otherwise. When it fires it adds 300 × priority to the salience and writes a class vote.
- **Block 2:** `=` attends to the position with the highest salience, so the highest priority wins and the earliest match breaks ties. It copies that position's vote. `=` carries a weak ENTY vote of its own, which wins only when nothing matches.
- **Priority order** is the description's: ABBR ("stands for", "abbreviat") > DESC "comes from" > initial who / where / initial when / why > definition and cause words > "how did/does/is/do/can" > other "how" (NUM) > "what is X" under 17 characters (DESC) > topical nouns > "called" / "what kind" > "what are the" > initial "what is".

**Runs:**
1. **v1:** basic lexicon. 84.5%.
2. **v2:** about 150 more nouns. 84.5%.
3. **v3:** nouns of 5+ letters became stems, so plurals and derived forms match. 84.5%.
4. **v4:** same as v3, plus a diagnostic that prints predictions for 13 questions I wrote myself. All 13 were classified as intended, so the circuit is working. 84.5%.

The unchanged score is a coincidence of the lexicon changes, not a broken circuit. I didn't inspect the dataset examples or errors, so I can't say which questions account for the remaining 7.5-point gap. The likeliest cause is that the original's keyword list covers more than mine; I didn't try to close the gap by tuning. I used 4 of the 6 allowed runs.

Two differences from the original:
- **Size:** by my estimate the model has about 300k parameters, against the original's 57,800, because of the wide residual layout I chose. The run printed accuracy but not the parameter count, so this is unconfirmed.
- **Extra keywords:** a few, such as "stand for" alongside "stands for", come from my own knowledge rather than the description.

Final model: `interpretable_transformers_lib/trec_kwlist_v4_selftest.py`.
Control (no description) r1: visible 82.50 / hidden 80.30; 206,914,601 params; 24 turns, $1.73

Final program: experiments/nl-bottleneck/recon_control_final/trec-coarse-r1.py (snapshot LexiconVotes_v2). Tokens: 48,869 output (19,049 thinking), 75,756 cache write, 713,903 cache read, 32 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
LexiconVotes_v181.569,900,000yesLook-back attention (40 chars) + MLP phrase detectors from a hand-written TREC lexicon casting class votes, pooled by uniform attention; argmax of votes + prior.
LexiconVotes_v2 (final)82.5207,000,000yesv1 + expanded hand-written lexicon (auto plurals, more head nouns/prefixes, extra DESC/ENTY/HUM/NUM cue phrases).

Agent's final message:

I'm going with **LexiconVotes_v2**. It scored **82.5% (165/200)** on the fixed evaluation set, up from 81.5% for v1. I used 2 of the 6 allowed runs.

**How the model works.** It has two layers and every weight is set in closed form, with nothing trained:
1. **Character window.** Forty attention heads each look back a fixed number of characters (head *j* attends to position *i−j*). Together they give every position a view of the last 40 characters.
2. **Phrase detectors.** The MLP has one ReLU unit per hand-written pattern. A unit outputs exactly 1 when its pattern ends at that position, and then adds fixed votes to the six classes. Some patterns are tied to the start of the question, such as "who…", "why…", "how…" and "what is the…". Others can match anywhere, such as "how many", "stand for", "meaning" and "population". There are also "head noun" patterns: a prefix like "what", "which" or "the largest" followed by a noun from a class list, for example "what country" → LOC or "the first president" → HUM.
3. **Pooling.** A uniform attention head averages the votes over the whole prompt into the final `=` position. The output head then picks the class with the highest total, using small preset priors as tie-breakers. If nothing else matches, the answer falls back to ENTY.

All word lists and weights come from my own knowledge of the TREC question categories. I never looked at the dataset or at which questions it got wrong.

**Why I stopped after two runs.** The only way to improve further would have been to read the misclassified questions in the scored set and add rules for them. That would be fitting to the evaluation examples, which the rules forbid. Without that feedback, more lexicon changes would just be guesses.

**Checks I couldn't run.** I wanted to test a few questions I made up before the first scored run, but those ad-hoc commands needed approval, so I didn't run them. The scored runs are the only tests.

Final model: `interpretable_transformers_lib/LexiconVotes_v2.py`

word-reversal-3x3

Original: run word-reversal-3x3-opus55-s1, program E30_HardAttention (experiments/nl-bottleneck/originals/word-reversal-3x3.py), visible 100.0%, hidden 100.00%, 3 params.

The original agent's own description of this program: E27 with softmax replaced by argmax, so no leakage and no beta. Only the 3 query coefficients remain, at the cost of a non-standard attention rule.

Description given to the reconstructors (188 words):

This model has no MLP: one causal attention read acts as a pointer, followed by a nearest-value lookup.

Attention: one layer, one head, using hard argmax instead of softmax. Each position's key is its 1-based position. The query at each generation step is the slot to copy. With i = position − 11 (the "=" is position 11) and word = floor(i/4), the query is 9 + i − 8·word. The score is negative squared distance between query and key, and the single best earlier position wins. So the answer copies prompt indices 8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2: word 3, separator, word 2, separator, word 1.

Value and readout: the value is the token id as one scalar, capped at the separator's id, so "=" reads as "_". That lets the first separator be copied from the "=" slot. Logits are the negative squared difference between the read value and each vocabulary id. An exact match scores 0; ties go to the lower id, "_".

Errors: selection is exact with no softmax leakage, so accuracy should be 100%, even with repeated letters.
Reconstruction r1: visible 100.00 / hidden 100.00 / extra 100.00; 3 params; 15 turns, $0.48

Final program: experiments/nl-bottleneck/recon_final/word-reversal-3x3-r1.py (snapshot HardArgmaxPointer3p). Tokens: 10,483 output (791 thinking), 28,629 cache write, 216,782 cache read, 22 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
HardArgmaxPointer3p (final)100.03yes1-layer 1-head hard-argmax attention, key=1-based pos, query=9+i-8*floor(i/4), score=-(q-k)^2; value=min(id,'_'); logits=-(v-id)^2. 3 params, no MLP.

Agent's final message:

My rebuild scored 100% (200/200), matching the original. It has 3 parameters and I needed only 1 of the 6 allowed runs.

What I built in `interpretable_transformer.py`, following the description:
- **Attention:** one layer and one head, with no MLP. It uses hard argmax instead of softmax. Each position's key is its 1-based position. At each generation step the query is `9 + i − 8·floor(i/4)`, where `i` is the position minus 11 (the "=" is position 11). The score is the negative squared distance between query and key, only earlier positions count, and the single best one wins.
- **Reading out the value:** the value is the token id, capped at the id of "_". That makes "=" read as "_", so the first separator in the answer is copied from the "=" slot. The logits are the negative squared difference between the value read and each vocabulary id. Greedy decoding picks the lower id on a tie.
- **The 3 parameters:** `q_offset = 9`, `word_stride = 8`, and `value_cap = 26` (the id of "_"). All three come straight from the algorithm; there was no training or fitting. The answer copies prompt indices 8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2, which reverses the words.

The description says the original makes no errors, even with repeated letters, and this model made none either. There were no limitations to copy.

One thing behaves differently from what you might expect: the template's original transformer classes are still in the file, but `SimpleTransformer` now points to the new 3-parameter model. So the model the harness builds contains only those 3 parameters, and the results CSV records `3.00e+00`.

My first attempt to apply the change was a Python script run through the shell, which was declined, so I made the same change with file edits instead. The snapshot is identical to the working file.

Final model: `interpretable_transformers_lib/HardArgmaxPointer3p.py`
Reconstruction r2: visible 100.00 / hidden 100.00 / extra 100.00; 3 params; 11 turns, $0.38

Final program: experiments/nl-bottleneck/recon_final/word-reversal-3x3-r2.py (snapshot PointerHardArgmax3p). Tokens: 9,272 output (705 thinking), 21,290 cache write, 141,427 cache read, 18 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
PointerHardArgmax3p (final)100.03yesNo-MLP: one hard-argmax head, key=1-based pos, query=9+i-8*floor(i/4), score=-(q-k)^2; value=min(id,'_'), logits=-(v-id)^2. 3 params.

Agent's final message:

I rebuilt the model from the description, and on the first run it scored **100.0% (200/200)** on the fixed set with 3 parameters. That matches the original's accuracy and parameter count. I used 1 of the 6 allowed runs and didn't need more, since nothing needed fixing.

How it follows the description:
- **No MLP.** There is one causal attention head that picks a single position with hard argmax instead of softmax.
- **Keys and queries.** Each position's key is its 1-based position. The query is `9 + i − 8·floor(i/4)`, where `i = t − 11` and the `=` sits at position 11. The score is `−(q − k)²`, so the answer copies prompt positions 8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2 in that order.
- **Value.** It's `min(token_id, id('_'))`, so the `=` reads as `_`. That's how the first separator gets copied from the `=` position.
- **Readout.** The logits are `−(read − vocab_id)²`. An exact match scores 0, and a tie goes to the lower id, because argmax picks the first maximum.
- **The 3 parameters.** They're set directly in `write_weights`: `q_offset = 9`, `q_stride = 8` and `v_cap = 26` (the id of `_`).
- **Errors.** The description predicts none, because selection is exact with no softmax leakage. Accordingly, I didn't add any error behaviour.

**Final model: `interpretable_transformers_lib/PointerHardArgmax3p.py`.**
Control (no description) r1: visible 100.00 / hidden 100.00 / extra 100.00; 45,892 params; 16 turns, $0.45

Final program: experiments/nl-bottleneck/recon_control_final/word-reversal-3x3-r1.py (snapshot PosCopy1L). Tokens: 11,036 output (1,700 thinking), 24,867 cache write, 160,809 cache read, 20 uncached input.

attemptvisible acc. (%)paramssnapshot savedagent's description of the attempt
PosCopy1L (final)100.045,900yes1-layer 1-head positional copy: query at answer step j attends to fixed source position SRC[j]; value carries token one-hot, unembedding reads it.

Agent's final message:

The model gets 100% accuracy (200/200) on `word-reversal-3x3`. That was the first run, so I used 1 of the 6 experiments and didn't need the rest.

**How it works:** it's a single layer with one attention head, and every weight is set by hand in `write_weights`.
- **Fixed positions:** prompts always have the same layout, so answer character *j* (predicted at position 11+*j*) is always the prompt character at position `[8,9,10, 3, 4,5,6, 7, 0,1,2][j]`. The two underscores come from prompt positions 3 and 7.
- **Embeddings:** model width is 96. The first 28 dimensions hold the token as a one-hot, the next 23 hold the position as a one-hot, and the last 28 are left empty for the copied token.
- **First layer norm:** every input has exactly two 1s, so its mean and standard deviation are known constants. I set the norm's bias so it only rescales the input and doesn't shift it.
- **Attention:** the query at answer step *j* points at its source position with a large weight (score margin about 200), and each key is just its own position. So each step attends almost entirely to the right prompt position. The attention value carries that position's token and writes it into the empty dimensions.
- **Output:** the MLP is all zeros. The output layer reads the copied-token dimensions directly, and the final layer norm doesn't change which character scores highest.

The first snapshot write was cut off at the end. I restored the missing lines and checked that it is identical to `interpretable_transformer.py`.

**Final model: `interpretable_transformers_lib/PosCopy1L.py` (100% accuracy).**

4. Dataset tasks: example disagreements

sentiment-sst2

Hidden set, 2000 examples. Counts: original wrong, reconstruction 1 right 93, original right, reconstruction 1 wrong 105, both wrong, different answers 0, both wrong, same wrong answer 334, both right (random) 1468. Up to 8 random examples per group (seed 0); padding underscores removed; pair tasks show the two sentences separated by |. Columns: true label, original, reconstruction 1, reconstruction 2, control.

original wrong, reconstruction 1 right (93)
inputlabeloriginalrecon 1recon 2control
a ham fisted sermon negativepositivenegativenegativepositive
a waste of fearless purity in the acting craft negativepositivenegativenegativenegative
second chance to find love in the most unlikely place positivenegativepositivepositivepositive
that rare movie that works on any number of levels positivenegativepositivenegativepositive
unsettling memorable cinematic experience positivenegativepositivepositivepositive
the perkiness of witherspoon who is always a joy to watch even when her materialpositivenegativepositivepositivepositive
the kind of movie that comes along only occasionally one so unconventional gutsypositivenegativepositivenegativenegative
using a stock plot about a boy injects just enough freshness into the proceedingpositivenegativepositivepositivepositive
original right, reconstruction 1 wrong (105)
inputlabeloriginalrecon 1recon 2control
the lack of naturalness makes everything seem self consciously poetic and forcednegativenegativepositivenegativenegative
the kind of soft core twaddle negativenegativepositivenegativepositive
repressed negativenegativepositivepositivepositive
the movie knew what to do with him positivepositivenegativenegativepositive
savor whenever the film 's lamer instincts are in the saddle negativenegativepositivenegativepositive
woody allen seems to have bitterly forsaken negativenegativepositivepositivepositive
than like a bottom feeder sequel in the escape from new york series negativenegativepositivepositivenegative
how many times they can work the words radical '' or suck '' into a sentence negativenegativepositivenegativepositive
both wrong, different answers (0)
inputlabeloriginalrecon 1recon 2control
both wrong, same wrong answer (334)
inputlabeloriginalrecon 1recon 2control
hard to say who might enjoy this negativepositivepositivepositivepositive
to face frightening late fees negativepositivepositivepositivenegative
does not have positivenegativenegativenegativenegative
grisly negativepositivepositivenegativepositive
it does n't disappoint positivenegativenegativenegativepositive
a real movie positivenegativenegativenegativepositive
you 've seen it all before negativepositivepositivepositivepositive
quarter the fun of toy story negativepositivepositivepositivepositive
both right (random) (1468)
inputlabeloriginalrecon 1recon 2control
the film at minutes is actually quite entertaining positivepositivepositivepositivepositive
's also a dare i say it twice delightfully charming and totally american i mightpositivepositivepositivepositivepositive
scuttled by a plot that 's just too boring and obvious negativenegativenegativenegativenegative
terrific special effects positivepositivepositivepositivepositive
ultimately satisfying positivepositivepositivepositivepositive
an odd show pregnant with moods stillborn except as a harsh conceptual exercise_negativenegativenegativepositivepositive
the film is visually dazzling the depicted events dramatic funny and poignant positivepositivepositivepositivepositive
actors in bad bear suits negativenegativenegativenegativenegative

paraphrase-mrpc

Hidden set, 1668 examples. Counts: original wrong, reconstruction 1 right 0, original right, reconstruction 1 wrong 0, both wrong, different answers 0, both wrong, same wrong answer 518, both right (random) 1150. Up to 8 random examples per group (seed 0); padding underscores removed; pair tasks show the two sentences separated by |. Columns: true label, original, reconstruction 1, reconstruction 2, control.

original wrong, reconstruction 1 right (0)
inputlabeloriginalrecon 1recon 2control
original right, reconstruction 1 wrong (0)
inputlabeloriginalrecon 1recon 2control
both wrong, different answers (0)
inputlabeloriginalrecon 1recon 2control
both wrong, same wrong answer (518)
inputlabeloriginalrecon 1recon 2control
at least of the affected flights were scheduled out of atlan|at least flights out of atlanta were listed as canceled not paraphraseparaphraseparaphraseparaphraseparaphrase
last month the washington post co introduced express a tablo|last month the washington post also launched a free tabloid not paraphraseparaphraseparaphraseparaphraseparaphrase
her birthday april was declared a public holiday and she att|when his grandmother then princess elizabeth turned in the dparaphrasenot paraphrasenot paraphrasenot paraphraseparaphrase
but jt was careful to clarify that it was not certain about |however we are not certain about the outcome of the discussinot paraphraseparaphraseparaphraseparaphraseparaphrase
he plans to visit an aids clinic in uganda and meet with inf|from there he plans to visit south africa and make stops in not paraphraseparaphraseparaphraseparaphraseparaphrase
now infogrames has decided to change its name to atari |infogrames is to change its name to atari when nasdaq opens not paraphraseparaphraseparaphraseparaphraseparaphrase
deasean and walker a resident of nearby barbey street were r|both shooting victims were rushed to brookdale university hoparaphrasenot paraphrasenot paraphrasenot paraphraseparaphrase
he caused a stir by misnaming the mayor of miami manny diaz_|he greeted the mayor of miami manny diaz as alex not paraphraseparaphraseparaphraseparaphraseparaphrase
both right (random) (1150)
inputlabeloriginalrecon 1recon 2control
we 're going to defend these charges in court said ira lee s|rob furst intends to defend the charges in court said ira leparaphraseparaphraseparaphraseparaphraseparaphrase
romanian port authorities said two ships that sank during wo|port authorities in romania said two ships that sank during paraphraseparaphraseparaphraseparaphraseparaphrase
you have to dig deep and come up with the goods against guys|you have to dig deep and come up with the goods against guysparaphraseparaphraseparaphraseparaphraseparaphrase
u s officials say hussein loyalists do not require high tech|u s officials say operatives loyal to the ousted saddam goveparaphraseparaphraseparaphraseparaphraseparaphrase
paul themba nyathi an mdc spokesman said the avenues clinic |mdc spokesperson paul themba nyathi said the avenues clinic paraphraseparaphraseparaphraseparaphraseparaphrase
three no votes would kill it for now |it would take three votes to kill the acc 's expansion paraphraseparaphraseparaphraseparaphraseparaphrase
microsoft acquired virtual pc when it bought the assets of c|microsoft acquired virtual pc from its developer connectix eparaphraseparaphraseparaphraseparaphraseparaphrase
he also had management jobs at three space centers before be|parsons held engineering management positions at three spaceparaphraseparaphraseparaphraseparaphraseparaphrase

nli-snli

Hidden set, 2000 examples. Counts: original wrong, reconstruction 1 right 86, original right, reconstruction 1 wrong 161, both wrong, different answers 50, both wrong, same wrong answer 706, both right (random) 997. Up to 8 random examples per group (seed 0); padding underscores removed; pair tasks show the two sentences separated by |. Columns: true label, original, reconstruction 1, reconstruction 2, control.

original wrong, reconstruction 1 right (86)
inputlabeloriginalrecon 1recon 2control
two women are wearing sunglasses and eating food |two women are flying contradictionentailmentcontradictioncontradictionentailment
a tall wrought iron fence casts horizontal shadows in the su|there is a woman contradictionentailmentcontradictioncontradictionentailment
a man in khaki walking down a sidewalk |a man is walking outdoors neutralentailmentneutralneutralentailment
several people in white clothes and red handkerchiefs and on|the tourists are looking at a fair contradictionneutralcontradictioncontradictioncontradiction
the young boy in the red shirt is kicking a soccer ball |the young boy is kicking a soccer ball outdoors neutralentailmentneutralneutralentailment
two modern buildings made of glass and stone accent the sett|some other buildings are made of concrete neutralentailmentneutralneutralentailment
a woman displays her excitement by jumping in the air and ra|a woman is jumping in the air after she has finished climbinneutralcontradictionneutralneutralneutral
a man and a woman walk down the sidewalk with a stroller and|the family are walking to the beach neutralcontradictionneutralneutralneutral
original right, reconstruction 1 wrong (161)
inputlabeloriginalrecon 1recon 2control
three men dressed in black outside camping and hiking |there are men outdoors entailmententailmentcontradictioncontradictionentailment
a woman and a child are walking past a tree and a building |two people run from an attacker contradictioncontradictionentailmententailmentneutral
woman giving a haircut to a black haired man |there is a woman cutting hair entailmententailmentneutralneutralentailment
a man in jacket hat and hood facing away from the camera hol|a woman waves at the camera contradictioncontradictionentailmententailmentcontradiction
a young boy in a white and red polo shirt is playing in the |the boy is outdoors entailmententailmentcontradictioncontradictionentailment
a man in a black t shirt is looking at a menu |the shirt is blue contradictioncontradictionentailmententailmentcontradiction
a lady in blue jeans and a brown jacket is walking down a ci|the jeans are black contradictioncontradictionentailmententailmentcontradiction
a group of people gathered around a round table working on a|a group of people speculating the correct dimensions of a prneutralneutralentailmententailmentneutral
both wrong, different answers (50)
inputlabeloriginalrecon 1recon 2control
a woman in blue is reading |a woman in blue is reading alone entailmentneutralcontradictioncontradictioncontradiction
a scene with the back end of a red vehicle with a yellow lic|a bunch of people and cars are seen from the inside of a helcontradictionneutralentailmententailmentcontradiction
four children at a skate park one in rollerblades and three |the children are riding around at the skate park entailmentcontradictionneutralneutralentailment
a white haired bearded man stands leans against a low wall n|the man is selling something on clearance neutralentailmentcontradictioncontradictionneutral
children and parents raising their hands in a library |a room full of people have a question neutralcontradictionentailmententailmentneutral
a person wearing a hat cleaning a statute |animals are wiping a statue contradictionentailmentneutralneutralcontradiction
people hugging at a wedding |people fighting in the streets contradictionneutralentailmententailmentneutral
a large crowd of people are standing in a line and on a balc|a large crowd is lining a steet with a unicycle lying in theentailmentcontradictionneutralneutralcontradiction
both wrong, same wrong answer (706)
inputlabeloriginalrecon 1recon 2control
man in black takes a nap on a bench in front of a sign readi|the man in black made a sign neutralentailmententailmententailmententailment
a boy leans over while a band plays in the background |a boy leans and a band plays neutralentailmententailmententailmententailment
a surfer is doing an airborne stunt whilst riding waves |the person is in the pacific ocean neutralcontradictioncontradictioncontradictionneutral
construction workers are gathered at a construction site alo|construction workers are gathered at a field contradictionentailmententailmententailmententailment
lots of people sitting along a low wall in front of grass wi|the people are having a picnic neutralentailmententailmententailmentneutral
two boys crossing a street with a red ball |the boys are are doing ballet contradictionneutralneutralneutralentailment
a female gymnast performing a jump |a gymnast is receiving a trophy contradictionneutralneutralneutralneutral
a worker is walking next to a train line |the worker is skipping across the grass contradictionneutralneutralneutralneutral
both right (random) (997)
inputlabeloriginalrecon 1recon 2control
group of men and women riding camels |people riding camels entailmententailmententailmententailmententailment
a photographer is kneeling in front of a railing that is pro|the photographer is getting a good angle to take a picture neutralneutralneutralneutralneutral
three older women one in a pale striped jacket another in a |three women are sitting at a table entailmententailmententailmententailmentneutral
a little blond boy in a blue winter coat is walking in the f|a little boy is in the forest entailmententailmententailmententailmententailment
a young stylishly dressed woman is standing on the sidewalk |the woman is standing outdoors entailmententailmententailmententailmententailment
a boy stands in the pool at the bottom of a blue water slide|there is a boy in the pool entailmententailmententailmententailmententailment
elderly native sitting in lawn chair in a hut |the native watches the young villagers play outside his hut_neutralneutralneutralneutralcontradiction
a indian lady sitting making a clothing piece with a young c|a woman is making clothing for the upcoming winter neutralneutralneutralneutralneutral

trec-coarse

Hidden set, 2000 examples. Counts: original wrong, reconstruction 1 right 77, original right, reconstruction 1 wrong 173, both wrong, different answers 28, both wrong, same wrong answer 250, both right (random) 1472. Up to 8 random examples per group (seed 0); padding underscores removed; pair tasks show the two sentences separated by |. Columns: true label, original, reconstruction 1, reconstruction 2, control.

original wrong, reconstruction 1 right (77)
inputlabeloriginalrecon 1recon 2control
what are the names of all the seas in the world and what ocean do they drain intLOCHUMLOCLOCLOC
what is the chemical composition of a barbie ENTYDESCENTYENTYENTY
from what cause does tuberculosis stem DESCENTYDESCDESCENTY
what is the fastest computer ENTYDESCENTYENTYENTY
what do ladybugs eat ENTYDESCENTYENTYENTY
what was the name of the titanic 's captain HUMENTYHUMHUMHUM
what is the brand name of the chemical daminozide ENTYDESCENTYENTYENTY
what is the most common cancer ENTYDESCENTYENTYENTY
original right, reconstruction 1 wrong (173)
inputlabeloriginalrecon 1recon 2control
what is the bernoulli principle DESCDESCENTYDESCENTY
what radio station did paul harvey work for HUMHUMENTYENTYENTY
what do you need to do to marry someone in jail DESCDESCENTYENTYENTY
where did the name root beer come from DESCDESCLOCLOCLOC
what nationality is pope john paul ii LOCLOCHUMHUMHUM
where did the marriage ceremony come from DESCDESCLOCLOCLOC
stuart hamblen is considered to be the first singing cowboy of which medium ENTYENTYHUMENTYENTY
what is dr ruth 's last name HUMHUMDESCDESCDESC
both wrong, different answers (28)
inputlabeloriginalrecon 1recon 2control
what can i do to get into an ivy league school DESCHUMENTYENTYENTY
what is the chemical reactivity of helium NUMDESCENTYENTYENTY
what happens when a body part falls asleep DESCENTYLOCENTYDESC
what is the us federal government website for standard industrial classificationLOCHUMENTYDESCENTY
what european race sees the winner of its mountain stage awarded a jersey with lENTYLOCHUMHUMLOC
what is ram in the computer ABBRDESCENTYENTYDESC
what 's distinctive about a palmiped 's feet DESCENTYNUMNUMENTY
what general direction does the journey in around the world in days proceed in LOCHUMNUMHUMHUM
both wrong, same wrong answer (250)
inputlabeloriginalrecon 1recon 2control
what is sap ABBRDESCDESCDESCDESC
what were the first words spoken on a film sound track DESCENTYENTYENTYENTY
what are some fun things to do in cozumel mexico for teenagers DESCENTYENTYENTYENTY
what six foot temperance advocate wielded her hatchet on saloons HUMENTYENTYENTYENTY
name a female figure skater HUMENTYENTYENTYENTY
which of the following tv newsmen was a rhodes scholar HUMENTYENTYENTYENTY
what is april 's gemstone ENTYDESCDESCDESCDESC
name the four horsemen of the apocalypse HUMENTYENTYENTYENTY
both right (random) (1472)
inputlabeloriginalrecon 1recon 2control
what 's the sequel to alice 's adventures in wonderland ENTYENTYENTYENTYENTY
what kind of sports did they play in the years the english civil war time ENTYENTYENTYENTYENTY
what shakespeare play opens with the line now is the winter of our discontent ''ENTYENTYENTYENTYENTY
what does the name jenna mean DESCDESCDESCDESCDESC
what color poker chip is usually assigned the lowest value ENTYENTYENTYENTYENTY
why do they call it a funny bone '' DESCDESCDESCDESCDESC
what u s state has the lowest highest elevation at feet LOCLOCLOCLOCLOC
what city was bobby kennedy assassinated in LOCLOCLOCLOCLOC

5. Token usage per run

From the result record of each session. Cache read tokens are the repeated context re-read on each turn and dominate the count. Summarizer calls (17 tasks, $5.06 total) did not record tokens.

runturnsoutput tok.of which thinkingcache writecache readuncached inputcostseconds
recon_control/addition-five-digits-r11516,8935,64734,213206,30120$0.65149
recon_control/boolean-circuit-5-bits-r11518,5691,77328,930259,18426$0.65155
recon_control/date-normalization-r12542,20418,43461,984497,79832$1.44377
recon_control/decimal-to-binary-8bit-r11413,0042,15839,790277,87120$0.63112
recon_control/digit-counting-10-r11615,3992,83034,756268,96926$0.64136
recon_control/edit-distance-8-r11820,0653,76041,624288,65526$0.79169
recon_control/gcd-three-digits-r11019,0803,42532,522200,93720$0.68156
recon_control/linear-interpolation-two-points-r11816,4663,42233,445284,41726$0.65147
recon_control/multiplication-five-digits-r11325,3318,64834,515282,64624$0.84225
recon_control/nli-snli-r168128,68728,975174,0092,464,19264$4.461066
recon_control/paraphrase-mrpc-r12734,9129,96358,271692,34838$1.30321
recon_control/parity-upto10-bits-r1118,9601,08622,124128,72218$0.3883
recon_control/sentiment-sst2-r14166,05914,25989,2691,240,82358$2.28586
recon_control/smallest-prime-factor-r11616,6514,57843,133295,55820$0.74150
recon_control/sort-five-digits-r11611,1411,80326,893192,62422$0.4899
recon_control/trec-coarse-r12448,86919,04975,756713,90332$1.73487
recon_control/word-reversal-3x3-r11611,0361,70024,867160,80920$0.4598
recon/addition-five-digits-r11413,5722,56324,545228,61324$0.51124
recon/addition-five-digits-r21112,4142,50920,961172,49420$0.45112
recon/boolean-circuit-5-bits-r11116,0654,72626,293165,10718$0.56143
recon/boolean-circuit-5-bits-r21922,6636,88334,214395,96332$0.81210
recon/date-normalization-r11143,88228,33661,617355,55422$1.44411
recon/date-normalization-r22663,76544,93079,630995,61438$2.11620
recon/decimal-to-binary-8bit-r11317,5886,37928,792228,05322$0.63159
recon/decimal-to-binary-8bit-r21215,7613,58429,491228,37822$0.60137
recon/digit-counting-10-r11520,2446,92739,284310,24026$0.78180
recon/digit-counting-10-r21519,7786,60735,604248,48722$0.73179
recon/edit-distance-8-r11873,45959,05490,983728,01730$2.34723
recon/edit-distance-8-r21245,24529,47753,395385,95824$1.41438
recon/gcd-three-digits-r11925,33910,41437,310335,04830$0.87233
recon/gcd-three-digits-r21421,5116,01030,223232,79124$0.72185
recon/linear-interpolation-two-points-r11226,18414,22936,783224,60120$0.86248
recon/linear-interpolation-two-points-r21326,53312,12838,442337,75624$0.91250
recon/multiplication-five-digits-r11229,50114,70440,008248,83322$0.96279
recon/multiplication-five-digits-r21432,72517,76940,594325,13924$1.04315
recon/nli-snli-r12759,65223,26074,691997,90844$1.99541
recon/nli-snli-r21438,95817,50249,664422,05426$1.26358
recon/paraphrase-mrpc-r11421,8684,60543,601350,55426$0.86192
recon/paraphrase-mrpc-r21521,0226,41139,011238,71522$0.78192
recon/parity-upto10-bits-r11410,57372527,275166,54920$0.4690
recon/parity-upto10-bits-r21310,00670421,025197,54022$0.4187
recon/sentiment-sst2-r13158,66721,71881,085771,46636$1.98580
recon/sentiment-sst2-r22457,12916,43285,997754,37136$1.98550
recon/smallest-prime-factor-r11717,1883,07935,122311,30028$0.69153
recon/smallest-prime-factor-r21217,2163,63628,133202,89720$0.61145
recon/sort-five-digits-r11112,8961,73826,531150,10718$0.50110
recon/sort-five-digits-r21111,4531,43617,478136,06518$0.4096
recon/trec-coarse-r16781,67329,399116,5352,198,95572$3.01758
recon/trec-coarse-r23664,31522,28778,5551,165,82152$2.15589
recon/word-reversal-3x3-r11510,48379128,629216,78222$0.4895
recon/word-reversal-3x3-r2119,27270521,290141,42718$0.3880

6. Full comparison metrics

Definitions as in the main report. Each row compares a reconstruction or control with the original on one example set.

runkindsetnorig errorscompared errorsboth wrongexpected both wrongJaccardphiidentical outputkappasame wrong answer
addition-five-digits-r1reconvisible2000000.01.0001.000
addition-five-digits-r1reconhidden20000000.01.0001.000
addition-five-digits-r1reconextra200000000.01.0001.000
addition-five-digits-r2reconvisible2000000.01.0001.000
addition-five-digits-r2reconhidden20000000.01.0001.000
addition-five-digits-r2reconextra200000000.01.0001.000
addition-five-digits-r1controlvisible2000000.01.0001.000
addition-five-digits-r1controlhidden20000000.01.0001.000
addition-five-digits-r1controlextra200000000.01.0001.000
boolean-circuit-5-bits-r1reconvisible2000000.01.0001.000
boolean-circuit-5-bits-r1reconhidden20000000.01.0001.000
boolean-circuit-5-bits-r1reconextra200000000.01.0001.000
boolean-circuit-5-bits-r2reconvisible2000000.01.0001.000
boolean-circuit-5-bits-r2reconhidden20000000.01.0001.000
boolean-circuit-5-bits-r2reconextra200000000.01.0001.000
boolean-circuit-5-bits-r1controlvisible2000000.01.0001.000
boolean-circuit-5-bits-r1controlhidden20000000.01.0001.000
boolean-circuit-5-bits-r1controlextra200000000.01.0001.000
date-normalization-r1reconvisible2000000.01.0001.000
date-normalization-r1reconhidden20000000.01.0001.000
date-normalization-r1reconextra200000000.01.0001.000
date-normalization-r2reconvisible2000000.01.0001.000
date-normalization-r2reconhidden20000000.01.0001.000
date-normalization-r2reconextra200000000.01.0001.000
date-normalization-r1controlvisible2000000.01.0001.000
date-normalization-r1controlhidden20000000.01.0001.000
date-normalization-r1controlextra200000000.01.0001.000
decimal-to-binary-8bit-r1reconvisible2000000.01.0001.000
decimal-to-binary-8bit-r1reconhidden20000000.01.0001.000
decimal-to-binary-8bit-r1reconextra200000000.01.0001.000
decimal-to-binary-8bit-r2reconvisible2000000.01.0001.000
decimal-to-binary-8bit-r2reconhidden20000000.01.0001.000
decimal-to-binary-8bit-r2reconextra200000000.01.0001.000
decimal-to-binary-8bit-r1controlvisible2000000.01.0001.000
decimal-to-binary-8bit-r1controlhidden20000000.01.0001.000
decimal-to-binary-8bit-r1controlextra200000000.01.0001.000
digit-counting-10-r1reconvisible2000000.01.0001.000
digit-counting-10-r1reconhidden20000000.01.0001.000
digit-counting-10-r1reconextra200000000.01.0001.000
digit-counting-10-r2reconvisible2000000.01.0001.000
digit-counting-10-r2reconhidden20000000.01.0001.000
digit-counting-10-r2reconextra200000000.01.0001.000
digit-counting-10-r1controlvisible2000000.01.0001.000
digit-counting-10-r1controlhidden20000000.01.0001.000
digit-counting-10-r1controlextra200000000.01.0001.000
edit-distance-8-r1reconvisible2000000.01.0001.000
edit-distance-8-r1reconhidden20000000.01.0001.000
edit-distance-8-r1reconextra200000000.01.0001.000
edit-distance-8-r2reconvisible2000000.01.0001.000
edit-distance-8-r2reconhidden20000000.01.0001.000
edit-distance-8-r2reconextra200000000.01.0001.000
edit-distance-8-r1controlvisible2000000.01.0001.000
edit-distance-8-r1controlhidden20000000.01.0001.000
edit-distance-8-r1controlextra200000000.01.0001.000
gcd-three-digits-r1reconvisible2000000.01.0001.000
gcd-three-digits-r1reconhidden20000000.01.0001.000
gcd-three-digits-r1reconextra200000000.01.0001.000
gcd-three-digits-r2reconvisible2000000.01.0001.000
gcd-three-digits-r2reconhidden20000000.01.0001.000
gcd-three-digits-r2reconextra200000000.01.0001.000
gcd-three-digits-r1controlvisible2000000.01.0001.000
gcd-three-digits-r1controlhidden20000000.01.0001.000
gcd-three-digits-r1controlextra200000000.01.0001.000
linear-interpolation-two-points-r1reconvisible2000000.01.0001.000
linear-interpolation-two-points-r1reconhidden20000000.01.0001.000
linear-interpolation-two-points-r1reconextra200000000.01.0001.000
linear-interpolation-two-points-r2reconvisible2000000.01.0001.000
linear-interpolation-two-points-r2reconhidden20000000.01.0001.000
linear-interpolation-two-points-r2reconextra200000000.01.0001.000
linear-interpolation-two-points-r1controlvisible2000000.01.0001.000
linear-interpolation-two-points-r1controlhidden20000000.01.0001.000
linear-interpolation-two-points-r1controlextra200000000.01.0001.000
multiplication-five-digits-r1reconvisible2000000.01.0001.000
multiplication-five-digits-r1reconhidden20000000.01.0001.000
multiplication-five-digits-r1reconextra200000000.01.0001.000
multiplication-five-digits-r2reconvisible2000000.01.0001.000
multiplication-five-digits-r2reconhidden20000000.01.0001.000
multiplication-five-digits-r2reconextra200000000.01.0001.000
multiplication-five-digits-r1controlvisible2000000.01.0001.000
multiplication-five-digits-r1controlhidden20000000.01.0001.000
multiplication-five-digits-r1controlextra200000000.01.0001.000
nli-snli-r1reconvisible20073826929.90.8020.8250.8900.8340.928
nli-snli-r1reconhidden2000842917756386.10.7540.7520.8520.7740.934
nli-snli-r2reconvisible20073856931.00.7750.7980.8700.8040.913
nli-snli-r2reconhidden2000842922752388.20.7430.7390.8440.7620.930
nli-snli-r1controlvisible20073865531.40.5290.4950.7000.5440.800
nli-snli-r1controlhidden2000842807553339.70.5050.4400.6700.4980.790
paraphrase-mrpc-r1reconvisible20061616118.61.0001.0001.0001.0001.000
paraphrase-mrpc-r1reconhidden1668518518518160.91.0001.0001.0001.0001.000
paraphrase-mrpc-r2reconvisible20061616118.61.0001.0001.0001.0001.000
paraphrase-mrpc-r2reconhidden1668518516516160.20.9960.9970.9990.9931.000
paraphrase-mrpc-r1controlvisible20061645419.50.7610.8030.9150.0001.000
paraphrase-mrpc-r1controlhidden1668518520437161.50.7270.7710.9020.0001.000
parity-upto10-bits-r1reconvisible2000000.01.0001.000
parity-upto10-bits-r1reconhidden20000000.01.0001.000
parity-upto10-bits-r1reconextra200000000.01.0001.000
parity-upto10-bits-r2reconvisible2000000.01.0001.000
parity-upto10-bits-r2reconhidden20000000.01.0001.000
parity-upto10-bits-r2reconextra200000000.01.0001.000
parity-upto10-bits-r1controlvisible2000000.01.0001.000
parity-upto10-bits-r1controlhidden20000000.01.0001.000
parity-upto10-bits-r1controlextra200000000.01.0001.000
sentiment-sst2-r1reconvisible2004044298.80.5270.6100.8700.7131.000
sentiment-sst2-r1reconhidden200042743933493.70.6280.7080.9010.7871.000
sentiment-sst2-r2reconvisible2004040328.00.6670.7500.9200.8271.000
sentiment-sst2-r2reconhidden200042745632797.40.5880.6680.8850.7531.000
sentiment-sst2-r1controlvisible2004047269.40.4260.4890.8250.6051.000
sentiment-sst2-r1controlhidden2000427504286107.60.4430.5010.8210.5991.000
smallest-prime-factor-r1reconvisible2000000.01.0001.000
smallest-prime-factor-r1reconhidden20000000.01.0001.000
smallest-prime-factor-r1reconextra200000000.01.0001.000
smallest-prime-factor-r2reconvisible2000000.01.0001.000
smallest-prime-factor-r2reconhidden20000000.01.0001.000
smallest-prime-factor-r2reconextra200000000.01.0001.000
smallest-prime-factor-r1controlvisible2000000.01.0001.000
smallest-prime-factor-r1controlhidden20000000.01.0001.000
smallest-prime-factor-r1controlextra200000000.01.0001.000
sort-five-digits-r1reconvisible2000000.01.0001.000
sort-five-digits-r1reconhidden20000000.01.0001.000
sort-five-digits-r1reconextra200000000.01.0001.000
sort-five-digits-r2reconvisible2000000.01.0001.000
sort-five-digits-r2reconhidden20000000.01.0001.000
sort-five-digits-r2reconextra200000000.01.0001.000
sort-five-digits-r1controlvisible2000000.01.0001.000
sort-five-digits-r1controlhidden20000000.01.0001.000
sort-five-digits-r1controlextra200000000.01.0001.000
trec-coarse-r1reconvisible2001632112.60.2970.4240.8550.8190.727
trec-coarse-r1reconhidden200035545127880.10.5270.6200.8610.8260.899
trec-coarse-r2reconvisible2001631142.50.4240.5870.9050.8811.000
trec-coarse-r2reconhidden200035543729677.60.5970.6920.8890.8610.926
trec-coarse-r1controlvisible2001635112.80.2750.3980.8400.7990.727
trec-coarse-r1controlhidden200035539423069.90.4430.5270.8380.7960.843
word-reversal-3x3-r1reconvisible2000000.01.0001.000
word-reversal-3x3-r1reconhidden20000000.01.0001.000
word-reversal-3x3-r1reconextra200000000.01.0001.000
word-reversal-3x3-r2reconvisible2000000.01.0001.000
word-reversal-3x3-r2reconhidden20000000.01.0001.000
word-reversal-3x3-r2reconextra200000000.01.0001.000
word-reversal-3x3-r1controlvisible2000000.01.0001.000
word-reversal-3x3-r1controlhidden20000000.01.0001.000
word-reversal-3x3-r1controlextra200000000.01.0001.000