Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ defaults:
jobs:
tests:
runs-on: ${{ matrix.OS }}
name: "💻-${{matrix.os }} 🐍-${{ matrix.python-version }}"
name: "💻-${{matrix.os }} 🐍-${{ matrix.python-version }} 🌉-pontibus-${{ matrix.pontibus }}"
strategy:
fail-fast: false
matrix:
Expand All @@ -31,6 +31,12 @@ jobs:
- "3.12" # gufe requires >= 3.12
- "3.13"
- "3.14"
pontibus: [true]
include:
# pontibus is optional: check that everything works without it
- os: ubuntu-latest
python-version: "3.12"
pontibus: false

steps:
- uses: actions/checkout@v4
Expand All @@ -49,6 +55,14 @@ jobs:
python=${{ matrix.python-version }}
init-shell: bash

- name: "Remove pontibus"
if: ${{ !matrix.pontibus }}
run: |
micromamba remove -y --force pontibus
if python -c "import pontibus" 2>/dev/null; then
echo "pontibus is still importable" && exit 1
fi

- name: "Install"
run: python -m pip install --no-deps -e .

Expand Down
50 changes: 38 additions & 12 deletions openfe_benchmarks/scripts/generate_results_archives.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@
from gufe import ProteinComponent, SolventComponent
from openff.units import unit
from cinnabar import FEMap
from pontibus.protocols.relative import HybridTopProtocol
from pontibus.protocols.solvation import ASFEProtocol

try:
from pontibus.protocols.relative import HybridTopProtocol
from pontibus.protocols.solvation import ASFEProtocol
except ImportError: # pontibus is an optional dependency
HybridTopProtocol = None
ASFEProtocol = None

from openfe_benchmarks.scripts.utils import load_archive

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -248,16 +254,20 @@ def _extract_results_from_archive(alchemical_archive):
extraction_functions = {
# default openfe hybrid rbfe in openfe using a split leg protocol
RelativeHybridTopologyProtocol: _extract_hybrid_topology_rfe_data,
# default hybrid rbfe in pontibus using the split leg protocol
HybridTopProtocol: _extract_hybrid_topology_rfe_data,
# pontibus ASFE
ASFEProtocol: _extract_asfe_data,
# SepTop RBFE
SepTopProtocol: _extract_septop_rbfe_data,
# TODO add support for openfe ASFE
# TODO add support for openfe ABFE
}

# pontibus protocols are only supported when pontibus is installed
if HybridTopProtocol is not None:
# default hybrid rbfe in pontibus using the split leg protocol
extraction_functions[HybridTopProtocol] = _extract_hybrid_topology_rfe_data
if ASFEProtocol is not None:
# pontibus ASFE
extraction_functions[ASFEProtocol] = _extract_asfe_data

raw_results = defaultdict(list)

for transformation, dag_results_list in alchemical_archive.transformation_results:
Expand All @@ -266,15 +276,27 @@ def _extract_results_from_archive(alchemical_archive):
f"Transformation {transformation.key} is does not meet minimum number of repeats requirement. Must at least {MIN_ALLOWED_REPEATS}."
)
protocol_cls = transformation.protocol.__class__
extract_func = extraction_functions.get(protocol_cls)
if protocol_cls not in extraction_functions:
pontibus_hint = (
" This is a pontibus protocol and pontibus is either not installed or not compatible."
if protocol_cls.__module__.startswith("pontibus")
else ""
)
raise ValueError(
f"Unsupported protocol {protocol_cls.__name__}; "
f"{pontibus_hint} "
"pontibus protocols require pontibus to be installed."
)

extract_func = extraction_functions[protocol_cls]
result_data = extract_func(transformation, dag_results_list)
raw_results[protocol_cls].append(result_data)

if RelativeHybridTopologyProtocol in raw_results:
raw_results[RelativeHybridTopologyProtocol] = _combine_hybrid_topology_results(
raw_results[RelativeHybridTopologyProtocol]
)
if HybridTopProtocol in raw_results:
if HybridTopProtocol is not None and HybridTopProtocol in raw_results:
raw_results[HybridTopProtocol] = _combine_hybrid_topology_results(
raw_results[HybridTopProtocol]
)
Expand Down Expand Up @@ -375,9 +397,13 @@ def run_generate_results(
archive_results_by_protocol = _extract_results_from_archive(alchemical_archive)

# raise an error if we find specific mixes of protocols
if ASFEProtocol in archive_results_by_protocol and (
HybridTopProtocol in archive_results_by_protocol
or RelativeHybridTopologyProtocol in archive_results_by_protocol
if (
ASFEProtocol is not None
and ASFEProtocol in archive_results_by_protocol
and (
HybridTopProtocol in archive_results_by_protocol
or RelativeHybridTopologyProtocol in archive_results_by_protocol
)
):
raise RuntimeError(
f"Found a mix of ASFE and Hybrid Topology protocols in the archive. This is not supported. Found protocols: {list(archive_results_by_protocol.keys())}"
Expand All @@ -404,7 +430,7 @@ def run_generate_results(
logger.info("System groups and names are set per archive from systems input")

# workout what we are going to do based on the found protocols
if ASFEProtocol in archive_results_by_protocol:
if ASFEProtocol is not None and ASFEProtocol in archive_results_by_protocol:
# if its just absolute values add them to the DGs and be done
for result in archive_results_by_protocol[ASFEProtocol]:
if final_system_group is not None:
Expand Down
32 changes: 22 additions & 10 deletions openfe_benchmarks/scripts/prepare_metadata_submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,14 @@
from openfe.protocols.openmm_septop import SepTopProtocol
from openfe.protocols.openmm_afe import AbsoluteSolvationProtocol
from openfe.protocols.openmm_utils.system_validation import get_alchemical_components
from pontibus.protocols.relative import HybridTopProtocol
from pontibus.protocols.solvation import ASFEProtocol

try:
from pontibus.protocols.relative import HybridTopProtocol
from pontibus.protocols.solvation import ASFEProtocol
except ImportError: # pontibus is an optional dependency
HybridTopProtocol = None
ASFEProtocol = None


from openfe_benchmarks.data import BenchmarkIndex
from openfe_benchmarks.results import BenchmarkResults
Expand All @@ -46,11 +52,13 @@
# map the protocol to a calculation type for the purposes of metadata aggregation and summary
_PROTOCOL_MAPPING = {
RelativeHybridTopologyProtocol: "rbfe",
HybridTopProtocol: "rbfe",
SepTopProtocol: "septop",
ASFEProtocol: "asfe",
AbsoluteSolvationProtocol: "asfe",
}
if HybridTopProtocol is not None:
_PROTOCOL_MAPPING[HybridTopProtocol] = "rbfe"
if ASFEProtocol is not None:
_PROTOCOL_MAPPING[ASFEProtocol] = "asfe"


@dataclass(frozen=True)
Expand Down Expand Up @@ -392,15 +400,19 @@ def _detect_mode(transformations: list[Transformation]) -> str:
"""
# grab the first one here we are assuming that all transformations in the archive are of the same type
protocol_cls = transformations[0].protocol.__class__
mode_key = _PROTOCOL_MAPPING.get(protocol_cls, None)

if mode_key is None:
if protocol_cls not in _PROTOCOL_MAPPING:
pontibus_hint = (
" This is a pontibus protocol and pontibus is either not installed or not compatible."
if protocol_cls.__module__.startswith("pontibus")
else ""
)
raise ValueError(
"Unable to detect calculation from transformation protocol. "
f"Observed protocol class: {protocol_cls}. "
f"Known classes: {''.join(cls.__name__ for cls in _PROTOCOL_MAPPING.keys())}."
f"Observed protocol class: {protocol_cls}."
f"{pontibus_hint} "
f"Known classes: {', '.join(cls.__name__ for cls in _PROTOCOL_MAPPING.keys())}."
)
return mode_key
return _PROTOCOL_MAPPING[protocol_cls]


def _get_mapping_annotations(trans: Transformation) -> dict[str, object]:
Expand Down
13 changes: 13 additions & 0 deletions openfe_benchmarks/tests/test_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
test performs no output validation or cleanup.
"""

import importlib.util
import runpy
import multiprocessing as mp
import os
Expand All @@ -17,6 +18,11 @@

TEST_DEBUG = False # set to True to avoid leaving outputs behind
SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "scripts"
PONTIBUS_AVAILABLE = importlib.util.find_spec("pontibus") is not None
PONTIBUS_SCRIPTS = [
"openfe_benchmarks/scripts/_example_plan_asfe.py",
"openfe_benchmarks/scripts/_example_plan_rbfe_pontibus.py",
]


def collect_example_scripts():
Expand Down Expand Up @@ -52,6 +58,13 @@ def test_example_scripts_run_as_main(script_path: Path):
The test runs each example in an isolated process with a 120s timeout and
fails if the script raises or times out.
"""
repo_root = SCRIPTS_DIR.parent.parent
if (
not PONTIBUS_AVAILABLE
and script_path.relative_to(repo_root).as_posix() in PONTIBUS_SCRIPTS
):
pytest.skip("pontibus is not installed")

queue: mp.Queue = mp.Queue()
proc = mp.Process(target=_run_script_as_main, args=(script_path, queue))
proc.start()
Expand Down
Loading