Day 1 — 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
Start an agent and ask it to reproduce one of the two papers.
I am deliberately not telling you what to type.
The setup
1. Get the papers.
cd <the course repo> ls day1/papers/ PhysRev.136.B1224.pdf Peters 1964 1608.01940v4.pdf The basic physics of GW150914
2. Pick one.
Peters 1964 — analytic. Orbital decay of a binary from the quadrupole formula. Four pages, from 1964, and your agent may or may not have read it.
1608.01940 — observational. GW150914's masses from the strain data, with a ruler.
3. Make a working directory.
mkdir -p ~/gw-day1 cd ~/gw-day1 cp <repo>/day1/papers/*.pdf .
4. Start an agent, there.
claude # or codex
Work in an empty directory you own. The agent will read and write files in it. Never start one in your home directory or in a repository you care about.
Examples, not instructions. Type your own — the differences between what everyone asks for are the experiment.
Terse. “Reproduce this paper.”
Perfectly legitimate. Watch what it decides “reproduce” means.
Specific. “Read PhysRev.136.B1224.pdf. Derive equations (5.6) and (5.7) yourself and check them with sympy. Then reproduce Figure 1 exactly.”
Names the artefact, names the check —
sympy is symbolic algebra in python: exact expressions, so
“check” means an identity, not a float.
With a contract. “…and leave me: a script I can run, a figure, and a list of every claim you verified and how. Say explicitly which results you derived and which you recalled.”
The last sentence is the one that pays.
Also worth trying
Set the dials before you start
Whatever you type, write down which model and which reasoning effort you used. Next stage. We need it for the comparison.
The two dials
claude # interactive claude -p "…" # headless, one shot /model → Opus · Sonnet · Haiku /effort → low … high … max claude --dangerously-skip-permissions
codex # interactive codex exec "…" # headless, one shot /model → model + reasoning effort codex --full-auto codex --dangerously-bypass-approvals-and-sandbox
Model. A bigger model is more expensive per token and usually better at holding a long argument together. It is not uniformly better.
Reasoning effort. How much thinking it does before acting. Costs time and tokens. For a derivation this matters more than the model.
These flags change every few months.
claude --help, codex --help. Do not memorise them; memorise
that the two dials exist, because everything else in this session depends on them.
The week, in one table
The organising idea for the whole week. Not “can AI do science” — but which of these specific jobs can it hold, and what does it need in order to hold them?
| The job | What it needs from a tool | Where it shows up this week |
|---|---|---|
| Read the literature | the source, not a summary of it | today — the paper is in the repo |
| Do a derivation | symbolic algebra, and a way to be checked | today (sympy) · day 4 (Hellings–Downs) |
| Explore — try things, most of them wrong | cheap iteration, no ceremony | every workshop |
| Write code | your conventions, your file layout | day 2 |
| Run code you did not write | docs, source, the ability to read both | day 2 (gwpy, lal) |
| Run something heavy | a cluster, patience, a babysitter | day 3 |
| Make a figure | data provenance and a way to regenerate it | today (marimo) |
| Keep track of what you learned | notes that survive the session ending | today (the wiki) |
| Write the paper | LaTeX, and everything above still reproducible | day 5 |
| Give the talk | slides that are built, not maintained | day 5 |
Every row is a place where the tool either fits your workflow or fights it. Most of the practical content of this course is about the fit.
While that runs
Your agent will take a while. Leave it. If it asks you something, answer briefly and let it continue.
Chat, agent, harness. A chatbot is text in, text out. An agent is the same model in a loop with tools. The harness is everything around it — and the part you configure.
It is one flat block of text, and it is finite. When it fills, it gets summarised, and you do not choose which detail is lost.
It is also what you pay for. The next stage is what a turn costs.
Close the terminal and it is gone. There is exactly one fix: write it down, in the project, in a form the agent will read next time.
All three in full, with the commands, are on using-the-tools.html —
one page for the whole week, which fills in as the week goes. Read them while your run
works.
Still while that runs
A turn is one step of the loop: the agent acts, sees what came back, and decides what to do next. Every turn sends the whole conversation again, from the beginning. Five consequences, and all five are practical.
/clear costs you the setup again and nothing
else.Two runs from later in the week, as each one recorded itself. Each is one task, run once, with nobody watching.
day 2 day 3 model claude-opus-5 claude-opus-5 effort xhigh xhigh turns 132 242 tokens in 19,475,555 58,906,227 tokens out 169,476 235,493 of the input, a cache read 19,213,028 58,560,011 in, per token out 115 250 cached share of the input 98.7% 99.4%
Both of those are one task run once, later in this week — the grid on day 2 and the run left alone on day 3. Read the last two lines. Roughly a hundred times more went in than came out of the first, and about two hundred and fifty times on the second; and in both, near enough all of the input was a cache read rather than something the model had not seen before. That is what a conversation of a few hundred turns looks like from the invoice's side.
Token counts rather than prices, deliberately: a price is a fact
about a price list on the day, and these two runs were measured in August 2026. The
ratios are the durable part. Your own harness will print both if you ask it —
/context in Claude Code, /status in Codex.
Somebody else's run
Mark Cheung, down the corridor at the Institute, wanted to see what a gravitational wave does at every viewing angle at once. He sent this over. We are going to sit in it.
What it is. One HTML file, 6.9 MB, no install and no network: the four prompts he typed, and beside each one the thing it produced. Five interactive visualisers of numerical-relativity waveforms, all embedded inside it.
What to watch for. The second prompt does not ask for a visualiser. It asks for eight candidate ways to draw the thing, side by side, so that he can choose. The third prompt is one sentence: “I like option 4.” Taste, in the loop, twice.
The two dials, from his work. All four prompts ran on the same model at the same reasoning effort — Opus, medium, unchanged between prompts — and his README says so. That is what the dials being recorded looks like.
day1/show/gw_tutorial.html, and his four prompts verbatim beside it in
prompts.txt. Both are yours to keep — he wrote them to be reused
for teaching.
[src:borrowed — the file, the four prompts, the 6.9 MB and the
model-and-effort line are Mark Ho-Yeuk Cheung's (IAS), handed over 2026-08-10 and
reused with his permission; credit and licence in day1/show/CREDIT.md]
It is not embedded here. It is self-contained, it is 6.9 MB, and it has its own navigation — open it in a second tab and come back to this one.
What I got
I ran both papers before you did. One instruction, given twice: once to a four-page analytic paper, once to a paper whose first three figures are made from the strain itself.
Everything that could be checked against an equation was checked and passed. All three figures that touched data are wrong, each in a different way, and nothing in either repository noticed.
| run | model | effort | format asked for | tokens in / out | wall | when | receipt |
|---|---|---|---|---|---|---|---|
| Peters 1964 | — | — | tex → pdf, marimo → html, a script of assertions | — | — | 2026-07-24 | run.log |
| 11/11 reported results reproduced | |||||||
| GW150914, the basic physics | — | — | tex → pdf, marimo → html, a script of assertions, and three figures from strain | — | — | 2026-07-24 | run.log |
| 23/23 reported results reproduced | |||||||
| Fig 1 strain, Fig 2 spectrogram, Fig 3 chirp-mass ridge fit -> 36 Msun (paper 30-40) | |||||||
Not recorded for these runs: tokens, wall time, model, reasoning effort. They predate the convention; the fields are here so a later run fills them.
That is the usual report: a date, a list of files, and a line saying it all worked. Both lines are true. Neither of them is about the three figures further down, because no assertion in either repository touches those figures at all.
Both runs happened on 2026-07-24. The instructions on the next stages are the ones recorded in this repository — the standing rule the runs were given, and the wording this session has quoted since it was written. They are not a keystroke log, and no tokens, wall time, model or effort setting were recorded at the time.
That is a defect, not a style: from day 2 the prompts are kept verbatim as they are typed and the run records what it consumed. The empty columns above are what a run looks like when nobody wrote that down.
What was asked
Not “reproduce this paper”. The instruction names the unit of work (a section), what to produce for each one, and how each claim is to be checked.
Four pages of analysis, from 1964, and no data anywhere in it. This is the easy case, and it is worth naming as the easy case before the hard one.
What came back
| reproduction.tex → .pdf | the argument, section by section |
| reproduce_peters.py | the assertions, and where every number is stamped |
| results.json | every number the reproduction computed, as it came out |
| results_provenance.json | where each of those came from — the script, the function, the line, and whether that section touched data or only algebra |
| provenance.json | the same facts as a registry, which is what the .tex resolves its tags against |
| figures.py → figures.html | a marimo notebook: the two figures, with the parameters left free |
| peters.py | the reference implementation the rest imports |
| run.log | the receipt |
Those files are ours and are not in the copy you were given — the point is what your own run leaves, in whatever shape it leaves it.
The middle three files are the ones worth opening. A number is written into
results.json at the moment it is computed, and the same assignment
writes a second record saying which function computed it and on which line. Neither
file is edited by hand and neither can be: they are both written by the run. Here is
one number in all four places it appears.
One number, in the four places that hold it: the line that computed it, what it came out at, the record of where that came from, and the sentence in the write-up that cites it.
reproduce_peters.py:259
OUT["ht_lifetime_myr"] = Tht
results.json
"ht_lifetime_myr": 300.5982272803736
results_provenance.json
"ht_lifetime_myr": {
"evidence": "numeric",
"file": "reproduce_peters.py",
"func": "section_V_numbers",
"line": 259,
"section": 5,
"section_title": "Peters' own worked lifetimes (§V, p. B1231-B1232)"
}
reproduction.tex:204
lifetime $301$ Myr\src{ht_lifetime_myr} (literature $\sim300$).
The first line is the physics — how long the Hulse–Taylor binary has left before it merges. Everything under it is bookkeeping, and the bookkeeping is what lets somebody who was not there check the first line. The write-up cites the key rather than the digits, so a rerun that moved the number would move the sentence with it, or refuse to build.
Scroll it. It is not a paper and it is not trying to be one — it is what the run left so that somebody who was not there could check it. Notice how much of it is claims and evidence and how little is exposition, and notice the small grey key beside each number.
sympy: the difference between the derived
〈da/dt〉, 〈de/dt〉 and da/de and the paper's printed expressions
simplifies to zero
[src:dadt_5_6_residual_is_zero]
[src:dedt_5_7_residual_is_zero]
[src:dade_5_8_residual_is_zero]. A machine check, not a
re-reading by the mind that made the error.Peters works a Sun-like binary with a 4.5-day period, gets a lifetime of about 3×1012 years, and describes the separation as about ten solar radii. Both statements are his own text, in §V. [src:borrowed — the 4.5-day period and the "about ten solar radii" are Peters 1964 §V, p. B1228]
The lifetime comes back at 3e12 yr [src:solar_lifetime_yr]. The separation does not: Kepler at two solar masses and 4.5 days gives 14 R☉ [src:solar_a_from_4p5d_Rsun], and ten solar radii would be a period of 2.6 d [src:solar_period_at_10Rsun_d]. The lifetime is right; the separation in the text is rounded.
Unimportant to the physics, and completely characteristic. It is the difference between a reproduction and a paraphrase, and neither I nor an agent would have noticed it without running the number.
What was asked
The one change: this paper's first three figures are made from the strain, not from the algebra. So the run had to fetch data and use a package neither of us had used before.
It fetched 32 s of H1 and L1 strain around the event from GWOSC at
4096 Hz and cached it to a local array, so the analysis needs no network, and it used
gwpy in a separate environment — the day-1 environment deliberately
has no gravitational-wave stack in it.
What came back, first half
The chain the paper builds, rebuilt and checked at every link: the chirp mass from f and its derivative; a floor on the total mass of 69 M☉ [src:equal_mass_total_msun] because the symmetric mass ratio cannot exceed 1/4; a Kepler separation at 150 Hz of 345 km [src:eq9_separation_km]; a compactness of 1.7 [src:compactness_ratio_equal_mass] Schwarzschild radii, which nothing but a pair of black holes fits; a peak luminosity of 3.4e49 W [src:peak_luminosity_w], or 9e-4 of c&sup5;/G [src:peak_luminosity_over_planck].
Relax the equal-mass assumption and the argument still closes: the lighter body is above 5.1 M☉ [src:eq15_m2_min_msun] under the permissive bound, and the compactness stays near unity [src:q13_compactness_ratio].
That peak luminosity equals −dE/dt from Peters' circular da/dt, computed from the other paper and agreeing to zero relative error [src:peak_luminosity_vs_peters_relerr]. Two papers, fifty years apart, are the same formula read from opposite ends. An independent route agreeing is worth more than any number of internal assertions.
What came back, second half
Every one of them is wrong, in a different way. The next four stages are the four failures, one at a time, each beside the paper's own figure.
Two of the three were caught by someone who has seen enough of these plots to know what one looks like. The third was caught by nobody, and the fourth failure is about why.
Failure 1
Cause 1 — the pass band is not flat.
The code did what the paper's text says: band-pass 35–350 Hz, notch, no whitening. But inside that band the noise still varies by a factor of a few hundred, peaking at 37 Hz — so a plain band-pass leaves the low edge of the band dominant. That is the big slow wobble. The paper never mentions whitening (day 2's subject). It clearly whitened.
Cause 2 — the shift went the wrong way.
The paper shifts H1 back by 6.9 ms. The code added it, putting the two chirps 14 ms apart instead of on top of each other. Cross-correlating the whitened streams recovers −7.3 ms on its own — the data will tell you the answer if you ask it.
What caught it: someone who has seen enough of these plots to know what one looks like. No assertion in the repository would ever have fired. [src:borrowed — the 6.9 ms shift and the band and notch prescription are the paper's own (1608.01940 §Fig 1); the cross-correlation recovery and the noise variation across the band are the session artefact's own measurements]
Failure 2
What it did. A gwpy
Q-transform, plotted raw. That function returns normalised
energy; the paper's colour bar is normalised
amplitude — the square root.
Why it is wrong. Two different
quantities under the same axis label. The colour ceiling was then hand-set to 25
— vmax=25 in the plotting call — until the picture looked
right, rather than matched to the paper's. The transform's own Q range was picked
by hand too, and differs from the one used for Fig. 3, with no reason recorded for
either.
What caught it. Matias, by eye — “your signal looks much brighter.” The chirp is visible in both, which is exactly why nobody looked twice.
Fixable in one line — take the square root and set the limit to the paper's — and that is the point. It was never wrong in a way that broke anything; it was wrong in a way that no test could see, and only a comparison against the original catches it.
Failure 3
What the paper says to do. Measure the time between successive zero crossings of the band-passed strain, take f = 1/(2Δt), average five adjacent crossings where the signal is weak, combine H1 and L1.
What it actually did. Took the peak of each column of the Q-transform. The steps are the transform's frequency bins. This is a different measurement of a different thing — and the file's own docstring says “from zero-crossings”.
What caught it. Nothing. The fitted chirp mass landed inside the range the paper states — the line quoted in the run table above — so the number agreed while the method did not.
The best of the three. The docstring describes the paper's method; the code below it does something else; the answer still came out in range. A right answer obtained the wrong way is the hardest failure to catch, because the only thing you normally check is the answer.
Failure 4
The run log for that exercise says every assertion passes. It does. And not one of those assertions touches any of the three figures you just saw.
The check is named after Figure 3. Here it is, whole:
reproduce_basic_physics.py, lines 137–144.
# Fig 3 method: fit f^-8/3 vs t on a clean chirp track -> M (~30 detector frame).
Mc = 30.0 * MSUN
tt = np.linspace(-0.20, -0.005, 200)
ff = B.track_f_of_t(tt, Mc, t_c=0.0)
Mfit = B.fit_chirp_mass_from_track(tt, ff) / MSUN
OUT["fig3_method_recovers_mc_msun"] = Mfit
check("(Fig 3) f^-8/3 straight-line fit recovers M = 30 Msun", abs(Mfit - 30) < 1e-6,
f"{Mfit:.4f} Msun")
Read it from the top. A chirp mass is chosen by hand on the first line.
track_f_of_t builds a frequency-versus-time track out of that number. The
track goes straight back into fit_chirp_mass_from_track, which inverts the
same formula and hands the number back. It is a function put against its own inverse.
No strain enters it at any point, and there is no arrangement of the data under which
the result would have come out differently.
The check is called (Fig 3), and it sits in a list that ends with a line saying all twenty-three reported results were reproduced. Read as a receipt, that line says the paper was reproduced. What it says is that the algebra was.
The three figures made from strain — the band-pass, the spectrogram, and the ridge with its fit, all three of which the last three stages have just shown to be wrong — carry no assertion at all. Nothing in the output admits it. The count is over the checks that exist, and a check nobody wrote subtracts nothing.
The first is the tolerance on the last line. Nothing measured off real noisy data comes back at 30.000000 [src:fig3_method_recovers_mc_msun]; only algebra does. A tolerance that tight is a statement about what is being compared, and it is on the page for anyone who reads it.
The second is in the same receipt, a few lines below the green banner. The run also printed what the real Figure 3 produced from the strain:
run.log, lines 6–8 and line 14. The second is what the same run got off the strain.
====================================================================== 23/23 reported results reproduced ====================================================================== ... Fig 1 strain, Fig 2 spectrogram, Fig 3 chirp-mass ridge fit -> 36 Msun (paper 30-40)
The number the run actually got off the data and the number the check verified are different numbers, in the same file, twelve lines apart — and it is the check that passed.
So the honest state of that exercise: the analytic half is verified; the data half was never checked at all — and the receipt does not say so.
The pattern
Across all of them, the failures are not random. They cluster:
| Failure mode | What it looks like | What defends against it |
|---|---|---|
| Recall dressed as derivation | the right final answer, with a derivation that does not produce it | ask which steps were derived and which recalled |
| Confident interpolation | a plausible coefficient where the real one is unusual | check against a printed number |
| Silent unit errors | everything consistent, one constant in the wrong system | dimensional analysis; a second implementation |
| Claiming without running | “the fit converges” — no log, no output | ask for the receipt, always |
| Drifting under a full context | fine early, degraded late, no announcement | bound the task; start fresh |
| Agreeing with you | you suggest something wrong and it goes along | ask it to argue against you |
| Uncovered by construction | a green summary line over work that no assertion touches | make it state what it did not check |
None of these are fixed by a better model. They are fixed by checks.
Questions of that kind are scattered
through homework.html, where every exercise the course sets lives.
Treat them as examples rather than as a form: the job is to invent the ones your own
run needs.
Before the next session
Physics
Ask for more than an answer
Make it explain the same result at three levels. Make it visualise something you only have as algebra. Make it derive a result a second way and reconcile the two.
Then push on the boundaries: where does each approximation break, and what happens on the other side?
The controlled experiment
Take one task and run it several ways: two models, two effort settings, terse prompt vs contract prompt. Ask the same questions of each. Report what actually moved the answer.
One sample per cell — report what you saw, not what is true in general.
Come back with a vault containing pages neither of us wrote by hand — and at least one page where you corrected something the agent got wrong.
Every exercise the course sets is on one page,
homework.html. Every concept this week introduces, and the day it
arrives: using-the-tools.html. Pick what interests you; nobody is
expected to do all of it.
None of this is settled. It is what one person does in July 2026, and half of it will look naive in a year. Take it as a starting point and go argue with it.