Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ uw_*.msh

# Visualization outputs (exclude generated, allow curated assets)
docs/examples/**/output/**
docs/examples/**/transcripts/**
docs/examples/**/*.png
docs/examples/**/*.jpg
docs/examples/**/*.gif
Expand Down
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ This keeps feature branches independent and makes cross-pollination of fixes str
- Cherry-pick to `main` if critical → tag patch release
- Cherry-pick to active feature branches (underworld-claude handles this)

### Adversarial Review Before the PR Opens
**Every branch gets an adversarial review before it becomes a PR**, and again
after any substantial post-review commit. The checklist — parallel rank
asymmetry, frame/unit boundaries, determinism, tests that cannot fail, CI
reach, and the solver/transcript contracts — is in
`docs/developer/guides/adversarial-review.md`. Post the findings on the PR.

### Git Worktrees for Session Isolation
**Use a worktree for any multi-file change** (docs cleanup, refactoring, features).
Multiple Claude sessions sharing one working directory will overwrite each other's work.
Expand Down
1 change: 1 addition & 0 deletions docs/developer/UW3_STYLE_CHARTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ These settle the June-2026 drift (see `docs/reviews/2026-07/API-CONSISTENCY-REVI
| Solver capabilities | Anything that configures or reads one solver is a METHOD on that solver, lazily importing its `utilities/*` implementation (the `boundary_flux` pattern) — never a free function as the documented entry point. |
| Namespaces | Every user-facing module is exported from its subpackage `__init__`/`__all__` in the PR that creates it. No deep-import-only features. |
| Docstrings | NumPy/Sphinx style with RST `:math:`. This SUPERSEDES the Markdown-for-pdoc prescription still printed in `UW3_Style_and_Patterns_Guide.md` — that section is wrong; do not follow it. |
| Constants in symbolic forms | A number that enters a residual, a scheme or a constitutive law is EXACT where it is exact: `sympy.Rational(1, 2)` for a theta of one half, `sympy.Integer(4)` for a weight of four, never `0.5` or `4.0`. A float cannot cancel, and prints as `0.5` or `64.0` in the form the run records. A regulariser that keeps a denominator finite is `uw.maths.functions.vanishing`, which prints as :math:`\varepsilon`, never a literal `1e-30`. User-supplied floats are made exact at the boundary with `sympy.nsimplify(x, rational=True)`. |

## 7. Data Access

Expand Down
174 changes: 174 additions & 0 deletions docs/developer/design/run-plan-and-transcript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# The run plan and the run transcript

*Status: design note. Vocabulary and the model it implies. No implementation.*

## Why this note exists

Underworld3 records what a run did — `model.transcript`, the on-disk log, the
figure. Building those made it clear that the hard part is not the recording
but the **view**: the record has to make a *relation* visible, and until we
knew which relation, each addition was a patch on the last.

The relation is this. A value stored by one action in one cycle is picked up by
a read in the next. Every silent defect found while building the timestepping
machinery was a read taking the wrong write:

- an Eulerian history that initialises only on its first solve, so a solver
reused for a second run silently reads the **previous run's** store
- the history-before-advection ordering trap
- `old_frame_traceback` on a deforming mesh (#423), where a semi-Lagrangian
history recorded in the old frame and read after the mesh moved amplifies
~10% per cycle, with no error and no symptom other than the growth rate
- reading a solver's `F0`/`F1` templates before the solve configured them

None of those is visible in a single cycle. All of them are structure across
cycles.

## Two documents, not one

**The plan** is what the run is *supposed* to do: which parts run, in what
order, on what alignment. It is the same for every cycle of a well-behaved
run.

**The transcript** is what the run *actually did*, including the things that
were never planned — a cycle abandoned, a jump back to an earlier cycle, a
re-take.

Both already exist in the current implementation without those names. The
figure's operator-sequence legend (`A`, `B`, ...) is an **inferred plan**; the
rows are the **transcript**. A run in which every step is `A` followed the
plan. A `B` is a step that did something else.

That naming makes the original motivating question mechanical: *is the model
doing the scientific task you say it is* becomes **a diff of the transcript
against the plan**.

## Vocabulary

The terms below are where the ideas came from, and they are deliberately NOT
the words the code and the figures use. "Score", "bar", "note", "rest" and
"simile" were useful for getting the model right and are forced as public
vocabulary: what ships says **transcript**, **step**, **part**, **did
nothing**, and **unchanged**. The mapping is kept here because the reasoning
depends on it — each musical term carries a convention that is the reason the
corresponding decision was made.


| term | meaning here |
|---|---|
| **bar** → *step* | one turn of the orchestrating loop. Numbered monotonically, never reused, and the thing you refer to. NOT necessarily a physical time interval — it may be a task. |
| **beat** | a position inside a step at which alignment is required. **Barriers** sit on beats: a mesh deform, an adapt, a migration, a remesh. |
| **part** (kept) | a participant with its own column. Two kinds: *actors* (solvers, swarm pushes, mesh movers) and *state-holders* (DDt histories, fields, particle coordinates). |
| **note** → *a mark* | what a part did in a step, carrying its own duration — its `dt`, which need not be the bar's. |
| **rest** → *did nothing* | notated absence. Distinguishes *did nothing this bar* from *was not being watched*. |
| **tuplet** | n notes in the space of the bar, bracketed with the ratio. Sub-cycling, notated as ordinary rather than flagged as anomalous. |
| **tie** | a value written in one step and read in the next, drawn as an arc across the barline. |
| **tempo** | how step numbers map to real time. Deliberately separate from the meter: `dt` varies, wall clock varies more, and the vertical axis is ordinal. |
| **performance event** | not part of the piece: a bar abandoned, a jump back, a re-take. Transcript only. |

Two conventions borrowed with the vocabulary and worth keeping:

- **Stable part order.** Parts appear in a fixed order (actors, then
histories, then swarms, then mesh), not order of first appearance, so a
reader finds the same part in the same place in every run's transcript.
- **Absence is written down.** Every part accounts for every step. A part
that did nothing is marked as having done nothing, never left blank.

## Why two dimensions

Different parts advance on different `dt`. A swarm may sub-cycle twice for one
Stokes solve; two histories on the same field may be at different orders. Those
cannot be laid on one axis without pretending they share a clock — which is the
failure being looked for.

So: **parts across the page, bars down the page.** Vertical is ordinal, not
time. A part whose accumulated time drifts away from its neighbours' is then a
visible misalignment rather than something you would have to instrument for.

## What the notation makes checkable

Each check is a reading of the notation rather than a separate assertion:

| reading | defect |
|---|---|
| a tie with no note at its head | a read of uninitialised history |
| two notes tied into one read | the physical step taken twice |
| a tuplet whose ratio does not fill its bar | sub-cycling that fails to tile the interval |
| a tie crossing a beat that carries a barrier, with nothing re-expressing it | the `old_frame_traceback` class (#423) |
| transcript ≠ plan | the model is not doing what the script says |

**None of these belong in the loop.** An earlier version asserted one of them
there — "a history must advance exactly once per bar" — and it was wrong twice
over: it would fire on legitimate sub-cycling, and it could not see the check
that matters most, since #423's signature is a growth rate across bars. It has
been removed. Derived from the notation the rule is the honest one — **the
notes in a bar tile its interval exactly once** — and sub-cycling satisfies it.

The separation that follows: **execution records, analysis judges.** A step
raises only on structural failures, where the transcript could not be
well-formed — a step opened inside another step, a rewind to a bar that kept no
snapshot. Everything above is a *finding*, produced by a pass over a finished
transcript, which can look across bars, can be re-run on an old transcript when
a new pathology is learned, and never has to decide mid-run whether something
was deliberate.

## Where reads and writes come from

Feasibility, because this is the part that decides whether the model is
buildable:

- **Writes** come from the runtime hooks already in place: the unknown after a
solve, `psi_star[i]` in `update_post_solve`, particle coordinates after an
advection.
- **Reads** are derivable *symbolically*. A solve's residual is a SymPy form,
so the variables it depends on are in `F0` / `F1`'s atoms. No kernel
instrumentation.

Within one solve, reads and writes are not ordered — the kernel reads
`psi_star` throughout the Newton iteration — so the edge is *write → solve*,
not write → instant. The cell is atomic; the meaning is in the edges between
cells. That is exactly where the cross-cycle relation lives, so the limitation
does not bite.

## What is missing today

Everything above except two items is a renaming of something already captured,
which is a reasonable sign the vocabulary fits rather than being imposed.

1. **Barriers are not events.** `_deform_mesh` does not declare itself, so
there is no beat to draw the rule at — and the check that most wants the
barrier (#423) has no anchor without it.
2. **Rests are not recorded.** A part that does nothing in a bar simply does
not appear, so silence and absence are indistinguishable.

A third, smaller: **a bar is not necessarily an interval, and `model.step(dt)`
insists that it is.** The signature requires a `dt`, so there is no container
for "the next task". If the event clock is the general thing, the timestep is
the common case rather than the definition.

## Inferred plan, then declared plan

The plan is **inferred** today — the figure takes the most common step as the
norm. That costs nothing and can only ever say *this step differs from its
neighbours*.

A **declared** plan — the script stating what a step is supposed to contain —
turns the diff into *this run disagrees with its own description*, which is the
stronger claim and the one that motivated the work. The path is to work
towards the declarative model from the inferred one rather than to require it
up front; nothing in the vocabulary above depends on which we have.

## A note on names

"Transcript" rather than log or record: a transcript is what actually
happened, including the false starts and the re-takes, which is precisely the
thing being kept.

The API followed: what was `model.journal` is `model.transcript`, and the
renderers are named for it — `transcript_table` sets it in text,
`transcript_figure` draws it, `transcript_flowchart` draws the sequence it
implies. Nothing user-facing is called a score: the word belongs to the
derivation above, not to the shipped vocabulary.

"Record" is kept as a **verb**. A step records what it did; the thing it
produces is the transcript.
11 changes: 11 additions & 0 deletions docs/developer/guides/CODE-REVIEW-PROCESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ The code review process serves to:

## Review Process Workflow

### Phase 0: Adversarial review (before the PR opens)

Run the pass described in [adversarial-review.md](adversarial-review.md): an
independent attempt to refute the change, aimed at the failure modes this
codebase produces — rank asymmetry, frame and unit boundaries, determinism,
tests that cannot fail, CI reach, and the contracts a change must not quietly
leave. Fix what it finds, then open the PR and post the findings on it.

Every PR, including one-line bug fixes and docs-only changes; again after any
substantial post-review commit.

### Phase 1: Preparation

1. **Author**: Prepare change materials
Expand Down
Loading