Skip to content

0.6.4 — the alchemical transformation, as far as it actually goes

This release makes alchemical free energies work from the command line and checks the answer against something outside the code. combine-topology writes a plan, build-md generates a runnable ladder from it, run.sh runs the windows on as many cards as you have, and MBAR turns the samples into a free energy. The result has been validated two ways: against an experimental number, and against a completely different alchemical route through the same thermodynamics.

No existing protocol changes. cMD, REST2, AIS and umbrella are untouched. protocol: alchemical is a new value; every configuration that resolved in 0.6.3 resolves identically.

protocol: alchemical resolves

build-md accepts an alchemical section:

protocol: alchemical
alchemical:
  cycle: RHFE                     # RBFE, RHFE, ABFE or AHFE -- names the run directory
  leg: 1                          # -> RHFE-leg1-run<N>/ and input/RHFE-leg1.in
  plan: ./plan                    # what `md-openmm combine-topology` wrote
  lambda_path: linear             # `staged` is refused; see below
  number_of_windows: 12           # or lambda_values: 0.0, 0.25, 0.5, 0.75, 1.0
  window_steps: 500000
  equilibration_steps: 10000
  report_interval_steps: 500
  checkpoint_interval_steps: 50000

and md-run parses the matching &alchemical block. Every .in round-trips back to exactly the resolved.config beside it, both ways of placing the windows included — the same contract the other four protocols keep, and a test rather than a convention.

Nothing in the section restates a fact the plan already holds. The two end states, the atom map, the environment and the combined numbering are in the plan, which carries its own digests and is validated where it is LOADED. A second copy here would be the one that drifts.

lambda_path: staged is REFUSED, and the reason changed during this cycle. It used to be a missing scientific decision — where the bonded components go. That is now settled: they are not staged (see below). What blocks it is an implementation gap, and a different one: a faithful staged path is directional. OpenFE's default discharges the disappearing region while growing the appearing one's core, then removes the old core while charging the new, which needs each region labelled as inserting or deleting. This layer moves one lambda_electrostatics and one lambda_sterics for both regions together, so a two-stage split here would read as OpenFE's default without being it — and would leave a charged region with no core at one end, the singularity staging exists to prevent. linear — the Amber18 one-step diagonal — is what the campaigns use.

The arithmetic a staged path needs is already written and held as five xfail(strict=True) tests: the day it is wired they XPASS and the lane fails, which is how the next person finds the arithmetic instead of deriving it again. A window must sit exactly on the knot — the slope of every λ component jumps there, so dU/ds has two values and no trapezoid drawn across it recovers the integral; a count whose grid misses it is refused with the arithmetic that would fix it.

Value-level refusals are delegated, not restated. SoftcoreSettings, WindowSettings and md_tools.alchemy.paths enforce their own rules at run time, and the configuration layer hands the resolved values to them. A configuration therefore cannot accept a setting the node will refuse three stages of equilibration later, nor refuse one the runtime would have accepted.

And both surfaces run one

build-md generates the run directory and md-run dispatches into it. One dispatch, md_tools.alchemy.generated.window_main, reached from both:

python w000.py                                              # one window
md-openmm md-run -i ../input/alchemical.in -odir . --window w000
./run.sh                                                    # all of them

A generated directory holds resolved.config, the plan copied in and content-addressed, leg/ (whose System is built ONCE at generation, with its digest checked at every window), one two-line entry point per window, and run.sh. No -p and no -s: a window reads leg/, and offering a ../build path would be a second System for the same windows.

For most of this cycle both surfaces REFUSED instead, rather than fall through to the preparation chain — which would have produced a run.sh that minimised, equilibrated and stopped, a complete-looking run that sampled nothing. Those two refusals were the acceptance criteria for the work that replaced them, and the tests that asserted them now assert what they became.

An input naming a preparation stage is unaffected: stage decides, not protocol, and minimisation is minimisation.

mode: decoupling, and a tutorial that runs

combine-topology had three modes -- single, hybrid, dual -- all of which turn one ligand into another. build_decoupling_plan had existed in the library since the plan builder did, so the simplest alchemical calculation there is, removing one molecule from water, was reachable only from Python. It is now a mode:

mode: decoupling
endpoints:
  A: {parameters: ./build/parameter}

Endpoint B is the ligand ABSENT, so endpoints.B, b_pose, map.file, map.automatic and dual.restraint_k_kj_mol_nm2 are each refused BY NAME. A map silently ignored here is a mutation somebody wrote and set the wrong mode on, and it would run as a decoupling without a word. The CLI calls build_decoupling_plan rather than build_topology_plan(mode="decoupling") precisely because the net-formal-charge refusal lives in the former -- a charged ligand changes the box's net charge, and PME's neutralising background then needs a finite-size correction this release does not implement. The plan builds either way, so reaching past that entry point would lose the check silently; a test asserts the refusal survives the trip through the configuration layer.

The CLI plan is bit-identical to the one the validation campaign ran (plan_sha256 29c233c0... from both paths). That is the test that matters: a CLI building a merely similar plan would make the tutorial's commands describe a different calculation from the one the published numbers came from, and nothing would say so.

Two defects came out of insisting the tutorial be runnable as written, neither of which any test would have caught:

  • build-md crashed on every decoupling plan. A decoupling's endpoints.B.reference is None, and that went into the path constructor: PathError: a path names both endpoints, raised AFTER the plan had been copied into the run directory, naming neither the plan nor the mode, for a configuration that was entirely correct. A decoupling's ends are now coupled / decoupled -- the names md_tools.alchemy.cycles already uses, so a generated leg is comparable with the campaign's records.
  • build-md's own log printed end states ... -> None, and LogWriter.field used {key:<28}, which silently concatenates when a key is longer than the column: plan.b663f7bf42da/combined.pdbmd_script/plan... read as one path in the log a person checks a generated run against.

New tutorial: ethanol / hydration free energy (tutorial removed in 0.6.5), run as written in 31 minutes on four cards. ΔG_hyd = −3.648 ± 0.107 kcal/mol against the campaign's three-repeat −3.523 ± 0.025 -- the same number, 0.125 apart against a single-run σ of 0.107.

Its most useful section was not planned. TI gives −4.195 while MBAR, BAR and both EXP directions sit between −3.62 and −3.84. Not a bug: TI trapezoids ⟨∂U/∂λ⟩ through twelve points, and near s = 1 it varies too sharply for a 1/11-wide gap to follow. So the page teaches what estimator disagreement DIAGNOSES -- TI away from MBAR means the λ spacing, EXP directions apart means the exponential average is carried by its tail, MBAR away from BAR means the samples themselves. Four estimators over identical samples are four probes of one measurement, and their pattern is the diagnostic.

tests/test_tutorial_hydration.py lifts the page's YAML blocks out and resolves them through the real resolvers, and checks the headline number against the campaign it claims agreement with. It is the first test in the suite that does this to a tutorial; all three of its guards were shown to fail before being trusted.

Five λ components, named as OpenFE names them

The bonded degrees of freedom were one lambda_bonded. They are now lambda_bonds, lambda_angles, lambda_torsions beside lambda_electrostatics and lambda_sterics — the names OpenFE's LambdaProtocol and openmmtools.alchemy.AlchemicalState both use, adopted deliberately. MD-tools imports nothing from OpenFE and nothing here needs it installed.

A shared vocabulary is what makes a per-component comparison with an independent implementation possible at all: one coarse component could only ever be compared against the sum of their three. And the split immediately found three places the coarse parameter had been hiding something.

  • Both finite-difference tests walked a 3-vector and perturbed one slot. Moving "the bonded slot" moves bonds, angles and torsions together, and compared that against ONE component's analytic partial: on a plan whose torsions differ and whose bonds do not, dU/dλ_bonds = 0 against a finite difference of 660.77 kJ/mol. They now perturb one key at a time, so five partials are checked independently where three were.
  • The independent reference summed all three families under one λ, so it could only agree where the three were equal — exactly the case a check moving one of them does not exercise. It would have passed the diagonal and silently stopped checking the off-diagonal.
  • _MIXABLE_BONDED is now derived from the parameter map, because a mixable force kind with no parameter builds a mixing force nothing moves: terms frozen at endpoint A, no derivative, nothing refused.

The bonded components are not staged, and that settles a question this repository had left open. OpenFE's default moves them as lambda x: x, linear across the whole path; staging exists to avoid a Coulomb term on a particle whose repulsive core has gone, and bonded terms have no such singularity. Two placements had been argued internally, each defensible with no external support. One agrees with OpenFE — and that agreement is the evidence, worth more than the argument that reached it.

Two divergences are recorded as deliberate rather than left to look like oversights: the Amber18 softcore (theirs is a closed Literal["gapsys", "beutler"]; ours is what pmemd integrates, so the AMBER comparison is like-for-like — and an OpenFE result is therefore an independent check rather than a reimplementation), and the dispersion correction (they default it off; turning it off in the alchemical System alone would make the end states disagree with what build-top produced). Full reasoning and sources in scientific defaults §13.

A run directory is <cycle>-leg<i>-run<N>

alchemical.cycle — RBFE, RHFE, ABFE, AHFE — and alchemical.leg name every alchemical run, exactly as every other protocol's runs are <method>-run<N>. An alchemical method is <cycle>-leg<i>, so a leg's repeats sort together under the cycle they belong to and a directory says what it is without being opened.

<system>/
  input/RHFE-leg1.in                  the solvent leg, shared by its repeats
  input/RHFE-leg2.in                  the vacuum leg
  RHFE-leg1-run1/ RHFE-leg1-run2/ RHFE-leg1-run3/
  RHFE-leg2-run1/ RHFE-leg2-run2/ RHFE-leg2-run3/

The shared input is per LEG. input/ belongs to a system, and two legs of one cycle resolve to different bytes — different plans, usually different window grids. Before this, that collision was answered by telling people to split the legs into separate systems, which was a workaround and is removed. Two legs belong under one system; the leg index is what keeps them apart.

The cycle is declared, not derived. An RBFE's solvent leg and an RHFE's solvent leg are the same calculation on the same box; what distinguishes them is the OTHER leg. Deriving the cycle from one leg would be guessing from evidence that does not contain the answer.

Four refusals, each before anything is written: an -odir that does not match the expected name (which it quotes); cycle without leg, or neither; a shared-input collision, which can now only mean two repeats disagreeing; and — the one that matters — an absolute cycle on a plan that mutates, or a relative cycle on a decoupling. A run mislabelled RBFE would be filed with the edges, combined by relative_binding, and produce a number that is not a binding free energy of anything, with nothing downstream able to tell: both are legs with windows and samples.

Relative hydration, and a second tutorial

New: ethane → chloroethane / relative hydration (tutorial removed in 0.6.5), mode: hybrid, six ladders, 102 windows, 102 ns.

ΔΔG_hyd = −1.746 ± 0.020 kcal/mol against the 0.7.0 M2 campaign's −1.743 ± 0.026.

The page is explicit that this is not the independent check. Both runs share the Hamiltonian, the window grid and the lengths; only the driver differs, so agreeing to 0.003 establishes that the generated directory is faithful and nothing more. The independent comparison is the decoupling route's −1.639 ± 0.074 — a different plan mode, a different Hamiltonian, one leg per molecule instead of two, no atom map at all — which sits 0.107 away, inside its own error bar.

Two structural points the page teaches because the run taught them:

  • The vacuum leg is not zero here. In a decoupling the ligand's own Hamiltonian is retained at both ends, so the vacuum leg is λ-independent and is not run. In a mutation the molecule itself changes, so it must be sampled — and it is extraordinarily reproducible: three seeds gave −1.6389, −1.6384, −1.6389, a spread of 0.0005 kcal/mol. All of the run-to-run scatter comes from the water (solvent spread 0.068).
  • A repeat is a run directory, not a name inside one. A generated run always writes r1; two repeats sharing a directory would be two experiments over one set of paths. dynamics.seed is deliberately not written into the .in, which is why three repeats share one input while two legs cannot.

matched_legs runs before a sample is read, and refuses two legs that do not carry one ligand Hamiltonian. Without it, a vacuum leg from one parameterisation and a solvent leg from another would subtract just as cleanly and mean nothing.

What the runtime has been shown to compute

New page: alchemy/validation.md.

Three FreeSolv molecules decoupled from TIP3P water, twelve windows, 1 ns each, three repeats — nine legs, 108 windows, 108 ns on CUDA/mixed:

ligand ΔG_hyd, kcal/mol ± (repeat spread) experiment
ethane +2.478 0.033 +1.83
chloroethane +0.839 0.067 −0.63
ethanol −3.523 0.025 −5.00

Ethanol is in the set because its hydration is dominated by the charging leg, which the two weakly polar molecules barely exercise.

The check that matters is not agreement with experiment — a force field can miss that for reasons that are not defects. It is that ΔG_hyd(chloroethane) − ΔG_hyd(ethane) obtained by DECOUPLING each reproduces the same difference obtained by MUTATING one into the other: a different plan, different end states, different particles appearing.

route ΔΔG_hyd, kcal/mol
decoupling each ligand −1.639 ± 0.074
mutating one into the other −1.743 ± 0.026

0.104 ± 0.079 apart, or 1.3 σ. Consistent.

A number this page nearly shipped

The first repeat alone gave −1.759 against −1.743 — an agreement to 0.016, and it was written into the validation page as the headline before the other repeats finished.

It was a coincidence of one realisation. Chloroethane's three repeats span 0.747 to 0.969, so any single one lands about 0.1 from the mean by luck in either direction. An agreement much tighter than the reproducibility of the two things being compared is not a stronger result; it is a sign that not enough of them have been run. The caution is kept in the page beside the corrected number rather than quietly replaced by it.

Two related corrections in the same page: the error bar is the REPEAT SPREAD, not MBAR's σ (which is the within-run error at fixed sampling and cannot see what differs between realisations); and an earlier claim that the spread exceeds the within-run error came from two points and is false at three — spread/σ is 0.4 to 1.2, comparable, neither dominant.

The self-check record carries its sample count

evaluation_self_check_kJ_mol is the largest disagreement between the evaluation Context and the sampling Context over every report a window made. As a worst-case witness that is right; as a number to compare BETWEEN windows it is not, because it grows with how many reports there were. A longer window looks worse for being longer, and a plot of it across a ladder measures window length.

The completion record therefore carries evaluation_self_check_n and evaluation_self_check_median_kJ_mol beside it, and is version 2. Not a silent addition: two shapes answering to one schema name is what a version string exists to prevent. verify_window reads the fingerprint and the two digests and nothing else, so a version 1 window still verifies and is not re-run.

The count also makes the resume scoping visible — a continued window's reporter starts fresh, so all three numbers describe only the evaluations since the resume, which was already true of the max alone and silently.

Also

Minimisation is not deterministic on CUDA, and six places said it was. Measured on the softcore System: single-point energies and forces are bit-identical across ten fresh Contexts — spread exactly 0.000e+00 — and five minimisations from that identical start scatter by 16 kJ/mol in mixed precision and 24 in double. Minimisation takes no seed, so nothing can be set to make it agree.

The premise everyone had been reading was true — "minimisation draws no velocities and has no seeded stochastic element" — which is exactly why the false conclusion drawn from it survived so many readings. A false statement gets caught; a true premise carrying a false conclusion does not. Three sites drew that conclusion and are corrected; three use only the premise to say the minimisation seed feeds no sampling, which is still right. Found by grepping the STATEMENT rather than the file.

The correction strengthens the design: sharing one min/ turns out to be load-bearing rather than tidiness — it is what makes every run start from the same minimised coordinates instead of from coordinates that ought to be the same. And run-layout.md's "two runs sharing an identical input and an identical seed would be bit-identical" is now marked as intent on a card and a guarantee on CPU and Reference, the same class of statement as the AIS resume.

Consequences worth carrying: a window with minimize_iterations > 0 is not seed-reproducible on a card, and two --cpu runs agreeing says nothing about a card.

  • The alchemy README said there is no protocol: alchemical in build-md. It was false the moment that landed, and now states the halfway point precisely.
  • Twelve documentation records linked to a roadmap that moved to the archive repository; the links now name where it went.
  • CLAUDE.md named three runtime entry points where there are four, and described a plan nothing consumed. Both corrected.
  • LogWriter.field used {key:<28}, which silently concatenated a long key into its value: the alchemical Outputs section printed two paths reading as one.

md_tools.analysis: three estimators for what a run produced

A new public package, behind an analysis extra (scikit-learn, rdkit), imported lazily so import md_tools costs nothing without it and a missing extra is reported by name rather than as a traceback from inside a fit.

needs
t_hdbscan a density partition of a torsional ensemble scikit-learn
t_mi mutual information between torsions, with its own bias measured numpy
t_symmetry which of those clusters are the same conformer rdkit

The estimators were written by the hpREST2 project and ported with that session's agreement; their 35 contract tests came across unmodified except for the import line, and pass.

Extracted mechanically, not copied. t_hdbscan sits on a periodic-metric layer inside a ~1600-line module that also holds a redundancy matrix, MI surrogates and a sensitivity scan. Rather than vendor it, the seven functions it needs were resolved to their transitive closure by AST — 22 names — and the MI layer fell OUTSIDE that closure. So the claim that the estimators are independent is established by construction rather than by reading, and a test asserts the metric layer never mentions the names left behind.

allow_single_cluster defaults to True, unlike sklearn. With it False, HDBSCAN's excess-of-mass selection refuses the root of the condensed tree, so a unimodal ensemble cannot come back as one state — it is split with a plausible mass split and nothing saying one state was never a candidate. The cost points the other way: True lets a genuinely multi-state ensemble collapse into one cluster. True is still better because the errors are not equally visible — one state reported where there are three looks wrong and prompts a check; three where there is one looks like a result. Over-splitting is the harder error to notice. Decided independently by this package's owner and hpREST2's on the same day.

The MI estimator measures its own bias. The plug-in histogram estimator is biased upward, so an independent pair returns a small positive number that reads as a weak coupling. On paracetamol the pair with the LARGEST raw MI is the one that is entirely bias:

pair raw debiased excess over null
omega–aryl 0.0334 +0.0206 +9.6 σ
aryl–phenol 0.0462 +0.0013 +0.4 σ
omega–phenol 0.0117 −0.0027 −1.2 σ

There is no boolean verdict field: it would collapse +9.6 σ and +0.4 σ into one answer, and +0.4 σ is exactly where a reader needs the number.

Two kinds of unassigned frame, and they are different claims. t_hdbscan returns -2 for DENSITY noise — the frame's neighbourhood is thinner than any clustered frame's, "nowhere in particular" — and -1 for a BARRIER frame, where the vote gate's k neighbours disagree about which cluster it belongs to, "between two somewheres". They were one code, which discarded one of two verdicts: cross-tabulated on a two-basin fixture, 151 frames were sparse yet UNANIMOUS and 4 were dense yet SPLIT, and the single code silently committed 55 % of HDBSCAN's noise frames to clusters. Density takes precedence where both apply, because reporting an isolated frame as a barrier reads as a transition state — a claim about a path.

The query-side density test is a PROXY and says so: HDBSCAN's noise label is a statement about the condensed tree and exists only for fitted frames, while predict must judge frames the fit never saw. summary() reports density_proxy_agreement as a measured number — 99.98 % on one ensemble, 88.99 % on another — rather than asserting the proxy reproduces the tree. A known objection is recorded with it: the threshold is a MAX over the clustered training frames, which is a one-frame statistic that moves with sample size in a direction you cannot sign; a 99th percentile is more stable and more accurate on one of the two ensembles measured. Left as a parameter decision rather than changed silently.

This moved numbers, which is the point: frames the fit called noise are no longer absorbed into clusters. Paracetamol's groups went 0.5145/0.4842 to 0.5140/0.4838 with the unassigned mass rising 0.00125 to 0.00225.

t_symmetry prunes clusters, and refuses to over-prune. Candidates are RDKit graph automorphisms respecting element, bond order, formal charge, isotope and specified stereochemistry; acceptance needs agreement of the JOINT distributions in both directions, by energy distance on the periodic embedding plus coverage asked each way so a subset cannot pass as an equivalence. Tolerances are calibrated from within-cluster split-half variability of the data at hand. Transitivity is not assumed — a connected component is re-tested against its reference. Masses sum once across a merge; nothing is multiplied by the symmetry order.

On paracetamol: four clusters prune to two groups, A at mass 0.5140 and B at 0.4838 with unassigned mass 0.00225, summing to exactly 1. Not all four merge — one pairing has the two torsions together, the other has them opposite, and those are different relative orientations. The relating operation is the para ring flip, and it is the COUPLED case: it moves aryl and phenol together, and both transformed quadruplets fall outside the torsion table, so it is reported unresolved without coordinates rather than approximated by a 180° shift.

Six analysis notebooks, executed

Under <system>/analysis, committed WITH their outputs so a reader sees the results without running anything, and a new Methods/Analysis page for the theory — covering only the three tools above, since RMSD, MBAR, WHAM and kinetics are standard methods explained where they are used.

ALA/analysis/pmf-1d.ipynb              phi, cMD against the REST2 cold rung
ALA/analysis/pmf-2d.ipynb              the Ramachandran surface the 1D projection averages away
paracetamol/analysis/pmf-1d.ipynb      three torsions, cMD against REST2
paracetamol/analysis/clustering.ipynb  1. clustering 2. pruning 3. summary
paracetamol/analysis/mutual-information.ipynb   t_mi, its bias, and the dependence graph
ethanol/analysis/free-energy.ipynb     MBAR/BAR/TI/EXP over one prepared leg

Roots come from the environment (MD_TUTORIAL_RUNS, MD_ALCHEMY_LEG) and a notebook that cannot find one refuses by name with the layout it expects. Executing them put a machine path in all six — the roots are read from the environment precisely so nothing is committed, and then each notebook PRINTED the resolved value, which execution baked into the file. Harmless in a script, a rule violation in a published notebook. The cells confirm without echoing now.

environment-analysis.yml describes the analysis environment, separate from the run environment for a measurable reason: pymbar 4 imports JAX, and a CUDA jaxlib preallocates most of the memory of every visible device at import — on a shared machine, a colleague's run taken by an analysis that needs no accelerator. The file pins the CPU build, verified as shipped: jax.devices() returns [CpuDevice(id=0)] on a host with nine GPUs.

No generated ladder had ever been extendable

Extending the paracetamol tutorial's 10 ns ladder by four more chunks found three defects in a row, each enough on its own to make --extend-from unusable from a build-md tree — which is the only kind of tree anybody has.

The manifest name was assumed. parent_files looked for <parent>/restart.json and nothing else, while run.sh files its segments as remd_records/restart_prod<N>.json. The assumed name is now a default and --extend-manifest names another.

compare_identity took the Hamiltonian compatibility branch for v2 only and compared every other format WHOLE. Correct when its docstring was written — "every v3 entry is still compared whole" — and false the moment v3 was bumped to v4 in this release: format alone then refused every 0.6.1/0.6.2/0.6.3 parent, with the function that says they agree one call away.

And routing v3 through the branch still refused, because _v3_selection_sha256_candidates always stamped detector_policy_version into the projection it hashed. A legacy-full-solute document never carried that key, so v3 hashed it without one and wrote exactly the digest v4 writes — and the reconstructed stamped digests were all wrong ones. The real parent and the fresh record agreed on selection_sha256 character for character and were refused on that field.

The chain then ran: four chunks, 50 ns, each segment's restart.json pinning its parent by the sha256 of that parent's manifest, checkpoint, analysis file and all four state trajectories, with step counts chaining 5M → 25M and acceptance stable at 0.2216 / 0.2255 / 0.2257 / 0.2256.

Tutorial section 8 is rewritten around it, including the asymmetry the run found: the original run files its manifest under remd_records/ and an extension writes restart.json at its own root, so only the FIRST chunk needs the flag. The section previously told the reader to use md-openmm md-run --extend-from, which fails in argparse — md-run defines neither flag. A test now asserts every long flag the section's shell blocks pass is one the generated ladder accepts.

Also, in this release

  • Sharing a card inside one launch is an MPS question, not a rank count. The coverage matrix refused more ranks than visible devices, which refuses a legitimate six-state ladder on three cards and permits the thing worth refusing — several ranks of different projects on one card, which no rank count can see. The guard now asks whether MPS is running.
  • The umbrella method README's window loop built one window N times and reported success. It edited the restraint into w.yaml while the config names umbrella.yaml, so two windows came out with the same content-addressed digest, both biased at −60°, while the loop claimed −150 and −120. Every run reported status: completed and the profile was N copies of one window. Split out of the umbrella session's commit, whose tutorial waits for 0.6.5.
  • The paracetamol REST2 page said the ring rotates. It does not: the scaler protects all six aromatic ring bonds plus the amide omega — 7 central bonds, 56 torsion terms unscaled against 18 scaled. The torsion that IS heated is 1-3-4-5, an ordinary torsion about the exocyclic C(aryl)–N bond, which is not part of the ring.
  • A page promised 0.6.1 for a section that cannot run at 0.6.1. Section 8's header claimed every command came from a run at that version while the section itself explains that before 0.6.4 no generated ladder was extendable. The header carries the exception by name now.
  • A library that switches the global matplotlib backend hijacks the caller's session. Two drawing functions called matplotlib.use("Agg"), which silently disables the inline backend in a notebook — every bare figure afterwards produces no image and no error. Found because the notebooks are executed and their figures counted.
  • A count repeated from a peer and never measured here. Three places said a unimodal Gaussian returns 3 clusters with the flag off. That is another tree's number; measured here it is 2. The count is fixture- and version-dependent, the splitting is not.

Evidence

Fast lane: 2918 passed, 3 skipped, 5 xfailed, 0 failed, 615 deselected.

tests/test_alchemy_windows_cuda.py on CUDA with the record change: 5 passed.

The softcore Hamiltonian's CUDA lane carries three known failures, deliberately, decided 2026-09-19: precision on a 2.4×10⁴ kJ/mol overlap term at λ = 1. Re-run here five times on a different card and confirmed bit-identical to the 2026-09-21 evidence — the same energy, the same derivative and the same force, digit for digit, which is what says they are deterministic arithmetic rather than drift. A fourth failure appeared in one run of five, in the file that propagates: its Langevin integrator and barostat are unseeded, so the configuration it compares at differs every run. That one is not evidence of anything in either direction until it is seeded.