diff --git a/docs/conf.py b/docs/conf.py index 4c207095..bdaf6fe3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,7 +61,7 @@ intersphinx_mapping = { "python": ("https://docs.python.org/3.9", None), "numpy": ("https://numpy.org/doc/stable", None), "scikit.learn": ("https://scikit-learn.org/stable", None), - "openmm": ("http://docs.openmm.org/latest/api-python/", None), + "openmm": ("https://docs.openmm.org/latest/api-python/", None), "rdkit": ("https://www.rdkit.org/docs", None), "openeye": ("https://docs.eyesopen.com/toolkits/python/", None), "mdtraj": ("https://www.mdtraj.org/1.9.5/", None), diff --git a/docs/guide/troubleshooting.rst b/docs/guide/troubleshooting.rst index 35303c84..b85fb218 100644 --- a/docs/guide/troubleshooting.rst +++ b/docs/guide/troubleshooting.rst @@ -58,3 +58,18 @@ Save this configuration file as ``debug_logging.conf`` and then run ``openfe qui Note that the ``--log debug_logging.conf`` argument goes between ``openfe`` and ``quickrun`` on the command line. This will cause every package to log at the debug level, which may be quite verbose and noisy but should aid in identify what is going on right before the exception is thrown. + +JAX warnings +------------ + +We use ``pymbar`` to analyze the free energy of the system. +``pymbar`` uses JAX to accelerate computation. +The JAX library can utilize a GPU to further accelerate computation. +If the necessary libraries for GPU acceleration are not installed and JAX detects a GPU, JAX will print a warning like this: + +.. code-block:: bash + + WARNING:2025-06-10 09:01:40,857:jax._src.xla_bridge:966: An NVIDIA GPU may be present on this machine, but a CUDA-enabled jaxlib is not installed. Falling back to cpu. + +This warning does not mean that the *molecular dynamics* simulation will fall back to using the CPU. +The simulation will still use the computing platform specified in the settings. diff --git a/news/jax-warning.rst b/news/jax-warning.rst new file mode 100644 index 00000000..f50b3dbd --- /dev/null +++ b/news/jax-warning.rst @@ -0,0 +1,24 @@ +**Added:** + +* Emit a clarifying log message when a user gets a warning from JAX (#1585). + Fixes #1499. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/openfe/__init__.py b/openfe/__init__.py index 48c17f85..d56e7340 100644 --- a/openfe/__init__.py +++ b/openfe/__init__.py @@ -1,19 +1,30 @@ -# silence pymbar logging warnings -import logging +# We need to do this first so that we can set up our +# log control since some modules have warnings on import +from openfe.utils import logging_control +logging_control._silence_message( + msg=[ + "****** PyMBAR will use 64-bit JAX! *******", + ], + logger_names=[ + "pymbar.mbar_solvers", + ], +) -def _mute_timeseries(record): - return "Warning on use of the timeseries module:" not in record.msg +logging_control._silence_message( + msg=[ + "Warning on use of the timeseries module:", + ], + logger_names=[ + "pymbar.timeseries", + ], +) +logging_control._append_logger( + suffix="\n \n[OPENFE]: The simulation is still using the compute platform specified in the settings \n See this URL for more information: https://docs.openfree.energy/en/latest/guide/troubleshooting.html#jax-warnings \n\n", + logger_names="jax._src.xla_bridge", +) -def _mute_jax(record): - return "****** PyMBAR will use 64-bit JAX! *******" not in record.msg - - -_mbar_log = logging.getLogger("pymbar.timeseries") -_mbar_log.addFilter(_mute_timeseries) -_mbar_log = logging.getLogger("pymbar.mbar_solvers") -_mbar_log.addFilter(_mute_jax) from importlib.metadata import version diff --git a/openfe/protocols/openmm_utils/multistate_analysis.py b/openfe/protocols/openmm_utils/multistate_analysis.py index 57811e47..6f1d785d 100644 --- a/openfe/protocols/openmm_utils/multistate_analysis.py +++ b/openfe/protocols/openmm_utils/multistate_analysis.py @@ -14,8 +14,6 @@ import numpy.typing as npt from openff.units import Quantity, unit from openff.units.openmm import from_openmm from openmmtools import multistate -from pymbar import MBAR -from pymbar.utils import ParameterError from openfe.analysis import plotting from openfe.due import Doi, due @@ -236,8 +234,10 @@ class MultistateEquilFEAnalysis: * Allow folks to pass in extra options for bootstrapping etc.. * Add standard test against analyzer.get_free_energy() """ + # pymbar has some side effects when imported so we only import it right when we + # need it + from pymbar import MBAR - # pymbar 4 mbar = MBAR( u_ln, N_l, @@ -312,6 +312,10 @@ class MultistateEquilFEAnalysis: issues with the solver when using low amounts of data points. All uncertainties are MBAR analytical errors. """ + # pymbar has some side effects from being imported, so we only want to import + # it right when we need it + from pymbar.utils import ParameterError + try: u_ln = self.analyzer._unbiased_decorrelated_u_ln N_l = self.analyzer._unbiased_decorrelated_N_l diff --git a/openfe/tests/utils/test_log_control.py b/openfe/tests/utils/test_log_control.py new file mode 100644 index 00000000..a725f09f --- /dev/null +++ b/openfe/tests/utils/test_log_control.py @@ -0,0 +1,518 @@ +import logging + +import pytest + +from openfe.utils import logging_control +from openfe.utils.logging_control import ( + _AppendMsgFilter, + _BaseLogFilter, + _MsgIncludesStringFilter, +) + + +@pytest.fixture +def logger(): + """Create a test logger with a handler that captures log records.""" + test_logger = logging.getLogger("test_logger") + test_logger.setLevel(logging.DEBUG) + test_logger.handlers = [] # Clear any existing handlers + + # Create a handler that stores log records + class ListHandler(logging.Handler): + def __init__(self): + super().__init__() + self.records = [] + + def emit(self, record): + self.records.append(record) + + handler = ListHandler() + test_logger.addHandler(handler) + + yield test_logger, handler + + # Cleanup + test_logger.handlers = [] + test_logger.filters = [] + + +class Test_MsgIncludesStringFilter: + """Tests for _MsgIncludesStringFilter.""" + + def test_single_string_blocks_matching_message(self): + """Test that a single string blocks messages containing it.""" + filter_obj = _MsgIncludesStringFilter("block this") + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="", + lineno=0, + msg="Please block this message", + args=(), + exc_info=None, + ) + assert filter_obj.filter(record) is False + + def test_single_string_allows_non_matching_message(self): + """Test that messages not containing the string are allowed.""" + filter_obj = _MsgIncludesStringFilter("block this") + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="", + lineno=0, + msg="This is fine", + args=(), + exc_info=None, + ) + assert filter_obj.filter(record) is True + + def test_list_of_strings_blocks_any_match(self): + """Test that any string in the list blocks the message.""" + filter_obj = _MsgIncludesStringFilter(["warning1", "warning2", "warning3"]) + + record1 = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="", + lineno=0, + msg="This has warning1 in it", + args=(), + exc_info=None, + ) + assert filter_obj.filter(record1) is False + + record2 = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="", + lineno=0, + msg="This has warning3 in it", + args=(), + exc_info=None, + ) + assert filter_obj.filter(record2) is False + + def test_list_allows_non_matching_messages(self): + """Test that messages not matching any string are allowed.""" + filter_obj = _MsgIncludesStringFilter(["warning1", "warning2"]) + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="", + lineno=0, + msg="This is completely different", + args=(), + exc_info=None, + ) + assert filter_obj.filter(record) is True + + def test_substring_matching(self): + """Test that substring matching works correctly.""" + filter_obj = _MsgIncludesStringFilter("JAX") + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="", + lineno=0, + msg="****** PyMBAR will use 64-bit JAX! *******", + args=(), + exc_info=None, + ) + assert filter_obj.filter(record) is False + + def test_case_sensitive_matching(self): + """Test that matching is case-sensitive.""" + filter_obj = _MsgIncludesStringFilter("Error") + + record1 = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="", + lineno=0, + msg="This has Error in it", + args=(), + exc_info=None, + ) + assert filter_obj.filter(record1) is False + + record2 = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="", + lineno=0, + msg="This has error in it", + args=(), + exc_info=None, + ) + assert filter_obj.filter(record2) is True + + +class Test_AppendMsgFilter: + """Tests for _AppendMsgFilter.""" + + def test_single_suffix_appends(self): + """Test that a single suffix is appended to the message.""" + filter_obj = _AppendMsgFilter(" [DEPRECATED]") + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="", + lineno=0, + msg="Original message", + args=(), + exc_info=None, + ) + result = filter_obj.filter(record) + assert result is True + assert record.msg == "Original message [DEPRECATED]" + + def test_multiple_suffixes_append_in_order(self): + """Test that multiple suffixes are appended in order.""" + filter_obj = _AppendMsgFilter([" [DEPRECATED]", " - see docs"]) + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="", + lineno=0, + msg="Original message", + args=(), + exc_info=None, + ) + filter_obj.filter(record) + assert record.msg == "Original message [DEPRECATED] - see docs" + + def test_idempotent_single_suffix(self): + """Test that applying the same suffix twice is idempotent.""" + filter_obj = _AppendMsgFilter(" [DEPRECATED]") + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="", + lineno=0, + msg="Original message [DEPRECATED]", + args=(), + exc_info=None, + ) + filter_obj.filter(record) + assert record.msg == "Original message [DEPRECATED]" + + def test_idempotent_multiple_suffixes(self): + """Test idempotency with multiple suffixes.""" + filter_obj = _AppendMsgFilter([" [DEPRECATED] - see docs", " [DEPRECATED] - see docs"]) + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="", + lineno=0, + msg="Original message [DEPRECATED] - see docs", + args=(), + exc_info=None, + ) + filter_obj.filter(record) + assert record.msg == "Original message [DEPRECATED] - see docs" + + def test_always_returns_true(self): + """Test that the filter always returns True to allow logging.""" + filter_obj = _AppendMsgFilter(" [INFO]") + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="", + lineno=0, + msg="Message", + args=(), + exc_info=None, + ) + assert filter_obj.filter(record) is True + + +class Testlogging_control: + """Tests for logging_control module.""" + + def test__silence_message_single_string_single_logger(self, logger): + """Test silencing a single message from a single logger.""" + test_logger, handler = logger + + logging_control._silence_message(msg="block this", logger_names="test_logger") + + test_logger.info("block this message") + test_logger.info("allow this message") + + assert len(handler.records) == 1 + assert handler.records[0].msg == "allow this message" + + def test__silence_message_multiple_strings_single_logger(self, logger): + """Test silencing multiple messages from a single logger.""" + test_logger, handler = logger + + logging_control._silence_message(msg=["warning1", "warning2"], logger_names="test_logger") + + test_logger.info("This has warning1") + test_logger.info("This has warning2") + test_logger.info("This is fine") + + assert len(handler.records) == 1 + assert handler.records[0].msg == "This is fine" + + def test__silence_message_single_string_multiple_loggers(self): + """Test silencing a message from multiple loggers.""" + logger1 = logging.getLogger("test_logger1") + logger2 = logging.getLogger("test_logger2") + + # Clear any existing filters + logger1.filters = [] + logger2.filters = [] + + logging_control._silence_message( + msg="block this", logger_names=["test_logger1", "test_logger2"] + ) + + assert len(logger1.filters) == 1 + assert len(logger2.filters) == 1 + + # Cleanup + logger1.filters = [] + logger2.filters = [] + + def test__silence_message_multiple_strings_multiple_loggers(self): + """Test silencing multiple messages from multiple loggers.""" + logger1 = logging.getLogger("test_logger1") + logger2 = logging.getLogger("test_logger2") + + logger1.filters = [] + logger2.filters = [] + + logging_control._silence_message( + msg=["warning1", "warning2"], logger_names=["test_logger1", "test_logger2"] + ) + + # Should have one filter per logger (not one per message) + assert len(logger1.filters) == 1 + assert len(logger2.filters) == 1 + + # Cleanup + logger1.filters = [] + logger2.filters = [] + + def test__silence_logger_single(self, logger): + """Test completely silencing a single logger.""" + test_logger, handler = logger + + logging_control._silence_logger(logger_names="test_logger") + + test_logger.debug("debug message") + test_logger.info("info message") + test_logger.warning("warning message") + test_logger.error("error message") + + # All messages should be blocked + assert len(handler.records) == 0 + + def test__silence_logger_multiple(self): + """Test silencing multiple loggers.""" + logger1 = logging.getLogger("test_logger1") + logger2 = logging.getLogger("test_logger2") + + original_level1 = logger1.level + original_level2 = logger2.level + + logging_control._silence_logger(logger_names=["test_logger1", "test_logger2"]) + + assert logger1.level == logging.CRITICAL + assert logger2.level == logging.CRITICAL + + # Cleanup + logger1.setLevel(original_level1) + logger2.setLevel(original_level2) + + def test__silence_logger_custom_level(self, logger): + """Test silencing logger with custom level.""" + test_logger, handler = logger + + logging_control._silence_logger(logger_names="test_logger", level=logging.ERROR) + + test_logger.debug("debug message") + test_logger.info("info message") + test_logger.warning("warning message") + test_logger.error("error message") + + # Only error and above should pass through + assert len(handler.records) == 1 + assert handler.records[0].msg == "error message" + + def test__append_logger_single_suffix_single_logger(self, logger): + """Test appending a single suffix to a single logger.""" + test_logger, handler = logger + + logging_control._append_logger(suffix=" [DEPRECATED]", logger_names="test_logger") + + test_logger.info("Original message") + + assert len(handler.records) == 1 + assert handler.records[0].msg == "Original message [DEPRECATED]" + + def test__append_logger_multiple_suffixes(self, logger): + """Test appending multiple suffixes.""" + test_logger, handler = logger + + logging_control._append_logger( + suffix=[" [DEPRECATED]", " - see docs"], logger_names="test_logger" + ) + + test_logger.info("Original message") + + assert len(handler.records) == 1 + assert handler.records[0].msg == "Original message [DEPRECATED] - see docs" + + def test__append_logger_multiple_loggers(self): + """Test appending to multiple loggers.""" + logger1 = logging.getLogger("test_logger1") + logger2 = logging.getLogger("test_logger2") + + logger1.filters = [] + logger2.filters = [] + + logging_control._append_logger( + suffix=" [INFO]", logger_names=["test_logger1", "test_logger2"] + ) + + assert len(logger1.filters) == 1 + assert len(logger2.filters) == 1 + + # Cleanup + logger1.filters = [] + logger2.filters = [] + + def test_pymbar_example(self, logger): + """Test the PyMBAR use case.""" + test_logger, handler = logger + + logging_control._silence_message( + msg="****** PyMBAR will use 64-bit JAX! *******", logger_names="test_logger" + ) + + test_logger.info("****** PyMBAR will use 64-bit JAX! *******") + test_logger.info("Other message") + + assert len(handler.records) == 1 + assert handler.records[0].msg == "Other message" + + def test_combining_multiple_controls(self, logger): + """Test combining silence and append on the same logger.""" + test_logger, handler = logger + + logging_control._silence_message(msg="block", logger_names="test_logger") + logging_control._append_logger(suffix=" [INFO]", logger_names="test_logger") + + test_logger.info("block this") + test_logger.info("allow this") + + assert len(handler.records) == 1 + assert handler.records[0].msg == "allow this [INFO]" + + +class TestBaseLogFilter: + """Tests for _BaseLogFilter base class.""" + + def test_cannot_instantiate_abstract_class(self): + """Test that _BaseLogFilter cannot be instantiated directly.""" + with pytest.raises(TypeError): + _BaseLogFilter("test") + + def test_subclass_must_implement_filter(self): + """Test that subclasses must implement the filter method.""" + + class IncompleteFilter(_BaseLogFilter): + pass + + with pytest.raises(TypeError): + IncompleteFilter("test") + + def test_subclass_with_filter_works(self): + """Test that a proper subclass can be instantiated.""" + + class CompleteFilter(_BaseLogFilter): + def filter(self, record): + return True + + filter_obj = CompleteFilter("test") + assert filter_obj.strings == ["test"] + + def test_string_conversion_to_list(self): + """Test that single strings are converted to lists.""" + filter_obj = _MsgIncludesStringFilter("single") + assert isinstance(filter_obj.strings, list) + assert filter_obj.strings == ["single"] + + def test_list_stays_as_list(self): + """Test that lists remain as lists.""" + filter_obj = _MsgIncludesStringFilter(["one", "two", "three"]) + assert isinstance(filter_obj.strings, list) + assert filter_obj.strings == ["one", "two", "three"] + + +class TestEdgeCases: + """Tests for edge cases and error conditions.""" + + def test_empty_string(self, logger): + """Test behavior with empty string.""" + test_logger, handler = logger + + logging_control._silence_message(msg="", logger_names="test_logger") + + test_logger.info("message") + + # Empty string matches everything as substring + assert len(handler.records) == 0 + + def test_empty_list(self, logger): + """Test behavior with empty list.""" + test_logger, handler = logger + + logging_control._silence_message(msg=[], logger_names="test_logger") + + test_logger.info("message") + + # Empty list should not block anything + assert len(handler.records) == 1 + + def test_special_characters_in_message(self, logger): + """Test that special characters are handled correctly.""" + test_logger, handler = logger + + logging_control._silence_message( + msg="[WARNING] *special* $chars$", logger_names="test_logger" + ) + + test_logger.info("[WARNING] *special* $chars$ in message") + test_logger.info("normal message") + + assert len(handler.records) == 1 + assert handler.records[0].msg == "normal message" + + def test_unicode_characters(self, logger): + """Test that unicode characters work correctly.""" + test_logger, handler = logger + + logging_control._silence_message(msg="🚫 blocked", logger_names="test_logger") + logging_control._append_logger(suffix=" ✅", logger_names="test_logger") + + test_logger.info("🚫 blocked message") + test_logger.info("allowed message") + + assert len(handler.records) == 1 + assert handler.records[0].msg == "allowed message ✅" + + def test_very_long_message(self, logger): + """Test handling of very long messages.""" + test_logger, handler = logger + + long_msg = "x" * 10000 + logging_control._silence_message(msg="needle", logger_names="test_logger") + + test_logger.info(long_msg + "needle" + long_msg) + test_logger.info("short message") + + assert len(handler.records) == 1 + assert handler.records[0].msg == "short message" diff --git a/openfe/utils/__init__.py b/openfe/utils/__init__.py index 0d276a97..a6a6c450 100644 --- a/openfe/utils/__init__.py +++ b/openfe/utils/__init__.py @@ -1,7 +1,10 @@ # This code is part of OpenFE and is licensed under the MIT license. # For details, see https://github.com/OpenFreeEnergy/openfe -from . import custom_typing +from . import ( + custom_typing, + logging_control, +) from .optional_imports import requires_package from .remove_oechem import without_oechem_backend from .system_probe import log_system_probe diff --git a/openfe/utils/logging_control.py b/openfe/utils/logging_control.py new file mode 100644 index 00000000..13a858b0 --- /dev/null +++ b/openfe/utils/logging_control.py @@ -0,0 +1,171 @@ +import logging +from abc import ABC, abstractmethod + + +class _BaseLogFilter(ABC): + """Base class for log filters that handle string or list of strings. + + Parameters + ---------- + strings : str or list of str + String(s) to use in the filter logic + """ + + def __init__(self, strings: str | list[str]) -> None: + if isinstance(strings, str): + strings = [strings] + self.strings: list[str] = strings + + @abstractmethod + def filter(self, record: logging.LogRecord) -> bool: + """Filter method to be implemented by subclasses. + + Parameters + ---------- + record : logging.LogRecord + Log record to filter/modify + + Returns + ------- + bool + True to allow the record, False to block it + """ + ... + + +class _MsgIncludesStringFilter(_BaseLogFilter): + """Logging filter to silence specific log messages. + + See https://docs.python.org/3/library/logging.html#filter-objects + + Parameters + ---------- + strings : str or list of str + If string(s) match in log messages (substring match) then the log record + is suppressed + """ + + def filter(self, record: logging.LogRecord) -> bool: + """Filter log records that contain any of the specified strings. + + Parameters + ---------- + record : logging.LogRecord + Log record to filter + + Returns + ------- + bool + False if the record should be blocked, True if it should be logged + """ + for string in self.strings: + if string in record.msg: + return False + return True + + +class _AppendMsgFilter(_BaseLogFilter): + """Logging filter to append a message to a specific log message. + + See https://docs.python.org/3/library/logging.html#filter-objects + + Parameters + ---------- + strings : str or list of str + Suffix text(s) to append to log messages + """ + + def __init__(self, strings: str | list[str]) -> None: + super().__init__(strings) + # Rename for clarity in this context + self.suffixes = self.strings + + def filter(self, record: logging.LogRecord) -> bool: + """Append suffix to log record message. + + Parameters + ---------- + record : logging.LogRecord + Log record to modify + + Returns + ------- + bool + Always True to allow the record to be logged + """ + for suffix in self.suffixes: + # Only modify if not already appended (idempotent) + if not record.msg.endswith(suffix): + record.msg = f"{record.msg}{suffix}" + return True + + +def _silence_message(msg: str | list[str], logger_names: str | list[str]) -> None: + """Silence specific log messages from one or more loggers. + + Parameters + ---------- + msg : str or list of str + String(s) to match in log messages (substring match) + logger_names : str or list of str + Logger name(s) to apply the filter to + + Examples + -------- + >>> _silence_message( + ... msg="****** PyMBAR will use 64-bit JAX! *******", + ... logger_names=["pymbar.timeseries", "pymbar.mbar_solvers"] + ... ) + """ + if isinstance(logger_names, str): + logger_names = [logger_names] + + filter_obj = _MsgIncludesStringFilter(msg) + for name in logger_names: + logging.getLogger(name).addFilter(filter_obj) + + +def _silence_logger(logger_names: str | list[str], level: int = logging.CRITICAL) -> None: + """Completely silence one or more loggers. + + Parameters + ---------- + logger_names : str or list of str + Logger name(s) to silence + level : int + Set logger level (default: CRITICAL to silence everything) + + Examples + -------- + >>> _silence_logger(logger_names=["urllib3", "requests"]) + """ + if isinstance(logger_names, str): + logger_names = [logger_names] + + for name in logger_names: + logging.getLogger(name).setLevel(level) + + +def _append_logger(suffix: str | list[str], logger_names: str | list[str]) -> None: + """Append text to logger messages. + + Parameters + ---------- + suffix : str or list of str + Suffix text to append to log messages + logger_names : str or list of str + Logger name(s) to modify + + Examples + -------- + >>> _append_logger( + ... suffix=" [DEPRECATED]", + ... logger_names="myapp" + ... ) + """ + if isinstance(logger_names, str): + logger_names = [logger_names] + + filter_obj = _AppendMsgFilter(suffix) + for name in logger_names: + logging.getLogger(name).addFilter(filter_obj) diff --git a/openfe/utils/logging_filter.py b/openfe/utils/logging_filter.py deleted file mode 100644 index 63d22a95..00000000 --- a/openfe/utils/logging_filter.py +++ /dev/null @@ -1,20 +0,0 @@ -import logging - - -class MsgIncludesStringFilter: - """Logging filter to silence specfic log messages. - - See https://docs.python.org/3/library/logging.html#filter-objects - - Parameters - ---------- - string : str - if an exact for this is included in the log message, the log record - is suppressed - """ - - def __init__(self, string): - self.string = string - - def filter(self, record): - return self.string not in record.msg diff --git a/openfecli/commands/quickrun.py b/openfecli/commands/quickrun.py index 02472d82..f34410d6 100644 --- a/openfecli/commands/quickrun.py +++ b/openfecli/commands/quickrun.py @@ -56,7 +56,7 @@ def quickrun(transformation, work_dir, output): from gufe.tokenization import JSON_HANDLER from gufe.transformations.transformation import Transformation - from openfe.utils.logging_filter import MsgIncludesStringFilter + from openfe.utils import logging_control # avoid problems with output not showing if queueing system kills a job sys.stdout.reconfigure(line_buffering=True) @@ -68,15 +68,16 @@ def quickrun(transformation, work_dir, output): configure_logger("openfe", handler=stdout_handler) # silence the openmmtools.multistate API warning - stfu = MsgIncludesStringFilter( - "The openmmtools.multistate API is experimental and may change in future releases" + logging_control._silence_message( + msg=[ + "The openmmtools.multistate API is experimental and may change in future releases", + ], + logger_names=[ + "openmmtools.multistate.multistatereporter", + "openmmtools.multistate.multistateanalyzer", + "openmmtools.multistate.multistatesampler", + ], ) - omm_multistate = "openmmtools.multistate" - modules = ["multistatereporter", "multistateanalyzer", "multistatesampler"] - for module in modules: - ms_log = logging.getLogger(omm_multistate + "." + module) - ms_log.addFilter(stfu) - # turn warnings into log message (don't show stack trace) logging.captureWarnings(True)