1# CLAUDE.md
3Notes for anyone, human or agent, working in this repository. Read
4[README.md](README.md) first for what the tutorial contains.
6## The shape of the thing
8A Vite + React app that embeds [numbl](https://numbl.org) as a library
9(`numbl/browser`, `createNumblSession`). This is the seqlab / barycentric-rational
10pattern, not the `numbl-project.json` + site-viewer pattern: there is no project
11file and the deploy is a plain `vite build`.
13```
14src/
15 App.tsx sidebar, hash routing, chapter switching
16 ui/ Lab (one runnable section), ChapterView, Controls, Markdown
17 engine/ the numbl session wrapper
18 plot/ a small hand-rolled SVG layer, plus a canvas field plot
19 figures/ static diagrams, one .tsx per part, keyed in figures/index.ts
20 matlab/ the shared .m library, shown to the reader on its own page
21 chapters/
22 intro/ the introduction: front matter, no labs
23 onedim/driver.m the driver for chapters 1-4, plus their shared panels
24 twodim/driver.m the driver for chapters 5-7, plus their shared panels
25 chNN/ per-chapter text.md, editable .m script, chapter.ts
26 ch11/panels.tsx also the offline-results upload for the benchmark plot
27bench/ offline ports of the two best methods for real MATLAB,
28 Python and Julia; the JSON they write (schema
29 "laplace-dirichlet-bench-1", documented in bench/README.md)
30 loads into chapter 11's work-precision plot
31```
33A **chapter** is prose in one `text.md` with `<!--lab:id-->` markers where its labs
34go. A **lab** is a driver (fixed, hidden), an editable script (the method), a list of
35declarative controls, and a render function. Each lab owns one numbl session for as
36long as it is on screen: moving a control rewrites `params.json` and re-runs, while
37editing the script reboots the session, because the script is compiled into `main.m`
38at boot.
40`main.m` is `driver.m` + `"\n"` + the editable script, so the script's functions
41become **local functions of the driver** and the driver can call them. That is the
42whole of the contract; each driver's header comment states which functions its
43script must define.
45## Working with numbl
47**This tutorial needs numbl 0.4.19 or later.** It runs correctly on 0.4.18 and
48is drastically slower there: the L-shaped sweep in Chapter 9 takes about three
49minutes instead of six seconds, and Chapter 8 about six seconds instead of a
50fifth of one.
52The reason is worth recording. Most of what this section used to say was a list
53of ways to fall off numbl's JIT fast path, found by profiling this tutorial: a
54`switch` anywhere in a call chain, a struct parameter, `A(k,:) = row` with a
55loop index, a bare `tic;`, and the expensive one: the rule that nothing nested
56under a loop the JIT had declined was compiled either, so a single unsupported
57line could put a whole solver on the interpreter. They were fixed
58upstream in 0.4.19, and the drivers here were rewritten back into ordinary
59MATLAB once they were: the sweeps are plain `for` loops again. If you find a
60comment describing a recursion-instead-of-a-loop workaround, it is stale; the
61git history has the old shape.
63What is left is worth knowing.
65### Keep the *timed* loop type-stable
67numbl specialises a loop on the types flowing through it. A loop whose iterate
68is assigned from several different functions, or whose body indexes a cell
69array, is not something it can specialise, and it runs interpreted. That is
70usually harmless, since the expensive callee inside is still compiled, but when
71the loop being measured *is* the thing under test the overhead lands in the
72number on screen. Two consequences visible in `ch08/iterative.m`:
74- each iterative method has its own loop rather than one loop with a switch
75 inside it, and multigrid keeps its grid hierarchy in one flat vector rather
76 than a cell array;
77- residual and error histories accumulate as squares and are square-rooted
78 once at the end. A `sqrt` inside the loop costs about 60 per cent on the
79 sweep, because sqrt may return a complex value and the history array is then
80 no longer a plain real array.
82Neither is a correctness matter. Check with `%!numbl:assert_jit` inside a loop
83body (it throws if that unit ran interpreted) and `NUMBL_JIT_LOG=1`, which
84prints every JIT decline with its reason.
86### `jsondecode` of a JSON array of arrays gives a cell of column vectors
88MATLAB gives a matrix when the rows are the same length. The L-shaped driver
89unpacks `p.nsm` by hand.
91### `jsonencode` refuses complex values, and flattens single-row matrices
93`jsonencode([1 2 3])` is `[1,2,3]`, not `[[1,2,3]]`, so anything matrix-shaped
94goes through `lp_rows`, which converts it to a cell array of rows first. NaN and
95Inf cross as `null`, which is why the TypeScript side uses `Num = number | null`
96everywhere and every plot helper skips nulls. A value whose imaginary part is
97entirely zero counts as real and encodes fine.
99### Backslash densifies sparse matrices
101`A \ b` converts a sparse `A` to dense before calling LAPACK, so there is no
102sparse direct solver here. That is why Chapter 5 stops at $N = 48$ and why
103Chapters 6 to 10 are about methods that do not need one. The text says so where
104it matters.
106### Scalar loops are often faster than vectorised slices
108A hand-written double loop over a stencil beats the equivalent
109`A(2:end-1,...)` expression by about a factor of three, because the vectorised
110form allocates several temporaries per sweep. The kernels in
111`ch08/iterative.m` are written as loops for that reason, which also happens to
112make them readable.
114### The browser is slower than the CLI
116The CLI uses a native LAPACK addon; the browser falls back to a TypeScript
117implementation, and it announces which one on `console.error`. Dense solves are
118noticeably slower in the page than in `npm run test:matlab`. Nothing in the
119tutorial depends on the absolute numbers, but do not quote a CLI timing as if it
120were what a reader will see.
122## Measuring time honestly
124Timings come from `tic`/`toc` **inside** the MATLAB layer, around the solve only, so
125they exclude session boot, JSON encoding and rendering. Each method gets a warm-up
126call before its first timed solve, because the first call also pays for compilation,
127and any solve under 5 ms is repeated a few times with the minimum kept. Below about
1280.1 ms the measurement is dominated by fixed overhead and the work-precision curves
129flatten out on the left; that is the clock, not the method.
131## Testing
133```
134npm run test:matlab # ~2 min: the .m layer through the numbl CLI
135npm run build && npm run test:browser [--shots]
136```
138Every assertion in `test:matlab` is a number stated in the prose: an observed order,
139an error at a given resolution, a ratio between two methods. If one fails, either the
140code or the text is wrong, and the text is not allowed to win by default.
142`test:browser` opens every chapter, presses run on the labs that wait to be asked,
143and checks that nothing throws and that the figures contain data. It cannot judge
144whether a picture is any good; `--shots` writes full-page screenshots into `tmp/` so
145that a human, or an agent with vision, can look.
147## Static figures
149`src/figures/` holds the diagrams that are drawn rather than computed: the problem
150statement, the stencils, the roundoff-floor V, the V-cycle, the re-entrant corner,
151the close-evaluation spike. They are addressed from the prose by `<!--fig:id-->`, the
152same way labs are addressed by `<!--lab:id-->`, and resolved through the registry in
153`figures/index.ts`. They exist because several chapters, Chapters 8 to 10 in
154particular, show nothing at all until the reader presses Run.
156They are plain inline SVG with no data behind them, written against a 716-unit-wide
157viewBox and `width="100%"`, using `plot/palette.ts` for colour so that a curve
158labelled `fd2` is the same blue everywhere. Two habits are worth keeping. Text is
159placed by hand, so a caption or a legend that fits at one size may collide at
160another: render and look. And the introduction's accuracy-against-cost picture is a
161schematic, which its caption says out loud; do not let it drift into looking like
162measured data.
164To look at them, point a headless browser at each chapter and screenshot
165`figure.static-figure`; `npm run test:browser --shots` writes whole pages, which is
166coarser but needs no extra script.
168## Deployment
170`.cofferdam/workflows/deploy.yml` runs on every push to `main`: `npm ci`,
171`npm run build`, then `upload-pages-artifact` and `deploy-pages`, which the
172vault substitutes with its own implementations to publish the repository's
173site. Two constraints shape it. `actions/cache` is not implemented in the
174vault's CI, so `setup-node` must not be given `cache: npm`. And a vault site is
175served sandboxed, in an opaque origin, so `localStorage` and friends *throw*;
176the app uses none, and adding one would break the deployed page while working
177perfectly in `npm run dev`.
179Watch a deploy with `cofferdam run list --limit 3 --json` and
180`cofferdam run view <n> --log`.
182## Plotting
184`src/plot/` is a small SVG layer with no chart library. `palette.ts` records which
185colour does which job and the validator run that justifies the set; if you add a
186series, re-run the validator rather than picking a hex that looks nice. Fields are
187drawn into a canvas at one pixel per grid cell and scaled, so a 512-by-512 grid costs
188no more to draw than a 16-by-16 one, and points outside the domain are passed as
189`null` and left as background.
191## Things deliberately not done
193- A sparse direct solver, for the reason above.
194- Graded meshes in Chapter 9. The chapter names local refinement and singular
195 enrichment as the standard remedies and then demonstrates a third, because the
196 third is the one that is short enough to read.
197- Close-evaluation quadrature in Chapter 10. The failure is shown and quantified; the
198 fix is a chapter of its own.
199- Neumann and mixed boundary conditions, anywhere.