Skip to content

return partial convergence analysis if mbar fails for individual fraction#1984

Open
aqemia-benedict-tan wants to merge 14 commits into
OpenFreeEnergy:mainfrom
aqemia-benedict-tan:main
Open

return partial convergence analysis if mbar fails for individual fraction#1984
aqemia-benedict-tan wants to merge 14 commits into
OpenFreeEnergy:mainfrom
aqemia-benedict-tan:main

Conversation

@aqemia-benedict-tan

Copy link
Copy Markdown

When running ABFE calculations in OpenFE, MBAR sometimes fails to produce an estimate at low fractions of uncorrelated samples (~0.1-0.4). It would be helpful to still return the convergence trace where MBAR can generate a meaningful result (e.g. >0.7) This PR modifies get_forward_and_reverse_analysis under openmm_utils so that NaN values are appended to the forwards/reverse estimate lists where MBAR fails and the successful estimates are still returned.

Checklist

  • All new code is appropriately documented (user-facing code must have complete docstrings).
  • Ran pre-commit: you can run pre-commit locally or comment on this PR with pre-commit.ci autofix.

Developers certificate of origin

@IAlibay IAlibay left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @aqemia-benedict-tan, this looks great - just the one comment on the code as-is.

The main thing we would need (and why I have to request changes) is that we need to add tests to cover the new behaviour

Ideally we would want to:

  1. Test that the check the array of forward and backwards energies returned make sense (similar to )
  2. Something in tests/analysis/test_plotting.py that checks that passing in an array with NaN's works fine and probably a manual check that the plot looks ok.

# MBAR can fail to converge at low fractions of uncorrelated
# samples. If this happens, append NaN and carry on so that
# estimates at higher fractions are still reported.
try:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Given the similar-ish duplicated code, it might be good to just move this to a helper method that takes in the sub-sampled u_ln, fraction, and either "reverse" or "forward" for the warning string.

@codecov

codecov Bot commented Jun 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.16%. Comparing base (75cb2e8) to head (d3b12f7).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1984      +/-   ##
==========================================
- Coverage   94.95%   90.16%   -4.79%     
==========================================
  Files         216      216              
  Lines       20517    20578      +61     
==========================================
- Hits        19481    18554     -927     
- Misses       1036     2024     +988     
Flag Coverage Δ
fast-tests 90.16% <100.00%> (?)
slow-tests ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@aqemia-benedict-tan

Copy link
Copy Markdown
Author

Thanks for the feedback @IAlibay, I have refactored to include the helper function _get_fraction_free_energy for the purpose of appending NaN in the event of low fraction failure. I have also added unit tests to verify that clean arrays are returned in the event of the lowest fraction in the forwards estimate failing and to assert that the convergence plot is returned successfully if an array containing NaN estimates is passed to it. The resulting plot looks like this:

image

@IAlibay

IAlibay commented Jun 4, 2026

Copy link
Copy Markdown
Member

Thanks @aqemia-benedict-tan ! I'lll have another look at this early next week.

@IAlibay IAlibay left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry - I somewhat changed my mind on what the behaviour should look like on failure, that will require some code changes.

Could you also:

  • Add a news entry under openfe/news.
  • Add yourself to the CITATION.cff author list?

Thanks!

fraction = chunk / N_l[0]

# Forward
DG, dDG = self._get_free_energy(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been thinking about this a little bit more and I've come to the realization that we probably should NaN both the forward and the reverse chunk if either fails.

My reasoning is that whilst a failure is directly caused by a lack of convergence in the estimator, it's more broadly caused by too few data points (generally too high a decorrelation time). So whilst one direction could pass (e.g. the reduced potentials happen to be easier to deal with), it's probably not reasonable to include either.

@aqemia-benedict-tan aqemia-benedict-tan Jun 11, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this makes sense - commits 7b7dba3 and dbf9b71 refactor the _get_fraction_free_energy helper to return NaN for both directions, if either the forwards or reverse estimate fails.
Commit 42ee838 updates the tests, the test at src/openfe/tests/protocols/test_openmmutils.py is adapted to ensure NaN is returned for both directions in the case that the lowest fraction of samples failes in either the forwards or reverse direction.
The new graph from the plotting test looks like this:

image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing, thanks!

uncorrelated samples) should be rendered as gaps without raising.
"""
forward_reverse = _make_forward_reverse(
forward=[np.nan, np.nan, -9.2, -9.4, -9.5, -9.6, -9.65, -9.7, -9.72, -9.75],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the nan happens towards the end? It's unlikely but could happen.

My understanding is that the fill_between call would just do nothing. In that case - it might be better to just return None for the whole thing.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! Commit ce66327 fixes this by returningNone if the final element of the reverse and forwards estimate (i.e. fraction 1.0) isNaN. A unit test is also added for this scenario.

Comment thread src/openfe/protocols/openmm_utils/multistate_analysis.py
rtol=5e-01,
) # fmt: skip

def test_forward_and_reverse_nan_on_mbar_failure(self, analyzer):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this - this test looks great!

@aqemia-benedict-tan

Copy link
Copy Markdown
Author

Sorry - I somewhat changed my mind on what the behaviour should look like on failure, that will require some code changes.

Could you also:

  • Add a news entry under openfe/news.
  • Add yourself to the CITATION.cff author list?

Thanks!

Added in commits 4ce6e8a and e5d52da

@IAlibay IAlibay left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more things and then I think we're there!


fractions.append(chunk / N_l[0])
fractions.append(fraction)
except ParameterError:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this outer try/except is now dead. Can you remove it (alongisde the associated ParameterError import)?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 8cd3275

"of uncorrelated samples (fraction 1.0); discarding the forward "
"and reverse convergence analysis."
)
warnings.warn(wmsg)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you set the stacklevel parameter to 2 here? We're not great at being consistent about it elsewhere, but it's a small QoL thing that will be useful for users.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 9232453

@github-actions

Copy link
Copy Markdown

No API break detected ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants