Fix forward/reverse convergence analysis: slice by time, not by replica#2059
Draft
aqemia-benedict-tan wants to merge 20 commits into
Draft
Fix forward/reverse convergence analysis: slice by time, not by replica#2059aqemia-benedict-tan wants to merge 20 commits into
aqemia-benedict-tan wants to merge 20 commits into
Conversation
…n where MBAR fails
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
I recently benchmarked the default$R^2=0.50$ , $RMSE=1.01$ kcal/mol), and the overlap matrices and replica exchange statistics looked healthy. However, the convergence behaviour appeared to be problematic, showing large fluctuations in both the forward and reverse estimates. The convergence plots generated for the solvent and complex leg for the ejm_47 ligand are shown below (input is ejm_47.json, raw results are given here: results.json).
AbsoluteBindingProtocolfor the TYK2 JACS dataset. The overall performance was strong (Solvent leg
Complex leg
Interestingly, the real time analysis (complex_real_time_analysis.yaml, solvent_real_time_analysis.yaml) indicated satisfactory convergence for both the solvent and complex leg (within 1 kcal/mol at >0.5 fraction of uncorrelated samples):
I inspected how$\Delta G$ estimates for calculations that are actually well converged.
MultistateEquilFEAnalysis.get_forward_and_reverse_analysisgenerates the forward and reverse estimates, and it seems there is a bug in how the MBAR input matrix is sliced for the individual fractions < 1.0, that can result in large fluctuations in the partial-fractionThe current code feeds slices with increasing number of columns/samples of the
u_lnmatrix obtained byself.analyzer._unbiased_decorrelated_u_lnto obtain DG/dDG estimates at increasing fraction of uncorrelated samples. This approach assumes that the columns are ordered per unit of time, so adding extra columns to your estimate corresponds to adding an increasing number of samples collected across all replicas.self.analyzeris anopenmmtoolsMultiStateSamplerAnalyzer;_unbiased_decorrelated_u_lnis one of its cached properties, ultimately built by_compute_mbar_decorrelated_energies. The 3D -> 2D reshape inopenmmtoolsis handled by thereformat_energies_for_mbarmethod as shown below:This flattening does not preserve the time-ordering of samples along the column axis; it groups them per replica (
column = replica * M + iteration, whereMis the number of decorrelated samples per replica). So at 50 % of the uncorrelated samples,u_ln[:, :u_ln.shape[1] // 2]selects all samples from half of the replicas and none from the other half. On its own that subset would still be usable — MBAR is permutation-invariant in its columns — but the function also passes MBAR a uniform per-state count (new_N_l = [chunk] * n_states) that the slice does not actually have: that equal split only holds once every replica is included (fraction 1.0). MBAR is therefore handed self-inconsistent inputs and returns a biased estimate for every fraction < 1.0. This also explains why the real-time MBAR estimate and the post-hoc convergence analysis agree only at the final estimate.This PR reshapes the 2D
u_lnmatrix into its 3D(state, replica, iteration)form, slices the iteration (time) axis to take the first (forward) or last (reverse)chunkiterations of every replica, then flattens back to 2D for MBAR. Because each iteration contributes exactly one sample to every state, each slice now contains exactlychunksamples per state — consistent with thenew_N_l = [chunk] * n_statespassed to MBAR — and the "fraction" axis genuinely means fraction of simulation time. The reshape fix is self-contained within the_get_fraction_free_energyhelper function, which was introduced by PR #1984. This means that the publicget_forward_and_reverse_analysisis unchanged compared to the same PR.I reran the forwards/reverse energy estimate for the TYK2 ejm_47 example with the fix implemented (generate_convergence.py), and obtained the convergence plots shown below (forwards estimate is now consistent with the real-time analysis yaml files).
Solvent leg
Complex leg
Tests
Both changes are in
TestFEAnalysis(src/openfe/tests/protocols/test_openmmutils.py).New —
test_fraction_free_energy_slices_by_time. A fast, property-based regression test for the fix. It builds a small replica-majoru_lnin which every column is tagged with its own flat index, mocks_get_free_energyto capture the matrices that_get_fraction_free_energyhands it, and asserts that the forward/reverse slices are the first/lastchunkiterations of every replica (a slice in time), not the first/lastchunk * n_statescontiguous columns. It also guards explicitly against a regression to the old contiguous-column selection. Because it checks which samples are selected rather than the resulting MBAR numbers, it fails on the pre-fix code and passes on the fix, and needs no.ncfixture or MBAR run.Modified —
test_free_energies. This characterisation test pins the per-fractionforward_DGs/reverse_DGsvalues. Since the fix changes which samples enter each intermediate fraction, those values change for every fraction < 1.0. The final fraction (1.0) is unchanged — it uses the full data set either way — confirming the reported free energy is unaffected and only the intermediate path to it is redistributed. Theforward_dDGs/reverse_dDGsarrays are left as-is (they still pass under the existingrtol=5e-01). The updated expected arrays were regenerated from the same fixture on this branch, and can be reproduced with:The existing MBAR-failure tests (
test_forward_and_reverse_nan_on_mbar_failure,test_forward_and_reverse_none_on_final_fraction_failure) are unaffected by the fix and continue to pass.LLM / AI generated code disclosure
LLMs or other AI-powered tools (beyond simple IDE use cases) were used in this contribution: yes
The new unit test
test_fraction_free_energy_slices_by_timewas generated by Claude Code (Opus 4.8) + manually reviewed and edited. The code shown above in the Tests section was also generated using Claude. Finally, the python script for local testing generate_convergence.py was also generated using Claude. All AI-generated code was manually reviewed + edited where necessary.Checklist
newsentry, or the changes are not user-facing.pre-commit.ci autofix.Developers certificate of origin