Day 2 — the practical session
Matias Zaldarriaga
Ondas Gravitacionales e Investigación Asistida por IA
Use ← and →, or the dots at the top. p switches between reading size and presenting size.
Before I say anything
Find GW150914 in the H1 strain. Build the template yourself. Report the peak SNR and the GPS time at which it occurs.
Then prove your normalisation: run the same filter over signal-free stretches and show that the mean of ρ² is 2.
That is the task, not the prompt. Type your own. Everyone reports the same two numbers, so we can compare directly — and a group whose normalisation is wrong reports a different SNR, which is what the second sentence is for.
So you know whether you are lost
Not an answer key. A way to tell “my pipeline is broken” from “this is what the answer looks like”.
| what you filter with | peak SNR in H1 |
|---|---|
| a real waveform model — IMRPhenomD, or anything like it | about 19.5 [src:h1_peak_snr] |
| a hand-written 0PN template | about 18.5 [src:template_rows] |
| full 3.5PN | about 16 [src:template_rows] |
At GPS 1126259462.4, in every case. The chirp mass lands near 31 solar masses in the detector frame [src:mchirp_true] — redshifted, not corrected to the source.
The three rows are not three grades of answer. They are three different templates, and the SNR is telling you how well each one matches. A number far from all three is a bug, not a result.
On the card on your table
There is one question I will ask every group this afternoon, and it is the same question three times.
1. Is your whitened data's variance 1 per sample? 2. Run your filter on signal-free noise. Is the mean of rho^2 equal to 2? 3. Inject a signal of known amplitude. Do you get the SNR back?
A group that can answer all three has a working pipeline, whether or not it found the event. A group reporting an SNR of 20 that can answer none of them has found nothing.
Every one of these caught a real bug in my own version of this exercise, and the third caught the worst one. Further down, the second one catches a real bug in a run that had already written its report.
What I ran, before you got here
The same task you have just started, given to eight combinations of model and reasoning effort. Every cell got this string, byte for byte. Rewording it for one cell would have ended the comparison, so it was settled before the first cell ran and no cell has seen a different one.
Two things in it are worth noticing now, because they are columns further down. The waveform generator is off the shelf and the filter is not, so every cell has the same easy half and the same hard half. And it asks for the histogram of ρ² and its mean without saying the mean should be 2 — whether a run knows that is one of the things being measured.
| run | model | effort | format asked for | tokens in / out | wall | when | receipt |
|---|---|---|---|---|---|---|---|
| Claude Opus 5 · xhigh | claude-opus-5 | xhigh | self-contained .html + results.json | 19,475,555 / 169,476 | 58 m 45 s | 2026-08-12 | run.log |
| Claude Opus 5 · low | claude-opus-5 | low | self-contained .html + results.json | 2,153,579 / 38,928 | 12 m 03 s | 2026-08-12 | — |
| Claude Sonnet 5 · xhigh | claude-sonnet-5 | xhigh | self-contained .html + results.json | 14,426,780 / 143,090 | 31 m 04 s | 2026-08-12 | — |
| Claude Haiku 4.5 · xhigh | claude-haiku-4-5-20251001 | xhigh | self-contained .html + results.json | 690,644 / 17,663 | 3 m 18 s | 2026-08-12 | — |
| Codex gpt-5.6-sol · xhigh | gpt-5.6-sol | xhigh | self-contained .html + results.json | 3,052,987 / 49,220 | 20 m 34 s | 2026-08-12 | — |
| Codex gpt-5.6-sol · low | gpt-5.6-sol | low | self-contained .html + results.json | 364,153 / 8,904 | 4 m 34 s | 2026-08-12 | — |
| Codex gpt-5.5 · xhigh | gpt-5.5 | xhigh | self-contained .html + results.json | 1,048,961 / 35,658 | 13 m 56 s | 2026-08-12 | — |
| Codex gpt-5.4 · xhigh | gpt-5.4 | xhigh | self-contained .html + results.json | 5,746,543 / 68,806 | 24 m 43 s | 2026-08-12 | — |
Tokens and wall time are durable, prices are not: a cost in dollars is a fact about a price list on the day it ran. Wall time is as measured on the machine named, and a laptop is contended.
[src:grid_gpt54_xhigh_tokens_in, grid_gpt54_xhigh_tokens_out, grid_gpt54_xhigh_wall_s, grid_gpt55_xhigh_tokens_in, grid_gpt55_xhigh_tokens_out, grid_gpt55_xhigh_wall_s, grid_haiku45_xhigh_cost_usd, grid_haiku45_xhigh_tokens_in, grid_haiku45_xhigh_tokens_out, grid_haiku45_xhigh_turns, grid_haiku45_xhigh_wall_s, grid_opus5_low_cost_usd, grid_opus5_low_tokens_in, grid_opus5_low_tokens_out, grid_opus5_low_turns, grid_opus5_low_wall_s, grid_opus5_xhigh_cost_usd, grid_opus5_xhigh_tokens_in, grid_opus5_xhigh_tokens_out, grid_opus5_xhigh_turns, grid_opus5_xhigh_wall_s, grid_sol56_low_tokens_in, grid_sol56_low_tokens_out, grid_sol56_low_wall_s, grid_sol56_xhigh_tokens_in, grid_sol56_xhigh_tokens_out, grid_sol56_xhigh_wall_s, grid_sonnet5_xhigh_cost_usd, grid_sonnet5_xhigh_tokens_in, grid_sonnet5_xhigh_tokens_out, grid_sonnet5_xhigh_turns, grid_sonnet5_xhigh_wall_s]
Each cell ran headless — one command, no conversation — in a fresh directory outside this repository, holding nothing but the two strain files. If a run could reach the course repository it would find my own matched filter and every answer, and that cell would be worthless.
One at a time, because wall time is a column and two agents on one laptop makes it meaningless. The two reference cells are the exception: they were launched together, before that rule existed, and their wall times are marked.
What came back — 1
Estimate the noise, divide it out, and look. No filtering, no template, no statistics: the chirp is visible in the whitened strain.
Three runs, three amplitude spectral densities, the same instrument. Six of the seven cells that produced a page produced all three of the figures the prompt names [src:grid_n_three_figures, grid_n_ran], and their noise curves agree. Estimating the noise is the half of the task that does not separate anybody — the seventh cell is at the end, and what it got wrong was not this.
Whitening is dividing each frequency by how noisy the instrument is there. The event is a fifth of a second long and it is the loudest thing in a third of an hour of data. You do not need a matched filter to see it. You need one to measure it, and to find out how often something that loud happens by accident.
What came back — 3
Slide the template along the whitened data, weight every frequency by how noisy it is, and plot what comes out.
Flat noise, and one spike. The spike is the whole of what a search does. Everything after it is deciding whether to believe it.
What came back — 4
Run the same filter where there is no signal. If the normalisation is right, ρ² is χ² with two degrees of freedom and its mean is 2.
Nobody told any of them the answer was 2 — the prompt asks for the histogram and the mean and stops there. Six of the seven runs that got this far supplied the expected value themselves, before quoting a number [src:grid_n_stated_2, grid_n_rho2_reported]. The seventh is two stages down.
1.884, from 960 samples [src:grid_sol56_xhigh_rho2_n_samples]
Consistent with 2 — and you can only know that because the count is there. Those samples are taken a second apart, so they are independent; χ² with two degrees of freedom has variance 4, so the mean carries an uncertainty of 0.065 [src:grid_sol56_xhigh_rho2_sem] and 1.884 sits 1.8 of those from 2 [src:grid_sol56_xhigh_rho2_sigmas_from_2].
None of that arithmetic is on that page. Without it a reader cannot tell this from a normalisation running 3 per cent low [src:grid_sol56_xhigh_rho2_scale]. A sample count is not an uncertainty, but it is what a reader needs to compute one.
2.012, from 7.9 million samples [src:grid_opus5_xhigh_rho2_n_samples]
Every sample of ρ(t) outside the event, not one a second. Those are correlated, so the count does not convert into an uncertainty the way the one on the left does — and this run answers that a different way, by putting synthetic Gaussian noise through the same filter, where the answer is 2 by construction, and getting 2.007 [src:grid_opus5_xhigh_rho2_gaussian_calibration].
Same statistic, two ways of showing it means something. Neither page is wrong. One of them lets you check.
Claude Opus 5 at low effort measured 2.257 [src:grid_opus5_low_rho2_mean] from 7.8 million samples [src:grid_opus5_low_rho2_n_samples] — and explained it, as a heavier-than-Gaussian tail in real detector noise. That explanation is available, and it is wrong here: the same model on the same data at higher effort returned 2.012 from the same amount of data.
A mean of ρ² that is 13 per cent high means ρ is 6 per cent high [src:grid_opus5_low_rho2_scale]. That run reported a peak SNR of 20.87 [src:grid_opus5_low_snr_h1] where the others report 19.7 [src:grid_opus5_xhigh_snr_h1] and 19.9 [src:grid_sol56_xhigh_snr_h1]. Its own check said so, on its own page, in a paragraph explaining why it did not matter.
What came back — 5
Codex gpt-5.4, at xhigh effort, reported a mean of ρ² closer to 2 than anybody else, and it is the only cell whose check could not have failed.
What it did. It derived the template normalisation correctly — every term of σ² = (h|h) is in its source — and then multiplied the result by one overall constant, which it obtained like this:
def build_scale(...):
...
return np.sqrt(2.0 / np.mean(raw_means))
The constant is defined as whatever makes the mean of ρ² come out at 2 on a stretch of noise. It came out at 255.67 [src:grid_gpt54_xhigh_rho2_fitted_scale].
What it reported. A mean of 1.959 [src:grid_gpt54_xhigh_rho2_mean], measured on held-out windows, and a sentence saying plainly what it had done: that it fixed one overall normalisation from off-source noise so held-out stretches land near 2, instead of working out the transform convention.
And the answer is right. Peak SNR 19.53 [src:grid_gpt54_xhigh_snr_h1], chirp mass 31.36 [src:grid_gpt54_xhigh_chirp_mass], the same GPS time as everybody else. The fitted constant landed within one per cent of the derived one.
Held-out validation is a real practice and that number is honest about what it measures: whether two stretches of noise look alike. What it cannot measure is the normalisation, because the normalisation was chosen to make it come out. The one check the prompt insisted on had been turned into a fit.
This is why the scoreboard has a column for how the mean was arrived at and not only for its value. Ranked by value alone, gpt-5.4 wins the column.
What came back — 6
Claude Haiku 4.5, at xhigh effort. Twenty-one cents [src:grid_haiku45_xhigh_cost_usd], three minutes [src:grid_haiku45_xhigh_wall_s], seventeen turns [src:grid_haiku45_xhigh_turns]. Against the reference cell's $16.47 [src:grid_opus5_xhigh_cost_usd] and its hour. And a report that reads like all the others.
What it reported. A peak SNR of 5,236 in H1 [src:grid_haiku45_xhigh_snr_h1], a chirp mass of 36.3 [src:grid_haiku45_xhigh_chirp_mass], and a peak 2.99 seconds into its own analysis window — a thousand seconds before the event, at the edge where its own filter wraps around.
Its summary: “Signal is 21,673 σ above noise… The signal ρ² exceeds the maximum noise ρ² by factors of 459× (H1) and 4,690× (L1). This is not a coincidence.”
What its own check said. It ran the check the prompt asked for. Its mean of ρ² came out at 11,448 [src:grid_haiku45_xhigh_rho2_mean]. That says its ρ is multiplied by about 76 [src:grid_haiku45_xhigh_rho2_scale].
It is the one cell that never names 2 [src:grid_n_stated_2, grid_n_rho2_reported]. Without the expected value, the histogram stops being a check and becomes a ruler: the signal is much bigger than the noise, so the detection is overwhelming. That comparison comes out the same whatever the normalisation is.
This is the cell the grid was run to find, and it is why the prompt does not say the mean should be 2. Every other run supplied that number itself. This one did not have it, and so nothing in its own report could tell it that a peak SNR of 5,236 sits where the answer is about 19.5.
Its page draws with eight <canvas> elements and one <script> tag pointing at a charting library on a content delivery network. It is the only cell whose page reaches outside itself [src:grid_haiku45_xhigh_page_external_refs, grid_n_offline], and on a laptop with no network it opens to a column of headings with nothing under them — including the one headed “Decisions that could reasonably have gone another way”. Ask for a self-contained file and check that it is one.
The whole grid
One row per cell. Every column is something you can check without knowing whether the page reads well.
| cell | own filter | peak SNR H1 | chirp mass | GPS of peak | mean rho^2 | from | scale | said 2 unprompted | or fitted to 2 | L1 glitch | figures | external refs | choices | measured | tokens in / out | wall | cost |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Claude Opus 5 · xhigh | yes | 19.67 | 31.78 | 1126259462.4280 | 2.012 | 7,864,320 | 1.003 | yes | no | blind, chi-squared | 12 | 0 | 12 | 7 | 19,475,555 / 169,476 | 58 m 45 s * | 16.47 |
| Codex gpt-5.6-sol · xhigh | yes | 19.86 | 31.35 | 1126259462.4282 | 1.884 | 960 | 0.971 | yes | no | blind, coincidence | 5 | 0 | 6 | 0 | 3,052,987 / 49,220 | 20 m 34 s * | — |
| Claude Opus 5 · low | yes | 20.87 | 31.70 | 1126259462.4280 | 2.257 | 7,831,551 | 1.062 | yes | no | blind, chi-squared | 9 | 0 | 11 | 0 | 2,153,579 / 38,928 | 12 m 03 s | 2.72 |
| Codex gpt-5.4 · xhigh | yes | 19.53 | 31.36 | 1126259462.4282 | 1.959 | — | 0.990 | yes | yes | not found | 6 | 0 | 5 | 0 | 5,746,543 / 68,806 | 24 m 43 s | — |
| Claude Sonnet 5 · xhigh | ended its turn waiting for a background notification that cannot arrive headless — exit code 0, CLI subtype 'success', and no page and no results.json | 14,426,780 / 143,090 | 31 m 04 s | 7.54 | |||||||||||||
| Codex gpt-5.6-sol · low | yes | 19.86 | 32.10 | 1126259462.4277 | 1.933 | 2,000 | 0.983 | yes | no | not found | 5 | 0 | 5 | 0 | 364,153 / 8,904 | 4 m 34 s | — |
| Claude Haiku 4.5 · xhigh | yes | 5236.21 | 36.33 | 1126258440.9927 | 11447.920 | — | 75.657 | no | no | not found | 0 | 1 | 8 | 0 | 690,644 / 17,663 | 3 m 18 s | 0.21 |
| Codex gpt-5.5 · xhigh | yes | 19.83 | 31.75 | 1126259462.4280 | 2.003 | 1,835,007 | 1.001 | yes | no | blind, coincidence | 6 | 0 | 6 | 0 | 1,048,961 / 35,658 | 13 m 56 s | — |
Every number above was read out of the cell's own results.json or measured off the page it left, by a rule fixed per column before the cells were read. scale is the square root of the cell's own mean of rho^2 over 2 — what its own background says its rho was multiplied by. or fitted to 2 is yes where the cell chose a normalisation constant by requiring that mean to come out at 2, which is decided from the source it wrote: such a cell cannot fail the check, whatever number it prints. An asterisk on the wall time means another cell was running on the same laptop.
[src:grid_gpt54_xhigh_chirp_mass, grid_gpt54_xhigh_choices_measured, grid_gpt54_xhigh_choices_n, grid_gpt54_xhigh_gps_peak, grid_gpt54_xhigh_page_external_refs, grid_gpt54_xhigh_page_inline_figures, grid_gpt54_xhigh_rho2_fitted_scale, grid_gpt54_xhigh_rho2_mean, grid_gpt54_xhigh_rho2_scale, grid_gpt54_xhigh_snr_h1, grid_gpt54_xhigh_tokens_in, grid_gpt54_xhigh_tokens_out, grid_gpt54_xhigh_wall_s, grid_gpt55_xhigh_chirp_mass, grid_gpt55_xhigh_choices_measured, grid_gpt55_xhigh_choices_n, grid_gpt55_xhigh_gps_peak, grid_gpt55_xhigh_page_external_refs, grid_gpt55_xhigh_page_inline_figures, grid_gpt55_xhigh_rho2_mean, grid_gpt55_xhigh_rho2_n_samples, grid_gpt55_xhigh_rho2_scale, grid_gpt55_xhigh_snr_h1, grid_gpt55_xhigh_tokens_in, grid_gpt55_xhigh_tokens_out, grid_gpt55_xhigh_wall_s, grid_haiku45_xhigh_chirp_mass, grid_haiku45_xhigh_choices_measured, grid_haiku45_xhigh_choices_n, grid_haiku45_xhigh_cost_usd, grid_haiku45_xhigh_gps_peak, grid_haiku45_xhigh_page_external_refs, grid_haiku45_xhigh_page_inline_figures, grid_haiku45_xhigh_rho2_mean, grid_haiku45_xhigh_rho2_scale, grid_haiku45_xhigh_snr_h1, grid_haiku45_xhigh_tokens_in, grid_haiku45_xhigh_tokens_out, grid_haiku45_xhigh_turns, grid_haiku45_xhigh_wall_s, grid_opus5_low_chirp_mass, grid_opus5_low_choices_measured, grid_opus5_low_choices_n, grid_opus5_low_cost_usd, grid_opus5_low_gps_peak, grid_opus5_low_page_external_refs, grid_opus5_low_page_inline_figures, grid_opus5_low_rho2_mean, grid_opus5_low_rho2_n_samples, grid_opus5_low_rho2_scale, grid_opus5_low_snr_h1, grid_opus5_low_tokens_in, grid_opus5_low_tokens_out, grid_opus5_low_turns, grid_opus5_low_wall_s, grid_opus5_xhigh_chirp_mass, grid_opus5_xhigh_choices_measured, grid_opus5_xhigh_choices_n, grid_opus5_xhigh_cost_usd, grid_opus5_xhigh_gps_peak, grid_opus5_xhigh_page_external_refs, grid_opus5_xhigh_page_inline_figures, grid_opus5_xhigh_rho2_gaussian_calibration, grid_opus5_xhigh_rho2_mean, grid_opus5_xhigh_rho2_n_samples, grid_opus5_xhigh_rho2_scale, grid_opus5_xhigh_snr_h1, grid_opus5_xhigh_tokens_in, grid_opus5_xhigh_tokens_out, grid_opus5_xhigh_turns, grid_opus5_xhigh_wall_s, grid_sol56_low_chirp_mass, grid_sol56_low_choices_measured, grid_sol56_low_choices_n, grid_sol56_low_gps_peak, grid_sol56_low_page_external_refs, grid_sol56_low_page_inline_figures, grid_sol56_low_rho2_mean, grid_sol56_low_rho2_n_samples, grid_sol56_low_rho2_scale, grid_sol56_low_rho2_sem, grid_sol56_low_rho2_sigmas_from_2, grid_sol56_low_snr_h1, grid_sol56_low_tokens_in, grid_sol56_low_tokens_out, grid_sol56_low_wall_s, grid_sol56_xhigh_chirp_mass, grid_sol56_xhigh_choices_measured, grid_sol56_xhigh_choices_n, grid_sol56_xhigh_gps_peak, grid_sol56_xhigh_page_external_refs, grid_sol56_xhigh_page_inline_figures, grid_sol56_xhigh_rho2_mean, grid_sol56_xhigh_rho2_n_samples, grid_sol56_xhigh_rho2_scale, grid_sol56_xhigh_rho2_sem, grid_sol56_xhigh_rho2_sigmas_from_2, grid_sol56_xhigh_snr_h1, grid_sol56_xhigh_tokens_in, grid_sol56_xhigh_tokens_out, grid_sol56_xhigh_wall_s, grid_sonnet5_xhigh_cost_usd, grid_sonnet5_xhigh_tokens_in, grid_sonnet5_xhigh_tokens_out, grid_sonnet5_xhigh_turns, grid_sonnet5_xhigh_wall_s]
The columns are the day's four subjects, and none of them needed a slide. Models and effort are the two left-hand columns. Benchmarking is what the rest of the row is. Headless is how every one of them ran: one command, no conversation, whatever it decided to do in between.
| model | effort | peak SNR H1 | mean rho^2 | L1 glitch | figures | tokens in | wall | cost |
|---|---|---|---|---|---|---|---|---|
| claude-opus-5 | xhigh | 19.67 | 2.012 | found | 12 | 19,475,555 | 58 m 45 s | 16.47 |
| claude-opus-5 | low | 20.87 | 2.257 | found | 9 | 2,153,579 | 12 m 03 s | 2.72 |
| gpt-5.6-sol | xhigh | 19.86 | 1.884 | found | 5 | 3,052,987 | 20 m 34 s | — |
| gpt-5.6-sol | low | 19.86 | 1.933 | not found | 5 | 364,153 | 4 m 34 s | — |
[src:grid_opus5_low_cost_usd, grid_opus5_low_page_inline_figures, grid_opus5_low_rho2_mean, grid_opus5_low_snr_h1, grid_opus5_low_tokens_in, grid_opus5_low_wall_s, grid_opus5_xhigh_cost_usd, grid_opus5_xhigh_page_inline_figures, grid_opus5_xhigh_rho2_mean, grid_opus5_xhigh_snr_h1, grid_opus5_xhigh_tokens_in, grid_opus5_xhigh_wall_s, grid_sol56_low_page_inline_figures, grid_sol56_low_rho2_mean, grid_sol56_low_snr_h1, grid_sol56_low_tokens_in, grid_sol56_low_wall_s, grid_sol56_xhigh_page_inline_figures, grid_sol56_xhigh_rho2_mean, grid_sol56_xhigh_snr_h1, grid_sol56_xhigh_tokens_in, grid_sol56_xhigh_wall_s]
Turning the effort down does not degrade both lines the same way. On one of them the number moves and the checking survives. On the other the number does not move at all — it agrees to four figures — and what goes is the checking: the low cell never searches L1 on its own, so the loudest thing in that detector is never looked at. Both cost about a fifth as much and finish four to five times sooner.
Claude Sonnet 5, at xhigh effort, wrote the whole pipeline — the filter, the search, the checks, the figure code, a report template — ran the search, and then ended its turn with the sentence “I'll now wait quietly for the background notification rather than continue polling.” In a conversation that is sensible. Headless there is no next turn: the process exits when the model stops talking, and nothing was ever going to wake it up.
It left no page and no results.json. What it left instead was exit code 0, and a machine-readable record saying "subtype": "success", after 109 turns and 31 minutes. If you are going to walk away from a run, the thing you check when you come back is the artifact. Not the exit code, and not the summary it wrote about itself.
The loudest thing in L1 in these 512 seconds is not the event. It is a glitch at GPS 1126259245.23, and in one cell's filter it reaches ρ = 17.2 where the real signal in that detector reaches 13.6. Four of the seven runs found it [src:grid_n_glitch_found, grid_n_ran]. All four found it by searching L1 on its own [src:grid_n_glitch_blind] — and only two threw it away without using H1 [src:grid_n_glitch_vetoed_alone], with a χ² that asks whether each band of the template contributed its share of the SNR. The other two simply noted that H1 saw nothing at that time.
Both answers are correct here, and they are not equally useful: a single-detector event has no second detector to appeal to. That is why it is on the list at the end of the hour.
Every cell left the same things — a page, a results.json, the source it wrote, and a record of what it consumed. You are not given ours. Your own run leaves them too, and that is the one worth opening.
All of it, as they left it
Every page the grid produced, whole, in the state its cell wrote it. Pick one. Nothing here is edited, reformatted or excerpted — the file below is the file, and open puts it in its own tab.
Seven pages, one per cell that produced one [src:grid_n_ran]. The eighth is the greyed-out button: it ran for 31 minutes and wrote no page at all. Reading two of these side by side is the fastest way to see what the scoreboard is measuring — the same event, the same strain, the same prompt, and seven quite different accounts of it.
The rest of the hour
First, the three checks.
Whitened variance, the mean of ρ², and an injection you recover. Do them before you look at your SNR, not after. If the mean of ρ² is not 2, find out why before you believe anything else on your screen.
Then compare your two numbers with the room.
Then pick one.
The weekend activity is the same task, as many times as you like, and you come back with your best attempt. Tomorrow is populations, and the thing being turned up is what you hand an agent when you are going to walk away from it.