You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Depends on: EDU-1 (#1607), ARCH-4 (#1563) · Blocks: —
Context:tutorials/ is the educational implementation of MACE — a top-level directory, a sibling of packages/ and the frozen legacy mace/. It is not a package and not the "reference backend" (that is the plain-torch/jax kernel oracle living inside the packages, mace_torch/backends/reference/ and mace_jax/kernels/reference.py). It exists so chemists and physicists can read and inspect the model blocks — pure-function JAX, readable over fast, evaluation/inspection-only: not a training path (there is no JAX training anywhere) and not a mirror of the latest architecture. EDU-1 (#1607) lays down the tutorials/ scaffold and mace_forward.py, a pure-function nanoGPT-style JAX MACE forward that computes energies with no closures. This ticket adds the pedagogy on top of it: marimo notebooks that walk through the equivariant math with inline LaTeX and a runnable forward/eval demo, plus a CI job that executes everything and pins a numerics cross-check against a real MACE forward so the tutorial can never silently drift from the model it teaches. This supersedes the hand-written theory Colab ("MACE theory and code (advanced)", the third detailed tutorial linked from the README). Nothing in packages/ depends on tutorials/; the dependency arrow runs one way (the cross-check test imports an oracle, never the reverse).
Notebooks are marimo, i.e. plain executable .py files (reactive notebooks), not .ipynb. They must run top-to-bottom headless in CI. Keep them import-light: they read tutorials/mace_forward.py and standard scientific-python (jax/numpy/matplotlib); they do not import packages/.
Math walkthrough (01_math_walkthrough.py) covers, in order, with derivations rendered as inline LaTeX (marimo mo.md(r"...")):
Spherical harmonics — the real (Y_\ell^m) basis used for edge directions, in the component normalisation the model uses.
Clebsch-Gordan coupling — how two irreps combine into an output irrep (prose says Clebsch-Gordan, never bare "CG"); tie this to the tensor-product convolution.
Message passing — one interaction layer as a scatter over the graph.
Many-body — the symmetric contraction up to correlation order as the many-body term.
Each section is small, self-contained, and executable (short numeric examples the reader can perturb).
Forward/eval demo (02_forward_demo.py): load a tiny model + a small structure, run mace_forward.py to get the energy, and display intermediate tensors so the reader can see the blocks light up. Eval/inspection only — no optimizer, no training loop, no forces (EDU-1 (EDU-1 — tutorials/ scaffold + pure-function JAX MACE forward (energies) #1607)'s forward exposes none).
Golden fixture and the edge list. The fixture is a tiny model + structure, committed (or committed as a reproducible recipe) so the cross-check is deterministic. The edge list is part of the fixture, not recomputed by the tutorial, and it is built the way mace.data.get_neighborhood builds it (mace/data/neighborhood.py:7-60), whose cell handling has three regimes the fixture must not straddle: the neighbour search always runs in an extended cell whose non-periodic rows are resized to the atom extent plus 2 * cutoff + 1; a fully aperiodic system returns that extended cell, deliberately, because long-range models need a non-degenerate box; a system with any periodic axis returns the physical cell, with only all-zero rows patched from the extended one, because stress and the electrostatic volume corrections divide by det(cell). Pin the fixture to a fully aperiodic molecule: the tutorial has no cell-dependent term, so the returned cell must not be able to influence the comparison at all.
Numerics cross-check (tutorials/tests/): assert the JAX educational forward reproduces a real MACE forward on the fixture to fp64 tolerance on energy. The oracle is versioned:
When ARCH-4 (ARCH-4 — Two-phase forward and derivative engine (forces via autograd, stress via strain) #1563) lands, the cross-check is repointed at mace_torch's two-phase forward and the legacy import is deleted. v1 reproduces develop's energies to the golden tolerances, so this is an oracle swap, not a tolerance change; the ticket closes on the v1 oracle.
The tolerance is a strict fp64 row, not an eyeballed epsilon. This test is the only file in tutorials/ allowed to import an oracle; mace_forward.py and both notebooks import none of them.
CI execution: a dedicated tutorials job in .github/workflows/ci-core.yaml (CPU-only, needs: lint, unconditional — GOV-1 (GOV-1 — CONTRIBUTING.md and the project governance document #1609)'s examples-must-not-break rule makes this a merge-time gate, and the job is cheap enough not to need a paths filter). It installs through .github/actions/setup-mace (extras: dev, plus pip-packages: -r requirements/tutorials.txt, a new pinned requirements file for jax[cpu] and marimo, following the requirements/les.txt / requirements/polar.txt convention for deps that are not extras) and runs .github/actions/run-tests with tests: tutorials/tests. Notebook execution is itself a pytest case, so one action invocation covers both. Two properties of that action apply here: tests/conftest.py governs only the tests/ subtree, so the capability contract is inert for tutorials/tests and require-caps stays empty; and --cov=mace is hardcoded in the coverage branch, so coverage stays "false".
Task:
Write tutorials/notebooks/01_math_walkthrough.py (marimo): spherical harmonics → Clebsch-Gordan coupling → message passing → many-body, each with inline-LaTeX derivation and a short runnable numeric example built on tutorials/mace_forward.py.
Write tutorials/notebooks/02_forward_demo.py (marimo): tiny-model forward/eval demo, surfacing intermediate block outputs; energy only, no training.
Commit the tiny golden fixture (fully aperiodic molecule + model + its edge list, or a reproducible recipe) under tutorials/tests/.
Add requirements/tutorials.txt (pinned jax[cpu], marimo) and the tutorials job in .github/workflows/ci-core.yaml wired as in §7.
Out of scope: the tutorials/ scaffold and mace_forward.py itself (EDU-1 (#1607)); any JAX training or run_train (there is no JAX training anywhere); mirroring the latest architecture (the tutorials are inspection-only, not a spec); the eval CLI and goldens inside packages/; every row marked DROP in EDU-1 (#1607)'s scope table.
Acceptance criteria:
tutorials/notebooks/01_math_walkthrough.py and 02_forward_demo.py exist as marimo notebooks and execute top-to-bottom headless with no error.
The math walkthrough renders inline-LaTeX derivations for spherical harmonics, Clebsch-Gordan coupling, message passing, and many-body, each with a runnable numeric example.
The forward demo runs the JAX forward on a tiny model and displays intermediate block tensors; no training/optimizer code is present.
A committed golden fixture drives a cross-check asserting the JAX forward matches a real MACE forward to fp64 tolerance on energy; the fixture is fully aperiodic and carries its own edge list.
A CPU-only tutorials job in ci-core.yaml executes both notebooks and runs the cross-check; a notebook that raises, or a cross-check outside tolerance, fails the job.
tutorials/ imports nothing from packages/; only the cross-check test imports an oracle (one-way arrow), and neither notebook does.
Verify:
# Marimo notebooks are executable Python; `marimo run` serves them and never exits,# so headless execution is a plain interpreter run.
python tutorials/notebooks/01_math_walkthrough.py
python tutorials/notebooks/02_forward_demo.py
python -m pytest tutorials/tests -v # notebook execution + fp64 cross-check
python - <<'PY' # only the cross-check may import an oracleimport ast, pathlibroots = {"mace", "mace_core", "mace_torch", "mace_jax"}offenders = []for path in pathlib.Path("tutorials").rglob("*.py"): if path.name == "test_cross_check.py": continue tree = ast.parse(path.read_text()) mods = [n.module or "" for n in ast.walk(tree) if isinstance(n, ast.ImportFrom)] mods += [a.name for n in ast.walk(tree) if isinstance(n, ast.Import) for a in n.names] offenders += [(str(path), m) for m in mods if m.split(".")[0] in roots]assert not offenders, offendersprint("one-way arrow: OK")PY
Review focus: pedagogical correctness of the math (a physics reader should recognise the derivations) and that the cross-check tolerance is a strict fp64 row, not loosened to hide drift; that the fixture is fully aperiodic so no cell convention can leak into the comparison; confirm the one-way dependency (packages/ never imports tutorials/, and only the cross-check imports an oracle).
Depends on: EDU-1 (#1607), ARCH-4 (#1563) · Blocks: —
Context:
tutorials/is the educational implementation of MACE — a top-level directory, a sibling ofpackages/and the frozen legacymace/. It is not a package and not the "reference backend" (that is the plain-torch/jax kernel oracle living inside the packages,mace_torch/backends/reference/andmace_jax/kernels/reference.py). It exists so chemists and physicists can read and inspect the model blocks — pure-function JAX, readable over fast, evaluation/inspection-only: not a training path (there is no JAX training anywhere) and not a mirror of the latest architecture. EDU-1 (#1607) lays down thetutorials/scaffold andmace_forward.py, a pure-function nanoGPT-style JAX MACE forward that computes energies with no closures. This ticket adds the pedagogy on top of it: marimo notebooks that walk through the equivariant math with inline LaTeX and a runnable forward/eval demo, plus a CI job that executes everything and pins a numerics cross-check against a real MACE forward so the tutorial can never silently drift from the model it teaches. This supersedes the hand-written theory Colab ("MACE theory and code (advanced)", the third detailed tutorial linked from the README). Nothing inpackages/depends ontutorials/; the dependency arrow runs one way (the cross-check test imports an oracle, never the reverse).Interface & constraints:
tutorials/, established by EDU-1 (EDU-1 —tutorials/scaffold + pure-function JAX MACE forward (energies) #1607)):Notebooks are marimo, i.e. plain executable
.pyfiles (reactive notebooks), not.ipynb. They must run top-to-bottom headless in CI. Keep them import-light: they readtutorials/mace_forward.pyand standard scientific-python (jax/numpy/matplotlib); they do not importpackages/.Math walkthrough (
01_math_walkthrough.py) covers, in order, with derivations rendered as inline LaTeX (marimomo.md(r"...")):correlationorder as the many-body term.Each section is small, self-contained, and executable (short numeric examples the reader can perturb).
Forward/eval demo (
02_forward_demo.py): load a tiny model + a small structure, runmace_forward.pyto get the energy, and display intermediate tensors so the reader can see the blocks light up. Eval/inspection only — no optimizer, no training loop, no forces (EDU-1 (EDU-1 —tutorials/scaffold + pure-function JAX MACE forward (energies) #1607)'s forward exposes none).Golden fixture and the edge list. The fixture is a tiny model + structure, committed (or committed as a reproducible recipe) so the cross-check is deterministic. The edge list is part of the fixture, not recomputed by the tutorial, and it is built the way
mace.data.get_neighborhoodbuilds it (mace/data/neighborhood.py:7-60), whose cell handling has three regimes the fixture must not straddle: the neighbour search always runs in an extended cell whose non-periodic rows are resized to the atom extent plus2 * cutoff + 1; a fully aperiodic system returns that extended cell, deliberately, because long-range models need a non-degenerate box; a system with any periodic axis returns the physical cell, with only all-zero rows patched from the extended one, because stress and the electrostatic volume corrections divide bydet(cell). Pin the fixture to a fully aperiodic molecule: the tutorial has no cell-dependent term, so the returned cell must not be able to influence the comparison at all.Numerics cross-check (
tutorials/tests/): assert the JAX educational forward reproduces a real MACE forward on the fixture to fp64 tolerance on energy. The oracle is versioned:mace—ScaleShiftMACE,default_dtype=float64,compute_force=False. The notebooks therefore land pinned, not on an inert assertion that activates two phases later.mace_torch's two-phase forward and the legacy import is deleted. v1 reproduces develop's energies to the golden tolerances, so this is an oracle swap, not a tolerance change; the ticket closes on the v1 oracle.The tolerance is a strict fp64 row, not an eyeballed epsilon. This test is the only file in
tutorials/allowed to import an oracle;mace_forward.pyand both notebooks import none of them.CI execution: a dedicated
tutorialsjob in.github/workflows/ci-core.yaml(CPU-only,needs: lint, unconditional — GOV-1 (GOV-1 — CONTRIBUTING.md and the project governance document #1609)'s examples-must-not-break rule makes this a merge-time gate, and the job is cheap enough not to need a paths filter). It installs through.github/actions/setup-mace(extras: dev, pluspip-packages: -r requirements/tutorials.txt, a new pinned requirements file forjax[cpu]andmarimo, following therequirements/les.txt/requirements/polar.txtconvention for deps that are not extras) and runs.github/actions/run-testswithtests: tutorials/tests. Notebook execution is itself a pytest case, so one action invocation covers both. Two properties of that action apply here:tests/conftest.pygoverns only thetests/subtree, so the capability contract is inert fortutorials/testsandrequire-capsstays empty; and--cov=maceis hardcoded in the coverage branch, socoveragestays"false".Task:
tutorials/notebooks/01_math_walkthrough.py(marimo): spherical harmonics → Clebsch-Gordan coupling → message passing → many-body, each with inline-LaTeX derivation and a short runnable numeric example built ontutorials/mace_forward.py.tutorials/notebooks/02_forward_demo.py(marimo): tiny-model forward/eval demo, surfacing intermediate block outputs; energy only, no training.tutorials/tests/.tutorials/tests/test_notebooks.py, which executes each notebook as a script and fails on any raise, andtutorials/tests/test_cross_check.py, which compares the JAX forward against the oracle at fp64 — legacymacetoday,mace_torchonce ARCH-4 (ARCH-4 — Two-phase forward and derivative engine (forces via autograd, stress via strain) #1563) lands.requirements/tutorials.txt(pinnedjax[cpu],marimo) and thetutorialsjob in.github/workflows/ci-core.yamlwired as in §7.Out of scope: the
tutorials/scaffold andmace_forward.pyitself (EDU-1 (#1607)); any JAX training orrun_train(there is no JAX training anywhere); mirroring the latest architecture (the tutorials are inspection-only, not a spec); the eval CLI and goldens insidepackages/; every row marked DROP in EDU-1 (#1607)'s scope table.Acceptance criteria:
tutorials/notebooks/01_math_walkthrough.pyand02_forward_demo.pyexist as marimo notebooks and execute top-to-bottom headless with no error.macefrom the day it lands, and againstmace_torchonce ARCH-4 (ARCH-4 — Two-phase forward and derivative engine (forces via autograd, stress via strain) #1563) exists — the ticket does not close on the legacy oracle.tutorialsjob inci-core.yamlexecutes both notebooks and runs the cross-check; a notebook that raises, or a cross-check outside tolerance, fails the job.tutorials/imports nothing frompackages/; only the cross-check test imports an oracle (one-way arrow), and neither notebook does.Verify:
Review focus: pedagogical correctness of the math (a physics reader should recognise the derivations) and that the cross-check tolerance is a strict fp64 row, not loosened to hide drift; that the fixture is fully aperiodic so no cell convention can leak into the comparison; confirm the one-way dependency (
packages/never importstutorials/, and only the cross-check imports an oracle).