/ tutorials / laplace-dirichlet
Sign in
tutorials / laplace-dirichlet
laplace-dirichlet / CLAUDE.md
180 lines · 9.1 KBPreviewCodeBlameHistoryRaw
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 matlab/ the shared .m library, shown to the reader on its own page
20 chapters/
21 onedim/driver.m the driver for chapters 1-4, plus their shared panels
22 twodim/driver.m the driver for chapters 5-7, plus their shared panels
23 chNN/ per-chapter text.md, editable .m script, chapter.ts
24```
26A **chapter** is prose in one `text.md` with `<!--lab:id-->` markers where its labs
27go. A **lab** is a driver (fixed, hidden), an editable script (the method), a list of
28declarative controls, and a render function. Each lab owns one numbl session for as
29long as it is on screen: moving a control rewrites `params.json` and re-runs, while
30editing the script reboots the session, because the script is compiled into `main.m`
31at boot.
33`main.m` is `driver.m` + `"\n"` + the editable script, so the script's functions
34become **local functions of the driver** and the driver can call them. That is the
35whole of the contract; each driver's header comment states which functions its
36script must define.
38## Gotchas in numbl 0.4.18
40These were all found the hard way, by measurement. Most of them cost a factor of
41twenty in speed rather than producing a wrong answer, which makes them easy to miss
42in a project whose subject is speed.
44### Invoke the script with `run('main.m')`, not `main;`
46numbl mis-binds the arguments of a script's local functions when the script is
47invoked by name from the REPL, which is what `session.execute` gives us: the callee
48sees its parameters as undefined. `engine/runner.ts` goes through `run()` for this
49reason.
51### The compiler's fast path, and the eight ways to fall off it
53numbl compiles MATLAB to JavaScript and infers types as it goes. A loop whose body
54it cannot type is run through a generic path that is 20 to 70 times slower. The
55following all cause that, and none of them are errors:
571. **Cell or struct indexing inside a loop.** `C{k}` or `p.tol` in a loop body
58 deoptimises the whole loop *and every function it calls*. Hoist the read above
59 the loop.
602. **A multiple-output call inside a loop.** `[a, b] = f(...)`. Return one matrix
61 and unpack it afterwards.
623. **A slice assignment inside a loop.** `A(k, :) = row`. Do the copy inside a
63 called function, or with a scalar loop.
644. **Bare `tic` / `toc` inside a loop.** Use handles: `t = tic; ...; toc(t)`.
655. **Growing an array inside a loop**, that is, `v(k) = ...` for a `v` that was
66 never preallocated.
676. **A loop variable with more than one kind of assignment.** This is the subtle
68 one. `Y = V; for ... Y = f(Y); end` is slow, and `Y = f(V); for ... Y = f(Y); end`
69 is fast, because in the first case the type of `Y` at the top of the loop is the
70 merge of "a parameter" and "whatever `f` returns". It applies to dead branches
71 too: an `if` arm that assigns `Y = reshape(...)` slows the loop down even when it
72 never runs. Give each loop variable one source, or use separate names.
737. **Passing a loop-written array into a function call, even after the loop.**
74 `for ... res(k) = ...; end; H = pack(res, ...)` is slow, while `H = [res; ...]`
75 is fast. This is why the iterative chapter hands the page its *squared*
76 residuals and lets JavaScript take the square roots.
778. **A call inside a loop, to a function that itself loops and calls further
78 functions.** The chain "loop → solver → kernel with loops" is compiled
79 generically even when every individual piece is fine. Recursion in place of the
80 outer loop is compiled normally, which is what `drv_res`, `drv_methods` and their
81 relatives in the drivers are for. This one was worth a factor of 30 on the
82 L-shaped conjugate gradient solve.
84The rule of thumb that follows: **hot loops should touch only preallocated numeric
85arrays and scalars, nothing they write should later be handed to a function, and any
86loop that drives a timed solve should be a recursion.** When in doubt, measure: time
87the same call inside and outside a loop and compare.
89### `sqrt`, `log` and `abs` widen a scalar's type to complex
91`v = v + r*r` in a loop followed by `v = sqrt(v)` after it makes the loop 20 times
92slower, because `sqrt` in numbl can return a complex number and the inferred type of
93`v` is widened accordingly. Accumulate into one variable and take the root into
94another, or better, return the sum of squares and take the root in the caller. Every
95`res_sumsq` in this repository exists for that reason.
97### `sqrt(NaN)` is `0 + NaN i`, and `log(NaN)` is complex
99MATLAB returns `NaN`. numbl returns a complex value whose **real part is zero**, so
100`real(sqrt(NaN))` is `0`, not `NaN`. Square-rooting an array with a NaN tail silently
101turns the tail into zeros, which in an error history reads as the iteration having
102converged perfectly. The iterative chapter therefore never takes a square root in
103MATLAB at all, and `lp_orders` guards its ratio before taking a logarithm.
105### `jsondecode` of a JSON array of arrays gives a cell of column vectors
107MATLAB gives a matrix when the rows are the same length. The L-shaped driver unpacks
108`p.nsm` by hand.
110### `jsonencode` refuses complex values, and flattens single-row matrices
112`jsonencode([1 2 3])` is `[1,2,3]`, not `[[1,2,3]]`, so anything matrix-shaped goes
113through `lp_rows`, which converts it to a cell array of rows first. NaN and Inf cross
114as `null`, which is why the TypeScript side uses `Num = number | null` everywhere and
115every plot helper skips nulls.
117### Backslash densifies sparse matrices
119`A \ b` converts a sparse `A` to dense before calling LAPACK, so there is no sparse
120direct solver here. That is why Chapter 5 stops at $N = 48$ and why Chapters 6 to 10
121are about methods that do not need one. The text says so where it matters.
123### Scalar loops are often faster than vectorised slices
125A hand-written double loop over a stencil beats the equivalent `A(2:end-1,...)`
126expression by about a factor of three, because the vectorised form allocates several
127temporaries per sweep. The kernels in `ch08/iterative.m` are written as loops for
128that reason, which also happens to make them readable.
130### The browser is slower than the CLI
132The CLI uses a native LAPACK addon; the browser falls back to a TypeScript
133implementation, and it announces which one on `console.error`. Dense solves are
134noticeably slower in the page than in `npm run test:matlab`. Nothing in the tutorial
135depends on the absolute numbers, but do not quote a CLI timing as if it were what a
136reader will see.
138## Measuring time honestly
140Timings come from `tic`/`toc` **inside** the MATLAB layer, around the solve only, so
141they exclude session boot, JSON encoding and rendering. Each method gets a warm-up
142call before its first timed solve, because the first call also pays for compilation,
143and any solve under 5 ms is repeated a few times with the minimum kept. Below about
1440.1 ms the measurement is dominated by fixed overhead and the work-precision curves
145flatten out on the left; that is the clock, not the method.
147## Testing
149```
150npm run test:matlab # ~2 min: the .m layer through the numbl CLI
151npm run build && npm run test:browser [--shots]
152```
154Every assertion in `test:matlab` is a number stated in the prose: an observed order,
155an error at a given resolution, a ratio between two methods. If one fails, either the
156code or the text is wrong, and the text is not allowed to win by default.
158`test:browser` opens every chapter, presses run on the labs that wait to be asked,
159and checks that nothing throws and that the figures contain data. It cannot judge
160whether a picture is any good; `--shots` writes full-page screenshots into `tmp/` so
161that a human, or an agent with vision, can look.
163## Plotting
165`src/plot/` is a small SVG layer with no chart library. `palette.ts` records which
166colour does which job and the validator run that justifies the set; if you add a
167series, re-run the validator rather than picking a hex that looks nice. Fields are
168drawn into a canvas at one pixel per grid cell and scaled, so a 512-by-512 grid costs
169no more to draw than a 16-by-16 one, and points outside the domain are passed as
170`null` and left as background.
172## Things deliberately not done
174- A sparse direct solver, for the reason above.
175- Graded meshes in Chapter 9. The chapter names local refinement and singular
176 enrichment as the standard remedies and then demonstrates a third, because the
177 third is the one that is short enough to read.
178- Close-evaluation quadrature in Chapter 10. The failure is shown and quantified; the
179 fix is a chapter of its own.
180- Neumann and mixed boundary conditions, anywhere.
↑↓ move↵openescclose