From Dennis Ogiermann, 2026-08-14: "We can recycle the grid over several timesteps (if you are not already doing this) to speed things further up."
Answering the parenthetical first
Partly, yes — and the part that is already right is the invalidation boundary. regrid! returns its input fields unchanged and leaves the forest generation untouched when the marks change nothing, so existing prepared operators stay valid (src/amr.jl:29-32); and PreparedForest refuses to run against a bumped generation rather than silently computing garbage (src/linalg.jl:525-530). That is the hard part and it is done.
Everything above that line is hand-rolled or unused.
What's missing
(a) Cadence is a constant in each example, not an API. examples/monodomain_amr.jl:41 has REGRID_EVERY = 20; examples/niederer_benchmark.jl:91 has REGRID_EVERY = 50, used at :450 and followed by a manual rebuild of Lop = diffusion_operator(bf) at :457. Two examples, two different numbers, no guidance anywhere on how to pick one, and no way for a user to discover the pattern short of reading an example.
(b) There is no buffer / safety zone, which is the thing that makes a long interval correct. If the forest is only re-marked every k steps, a depolarization wavefront can leave the refined region during those k steps and be integrated on coarse cells — a silent accuracy loss with no error and no assertion. The standard fix (Berger–Colella) is to dilate the refine marks by a buffer of b cells before balancing, so that b bounds how far the front can travel within the interval: roughly b ≥ k · v · Δt / h. Without a buffer, the safe interval is bounded by a property nobody has written down, which is why the two examples disagree by a factor of 2.5 and neither can justify its number.
(c) The reuse the generation guard already permits is not being taken. Neither AMR example calls prepare at all. examples/monodomain_amr.jl:62 does laplacian(bf) * V — a fresh allocating BlockField every single step — on the 19 out of every 20 steps where the forest generation is unchanged and a PreparedForest would be valid. examples/niederer_benchmark.jl:178 at least reuses a preallocated LV via apply!, but still never prepares, so it re-walks the tree and re-warms the exchange schedule instead of holding the prepared state across the 49 steps where it is valid.
Proposal
- Add a
buffer keyword to regrid! that dilates the refine marks by b cells (or blocks) before 2:1 balancing, so a longer interval is safe by construction rather than by luck. Document the b ≥ k · v · Δt / h reasoning next to it.
- Document the canonical adaptive time-stepping loop — regrid on a cadence, re-
prepare only when the generation bumps, reuse otherwise — in the AMR docs page and in the regrid! docstring, which currently explains invalidation without showing the reuse it enables.
- Convert both AMR examples to that loop, which is also the honest way to measure whether it helps.
Expected payoff, stated honestly
Not what the phrasing suggests. #49 measured this already: the coarse–fine halo exchange is the AMR tax at roughly 92% of an apply, and regridding is not. So skipping regrids does not buy much, and any claim that it does should be treated as unmeasured.
What is real is (c) — the per-step BlockField allocation in the monodomain example is pure waste on 95% of steps, and the package already has the mechanism to remove it. And (b) is a correctness win, not a speed win: it is what lets the interval grow at all without quietly under-resolving the front.
The check that would falsify all of this is a phase breakdown comparable to #49's, before and after, on examples/niederer_benchmark.jl. If t_regrid and the allocation count do not move, the issue was wrong.
Adjacent, possibly its own issue
boundary_rhs re-allocates on every call (src/linalg.jl:398-413: similar, fill!, allocate_output, _apply_raw!). It is documented as "assemble once per solve" (src/linalg.jl:381, DESIGN.md:1011), but nothing in PreparedOperator holds it, so a loop with time-varying Dirichlet data re-allocates the lift every timestep. Same family of waste, different mechanism.
🤖 Claude noticed the grid was already being recycled and the operator was not.
From Dennis Ogiermann, 2026-08-14: "We can recycle the grid over several timesteps (if you are not already doing this) to speed things further up."
Answering the parenthetical first
Partly, yes — and the part that is already right is the invalidation boundary.
regrid!returns its input fields unchanged and leaves the forest generation untouched when the marks change nothing, so existing prepared operators stay valid (src/amr.jl:29-32); andPreparedForestrefuses to run against a bumped generation rather than silently computing garbage (src/linalg.jl:525-530). That is the hard part and it is done.Everything above that line is hand-rolled or unused.
What's missing
(a) Cadence is a constant in each example, not an API.
examples/monodomain_amr.jl:41hasREGRID_EVERY = 20;examples/niederer_benchmark.jl:91hasREGRID_EVERY = 50, used at:450and followed by a manual rebuild ofLop = diffusion_operator(bf)at:457. Two examples, two different numbers, no guidance anywhere on how to pick one, and no way for a user to discover the pattern short of reading an example.(b) There is no buffer / safety zone, which is the thing that makes a long interval correct. If the forest is only re-marked every
ksteps, a depolarization wavefront can leave the refined region during thoseksteps and be integrated on coarse cells — a silent accuracy loss with no error and no assertion. The standard fix (Berger–Colella) is to dilate the refine marks by a buffer ofbcells before balancing, so thatbbounds how far the front can travel within the interval: roughlyb ≥ k · v · Δt / h. Without a buffer, the safe interval is bounded by a property nobody has written down, which is why the two examples disagree by a factor of 2.5 and neither can justify its number.(c) The reuse the generation guard already permits is not being taken. Neither AMR example calls
prepareat all.examples/monodomain_amr.jl:62doeslaplacian(bf) * V— a fresh allocatingBlockFieldevery single step — on the 19 out of every 20 steps where the forest generation is unchanged and aPreparedForestwould be valid.examples/niederer_benchmark.jl:178at least reuses a preallocatedLVviaapply!, but still never prepares, so it re-walks the tree and re-warms the exchange schedule instead of holding the prepared state across the 49 steps where it is valid.Proposal
bufferkeyword toregrid!that dilates the refine marks bybcells (or blocks) before 2:1 balancing, so a longer interval is safe by construction rather than by luck. Document theb ≥ k · v · Δt / hreasoning next to it.prepareonly when the generation bumps, reuse otherwise — in the AMR docs page and in theregrid!docstring, which currently explains invalidation without showing the reuse it enables.Expected payoff, stated honestly
Not what the phrasing suggests. #49 measured this already: the coarse–fine halo exchange is the AMR tax at roughly 92% of an apply, and regridding is not. So skipping regrids does not buy much, and any claim that it does should be treated as unmeasured.
What is real is (c) — the per-step
BlockFieldallocation in the monodomain example is pure waste on 95% of steps, and the package already has the mechanism to remove it. And (b) is a correctness win, not a speed win: it is what lets the interval grow at all without quietly under-resolving the front.The check that would falsify all of this is a phase breakdown comparable to #49's, before and after, on
examples/niederer_benchmark.jl. Ift_regridand the allocation count do not move, the issue was wrong.Adjacent, possibly its own issue
boundary_rhsre-allocates on every call (src/linalg.jl:398-413:similar,fill!,allocate_output,_apply_raw!). It is documented as "assemble once per solve" (src/linalg.jl:381,DESIGN.md:1011), but nothing inPreparedOperatorholds it, so a loop with time-varying Dirichlet data re-allocates the lift every timestep. Same family of waste, different mechanism.🤖 Claude noticed the grid was already being recycled and the operator was not.