From d59044fe35ff1b9f86690837170bb8a850d57b32 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Sat, 14 Feb 2026 22:11:04 +0000 Subject: [PATCH 1/5] Safely close reporter and clear GPU contexts to avoid UnboundLocalErrors Added try/except block to safely close things in case there's an unbound variable. --- .../protocols/openmm_afe/base_afe_units.py | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/openfe/protocols/openmm_afe/base_afe_units.py b/src/openfe/protocols/openmm_afe/base_afe_units.py index 4095982ab..3a13033a4 100644 --- a/src/openfe/protocols/openmm_afe/base_afe_units.py +++ b/src/openfe/protocols/openmm_afe/base_afe_units.py @@ -1319,24 +1319,29 @@ def run( ) finally: - # close reporter when you're done to prevent file handle clashes - reporter.close() - - # clear GPU context - # Note: use cache.empty() when openmmtools #690 is resolved - for context in list(sampler.energy_context_cache._lru._data.keys()): - del sampler.energy_context_cache._lru._data[context] - for context in list(sampler.sampler_context_cache._lru._data.keys()): - del sampler.sampler_context_cache._lru._data[context] - # cautiously clear out the global context cache too - for context in list(openmmtools.cache.global_context_cache._lru._data.keys()): - del openmmtools.cache.global_context_cache._lru._data[context] - - del sampler.sampler_context_cache, sampler.energy_context_cache - - # Keep these around in a dry run so we can inspect things - if not dry: - del integrator, sampler + # Have to wrap this in a try/except, because we might + # be in a situation where the reporter or sampler weren't created + try: + # Order is reporter, contexts, integrator, sampler + reporter.close() # close to prevent file handle clashes + + # clear GPU context + # Note: use cache.empty() when openmmtools #690 is resolved + for context in list(sampler.energy_context_cache._lru._data.keys()): + del sampler.energy_context_cache._lru._data[context] + for context in list(sampler.sampler_context_cache._lru._data.keys()): + del sampler.sampler_context_cache._lru._data[context] + # cautiously clear out the global context cache too + for context in list(openmmtools.cache.global_context_cache._lru._data.keys()): + del openmmtools.cache.global_context_cache._lru._data[context] + + del sampler.sampler_context_cache, sampler.energy_context_cache + + # Keep these around in a dry run so we can inspect things + if not dry: + del integrator, sampler + except UnboundLocalError: + pass if not dry: nc = self.shared_basepath / settings["output_settings"].output_filename From e5de08fe211f4c63cbbaa3f976adb47eb098bc79 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Sat, 14 Feb 2026 22:29:28 +0000 Subject: [PATCH 2/5] small fix small fix --- src/openfe/protocols/openmm_afe/base_afe_units.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/openfe/protocols/openmm_afe/base_afe_units.py b/src/openfe/protocols/openmm_afe/base_afe_units.py index 3a13033a4..994873b48 100644 --- a/src/openfe/protocols/openmm_afe/base_afe_units.py +++ b/src/openfe/protocols/openmm_afe/base_afe_units.py @@ -1322,7 +1322,7 @@ def run( # Have to wrap this in a try/except, because we might # be in a situation where the reporter or sampler weren't created try: - # Order is reporter, contexts, integrator, sampler + # Order is reporter, contexts, sampler, integrator reporter.close() # close to prevent file handle clashes # clear GPU context @@ -1339,6 +1339,8 @@ def run( # Keep these around in a dry run so we can inspect things if not dry: + # At this point we know the sampler exists, so we del the integrator + # first since it's associated with the sampler del integrator, sampler except UnboundLocalError: pass From cdf14536dba007da34816a64f4e12e0ae6eab745 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Sat, 14 Feb 2026 22:33:31 +0000 Subject: [PATCH 3/5] Also improve deletion for the hybridtop protocol Added error handling for reporter closure and context clearing. --- .../protocols/openmm_rfe/hybridtop_units.py | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/src/openfe/protocols/openmm_rfe/hybridtop_units.py b/src/openfe/protocols/openmm_rfe/hybridtop_units.py index c7afbd9c4..cd07d598b 100644 --- a/src/openfe/protocols/openmm_rfe/hybridtop_units.py +++ b/src/openfe/protocols/openmm_rfe/hybridtop_units.py @@ -1227,25 +1227,31 @@ def run( dry=dry, ) finally: - # close reporter when you're done, prevent - # file handle clashes - reporter.close() - - # clear GPU contexts - # TODO: use cache.empty() calls when openmmtools #690 is resolved - # replace with above - for context in list(sampler.energy_context_cache._lru._data.keys()): - del sampler.energy_context_cache._lru._data[context] - for context in list(sampler.sampler_context_cache._lru._data.keys()): - del sampler.sampler_context_cache._lru._data[context] - # cautiously clear out the global context cache too - for context in list(openmmtools.cache.global_context_cache._lru._data.keys()): - del openmmtools.cache.global_context_cache._lru._data[context] - - del sampler.sampler_context_cache, sampler.energy_context_cache - - if not dry: - del integrator, sampler + # Have to wrap this in a try/except, because we might + # be in a situation where the reporter or sampler wasn't created + try: + # Order is reporter, contexts, sampler, integrator + reporter.close() # close to prevent file handle clashes + + # clear GPU context + # Note: use cache.empty() when openmmtools #690 is resolved + for context in list(sampler.energy_context_cache._lru._data.keys()): + del sampler.energy_context_cache._lru._data[context] + for context in list(sampler.sampler_context_cache._lru._data.keys()): + del sampler.sampler_context_cache._lru._data[context] + # cautiously clear out the global context cache too + for context in list(openmmtools.cache.global_context_cache._lru._data.keys()): + del openmmtools.cache.global_context_cache._lru._data[context] + + del sampler.sampler_context_cache, sampler.energy_context_cache + + # Keep these around in a dry run so we can inspect things + if not dry: + # At this point we know the sampler exists, so we del the integrator + # first since it's associated with the sampler + del integrator, sampler + except UnboundLocalError: + pass if not dry: # pragma: no-cover return { From 6eea84de6e86f6988266b46ad57acdfba3aa2547 Mon Sep 17 00:00:00 2001 From: IAlibay Date: Sat, 14 Feb 2026 22:56:04 +0000 Subject: [PATCH 4/5] Add news item --- news/issue-1845.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 news/issue-1845.rst diff --git a/news/issue-1845.rst b/news/issue-1845.rst new file mode 100644 index 000000000..718565b32 --- /dev/null +++ b/news/issue-1845.rst @@ -0,0 +1,24 @@ +**Added:** + +* + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* Fixed a bug in Protocol termination for the HybridTop and AFE Protocols + which would unecessary declare an UnboundLocalError. + +**Security:** + +* From b6bda3def9fb182dcdf4213f335c600bc7040f55 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Mon, 16 Feb 2026 15:23:49 +0000 Subject: [PATCH 5/5] Update news/issue-1845.rst Co-authored-by: Alyssa Travitz <31974495+atravitz@users.noreply.github.com> --- news/issue-1845.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/issue-1845.rst b/news/issue-1845.rst index 718565b32..aee176f6b 100644 --- a/news/issue-1845.rst +++ b/news/issue-1845.rst @@ -17,7 +17,7 @@ **Fixed:** * Fixed a bug in Protocol termination for the HybridTop and AFE Protocols - which would unecessary declare an UnboundLocalError. + which would unnecessarily declare an ``UnboundLocalError``. **Security:**