Skip to content

Commit

Permalink
Merge branch 'main' into oechemless-registry
Browse files Browse the repository at this point in the history
  • Loading branch information
IAlibay authored Mar 1, 2024
2 parents 8b688f7 + afdffb1 commit 076117e
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 13 deletions.
10 changes: 2 additions & 8 deletions docs/guide/cli.rst → docs/guide/cli/cli_basics.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
Using the CLI
=============

In addition to the powerful Python API, OpenFE provides a simple command
line interface to facilitate some more common (and less complicated) tasks.
The Python API tries to be as easy to use as possible, but the CLI provides
wrappers around some parts of the Python API to make it easier to integrate
into non-Python workflows.
CLI basics
==========

The ``openfe`` command consists of several subcommands. This is similar to
tools like ``gmx``, which has subcommands like ``gmx mdrun``, or ``conda``,
Expand Down
78 changes: 78 additions & 0 deletions docs/guide/cli/cli_yaml.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
Customising CLI planning with yaml settings
===========================================

The planning commands in the CLI can be made more powerful by supplying
"yaml" formatted files to customise the planning algorithms.
This settings file has a series of sections for customising the different algorithms,
as an example, the settings file which re-specifies the default behaviour would look like ::

network:
method: plan_minimal_spanning_tree
mapper:
method: LomapAtomMapper
settings:
time: 1
threed: True
max3d: 0.95
element_change: True

The name of the algorithm is given behind the ``method:`` key and the arguments to the
algorithm are then optionally given behind the ``settings:`` key.
Both the `network:` and `mapper:` sections are optional.

This is then provided to the ``openfe plan-rbfe-network`` command as ::

openfe plan-rbfe-network -M molecules.sdf -P protein.pdb -s settings.yaml

Customising the atom mapper
---------------------------

There is a choice to be made as to which atom mapper is used,
currently included are the :class:`.LomapAtomMapper` and the :class:`.KartografAtomMapper`
For example to switch to using the ``Kartograf`` atom mapper, this settings yaml could be used ::

mapper:
method: KartografAtomMapper
settings:
atom_max_distance: 0.95
atom_map_hydrogens: True
map_hydrogens_on_hydrogens_only: False
map_exact_ring_matches_only: True

Full details on these options can be found in the `Kartograf documentation`_.

.. _Kartograf documentation: https://kartograf.readthedocs.io/en/latest/api/kartograf.mappers.html#kartograf.atom_mapper.KartografAtomMapper

Customising the network planner
-------------------------------

There are a variety of network planning options available, including
:func:`.generate_radial_network`,
:func:`.generate_minimal_spanning_network`, and
:func:`.generate_minimal_redundant_network`.

For example to plan a radial network using a ligand called 'CHEMBL1078774' as the central ligand, this settings yaml
could be given ::

network:
method: generate_radial_network
settings:
central_ligand: CHEMBL1078774

Where the required ``central_ligand`` argument has been passed inside the ``settings:`` section.

Note that there is a subtle distinction when ligand names could be interpreted as integers.
To select the first ligand, the **integer** 0 can be given ::

network:
method: generate_radial_network
settings:
central_ligand: 0

Whereas if we wanted to specify the ligand named "0", we would instead explicitly pass this as **a string** to the yaml
settings file ::

network:
method: generate_radial_network
settings:
central_ligand: '0'
13 changes: 13 additions & 0 deletions docs/guide/cli/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CLI Interface
=============

In addition to the powerful Python API, OpenFE provides a simple command
line interface to facilitate some more common (and less complicated) tasks.
The Python API tries to be as easy to use as possible, but the CLI provides
wrappers around some parts of the Python API to make it easier to integrate
into non-Python workflows.


.. toctree::
cli_basics
cli_yaml
2 changes: 1 addition & 1 deletion docs/guide/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ User Guide
setup/index
execution/index
results/index
cli
cli/index
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ The **OpenFE** toolkit provides open-source frameworks for calculating alchemica
.. grid-item-card:: Using the CLI
:img-top: _static/CLI.svg
:text-align: center
:link: guide/cli
:link: guide/cli/index
:link-type: doc

Reference guide on using the OpenFE CLI.
Expand Down
27 changes: 24 additions & 3 deletions openfe/setup/ligand_network_planning.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def _hasten_lomap(mapper, ligands):

def generate_radial_network(
ligands: Iterable[SmallMoleculeComponent],
central_ligand: SmallMoleculeComponent,
central_ligand: Union[SmallMoleculeComponent, str, int],
mappers: Union[AtomMapper, Iterable[AtomMapper]],
scorer: Optional[Callable[[LigandAtomMapping], float]] = None,
) -> LigandNetwork:
Expand All @@ -54,8 +54,10 @@ def generate_radial_network(
ligands : iterable of SmallMoleculeComponents
the ligands to arrange around the central ligand. If the central ligand
is present it will be ignored (i.e. avoiding a self edge)
central_ligand : SmallMoleculeComponent
the ligand to use as the hub/central ligand
central_ligand : SmallMoleculeComponent or str or int
the ligand to use as the hub/central ligand.
If this is a string, this should match to one and only one ligand name.
If this is an integer, this refers to the index from within ligands
mappers : AtomMapper or iterable of AtomMappers
mapper(s) to use, at least 1 required
scorer : scoring function, optional
Expand Down Expand Up @@ -83,6 +85,25 @@ def generate_radial_network(
mappers = [_hasten_lomap(m, ligands) if isinstance(m, LomapAtomMapper)
else m for m in mappers]

# handle central_ligand arg possibilities
# after this, central_ligand is resolved to a SmallMoleculeComponent
if isinstance(central_ligand, int):
ligands = list(ligands)
try:
central_ligand = ligands[central_ligand]
except IndexError:
raise ValueError(f"index '{central_ligand}' out of bounds, there are "
f"{len(ligands)} ligands")
elif isinstance(central_ligand, str):
ligands = list(ligands)
possibles = [l for l in ligands if l.name == central_ligand]
if not possibles:
raise ValueError(f"No ligand called '{central_ligand}' "
f"available: {', '.join(l.name for l in ligands)}")
if len(possibles) > 1:
raise ValueError(f"Multiple ligands called '{central_ligand}'")
central_ligand = possibles[0]

edges = []

for ligand in ligands:
Expand Down
57 changes: 57 additions & 0 deletions openfe/tests/setup/test_network_planning.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,63 @@ def test_radial_network(atom_mapping_basic_test_files, toluene_vs_others,
for mapping in network.edges)


@pytest.mark.parametrize('central_ligand_arg', [0, 'toluene'])
def test_radial_network_int_str(atom_mapping_basic_test_files, toluene_vs_others,
central_ligand_arg):
# check that passing either an integer or string to radial network still works
toluene, others = toluene_vs_others
ligands = [toluene] + others

network = openfe.setup.ligand_network_planning.generate_radial_network(
ligands=ligands, central_ligand=central_ligand_arg,
mappers=openfe.setup.LomapAtomMapper(), scorer=None,
)
assert len(network.nodes) == len(ligands)
assert len(network.edges) == len(others)
# check that all ligands are present, i.e. we included everyone
ligands_in_network = {mol.name for mol in network.nodes}
assert ligands_in_network == set(atom_mapping_basic_test_files.keys())
# check that every edge has the central ligand within
assert all(('toluene' in {mapping.componentA.name, mapping.componentB.name})
for mapping in network.edges)


def test_radial_network_bad_str(toluene_vs_others):
# check failure on missing name
toluene, others = toluene_vs_others
ligands = [toluene] + others

with pytest.raises(ValueError, match='No ligand called'):
network = openfe.setup.ligand_network_planning.generate_radial_network(
ligands=ligands, central_ligand='unobtainium',
mappers=openfe.setup.LomapAtomMapper(), scorer=None,
)


def test_radial_network_multiple_str(toluene_vs_others):
# check failure on multiple of specified name, it's ambiguous
toluene, others = toluene_vs_others
ligands = [toluene, toluene] + others

with pytest.raises(ValueError, match='Multiple ligands called'):
network = openfe.setup.ligand_network_planning.generate_radial_network(
ligands=ligands, central_ligand='toluene',
mappers=openfe.setup.LomapAtomMapper(), scorer=None,
)


def test_radial_network_index_error(toluene_vs_others):
# check if we ask for a out of bounds ligand we get a meaningful failure
toluene, others = toluene_vs_others
ligands = [toluene] + others

with pytest.raises(ValueError, match='out of bounds'):
network = openfe.setup.ligand_network_planning.generate_radial_network(
ligands=ligands, central_ligand=2077,
mappers=openfe.setup.LomapAtomMapper(), scorer=None,
)


def test_radial_network_self_central(toluene_vs_others):
# issue #544, include the central ligand in "ligands",
# shouldn't get self edge
Expand Down
39 changes: 39 additions & 0 deletions openfecli/tests/commands/test_plan_rbfe_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,42 @@ def test_custom_yaml_plan_rbfe_smoke_test(custom_yaml_settings, eg5_files, tmpdi
result = runner.invoke(plan_rbfe_network, args)

assert result.exit_code == 0


@pytest.fixture
def custom_yaml_radial():
return """\
network:
method: generate_radial_network
settings:
central_ligand: lig_CHEMBL1078774
mapper:
method: LomapAtomMapper
settings:
time: 45
element_change: True
"""


def test_custom_yaml_plan_radial_smoke_test(custom_yaml_radial, eg5_files, tmpdir):
protein, ligand, cofactor = eg5_files
settings_path = tmpdir / "settings.yaml"
with open(settings_path, "w") as f:
f.write(custom_yaml_radial)

assert settings_path.exists()

args = [
'-p', protein,
'-M', ligand,
'-C', cofactor,
'-s', settings_path,
]

runner = CliRunner()

with runner.isolated_filesystem():
result = runner.invoke(plan_rbfe_network, args)

assert result.exit_code == 0

0 comments on commit 076117e

Please sign in to comment.