Skip to content

Commit

Permalink
Adds YAML input to CLI planning tools (#583)
Browse files Browse the repository at this point in the history
* [skip ci]

draft of yaml format for CI

* adds yaml reader for cli options input

fixes #580

* change plan_rXfe_network_main functions to expect list of mappers

* move settings yaml parsing to be parameter

* add YAML_OPTIONS as parameter

* add YAML_OPTIONS to plan_rbfe_network

currently does nothing

* cli: add interpretation of settings yaml

* cli: enable settings yaml reading in plan_rbfe_network

* cli: some extra documentation on load_yaml

* cli: fix tests to account for normalisation

* cli: add test for custom rbfe yaml usage

* cli: update plan_rhfe to allow yaml options

* cli: use v1/2 compatible Pydantic

* cli: fixup pydantic v1/2 compat

* remove draft yaml idea

* cli:  centralise parsing of yaml options and defaults

* add pyyaml to deps

---------

Co-authored-by: Irfan Alibay <[email protected]>
  • Loading branch information
richardjgowers and IAlibay authored Nov 17, 2023
1 parent 94929f5 commit c96cb57
Show file tree
Hide file tree
Showing 8 changed files with 355 additions and 40 deletions.
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies:
- pytest-cov
- pytest-rerunfailures
- pydantic >1
- pyyaml
- coverage
- cinnabar ==0.3.0
- openff-toolkit>=0.13.0
Expand Down
37 changes: 18 additions & 19 deletions openfecli/commands/plan_rbfe_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from openfecli.utils import write, print_duration
from openfecli import OFECommandPlugin
from openfecli.parameters import (
MOL_DIR, PROTEIN, MAPPER, OUTPUT_DIR, COFACTORS,
MOL_DIR, PROTEIN, MAPPER, OUTPUT_DIR, COFACTORS, YAML_OPTIONS,
)
from openfecli.plan_alchemical_networks_utils import plan_alchemical_network_output

Expand All @@ -24,8 +24,8 @@ def plan_rbfe_network_main(
Parameters
----------
mapper : LigandAtomMapper
the mapper to use to generate the mapping
mapper : list[LigandAtomMapper]
list of mappers to use to generate the mapping
mapping_scorer : Callable
scorer, that evaluates the generated mappings
ligand_network_planner : Callable
Expand All @@ -51,7 +51,7 @@ def plan_rbfe_network_main(
)

network_planner = RBFEAlchemicalNetworkPlanner(
mappers=[mapper],
mappers=mapper,
mapping_scorer=mapping_scorer,
ligand_network_planner=ligand_network_planner,
)
Expand All @@ -78,13 +78,19 @@ def plan_rbfe_network_main(
@COFACTORS.parameter(
multiple=True, required=False, default=None, help=COFACTORS.kwargs["help"]
)
@YAML_OPTIONS.parameter(
multiple=False, required=False, default=None,
help=YAML_OPTIONS.kwargs["help"],
)
@OUTPUT_DIR.parameter(
help=OUTPUT_DIR.kwargs["help"] + " Defaults to `./alchemicalNetwork`.",
default="alchemicalNetwork",
)
@print_duration
def plan_rbfe_network(
molecules: List[str], protein: str, cofactors: tuple[str], output_dir: str
molecules: List[str], protein: str, cofactors: tuple[str],
yaml_settings: str,
output_dir: str,
):
"""
Plan a relative binding free energy network, saved as JSON files for
Expand Down Expand Up @@ -115,15 +121,6 @@ def plan_rbfe_network(

write("Parsing in Files: ")

from gufe import SolventComponent
from openfe.setup.atom_mapping.lomap_scorers import (
default_lomap_score,
)
from openfe.setup import LomapAtomMapper
from openfe.setup.ligand_network_planning import (
generate_minimal_spanning_network,
)

# INPUT
write("\tGot input: ")

Expand All @@ -142,27 +139,29 @@ def plan_rbfe_network(
cofactors = []
write("\t\tCofactors: " + str(cofactors))

solvent = SolventComponent()
yaml_options = YAML_OPTIONS.get(yaml_settings)
mapper_obj = yaml_options.mapper
mapping_scorer = yaml_options.scorer
ligand_network_planner = yaml_options.ligand_network_planner
solvent = yaml_options.solvent

write("\t\tSolvent: " + str(solvent))
write("")

write("Using Options:")
mapper_obj = LomapAtomMapper(time=20, threed=True, element_change=False, max3d=1)
write("\tMapper: " + str(mapper_obj))

# TODO: write nice parameter
mapping_scorer = default_lomap_score
write("\tMapping Scorer: " + str(mapping_scorer))

# TODO: write nice parameter
ligand_network_planner = generate_minimal_spanning_network
write("\tNetworker: " + str(ligand_network_planner))
write("")

# DO
write("Planning RBFE-Campaign:")
alchemical_network, ligand_network = plan_rbfe_network_main(
mapper=mapper_obj,
mapper=[mapper_obj],
mapping_scorer=mapping_scorer,
ligand_network_planner=ligand_network_planner,
small_molecules=small_molecules,
Expand Down
35 changes: 16 additions & 19 deletions openfecli/commands/plan_rhfe_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from openfecli.utils import write, print_duration
from openfecli import OFECommandPlugin
from openfecli.parameters import (
MOL_DIR, MAPPER, OUTPUT_DIR,
MOL_DIR, MAPPER, OUTPUT_DIR, YAML_OPTIONS,
)
from openfecli.plan_alchemical_networks_utils import plan_alchemical_network_output

Expand All @@ -21,8 +21,8 @@ def plan_rhfe_network_main(
Parameters
----------
mapper : LigandAtomMapper
the mapper to use to generate the mapping
mapper : list[LigandAtomMapper]
list of mappers to use to generate the mapping
mapping_scorer : Callable
scorer, that evaluates the generated mappings
ligand_network_planner : Callable
Expand All @@ -43,7 +43,7 @@ def plan_rhfe_network_main(
)

network_planner = RHFEAlchemicalNetworkPlanner(
mappers=[mapper],
mappers=mapper,
mapping_scorer=mapping_scorer,
ligand_network_planner=ligand_network_planner,
)
Expand All @@ -64,12 +64,16 @@ def plan_rhfe_network_main(
@MOL_DIR.parameter(
required=True, help=MOL_DIR.kwargs["help"] + " Any number of sdf paths."
)
@YAML_OPTIONS.parameter(
multiple=False, required=False, default=None,
help=YAML_OPTIONS.kwargs["help"],
)
@OUTPUT_DIR.parameter(
help=OUTPUT_DIR.kwargs["help"] + " Defaults to `./alchemicalNetwork`.",
default="alchemicalNetwork",
)
@print_duration
def plan_rhfe_network(molecules: List[str], output_dir: str):
def plan_rhfe_network(molecules: List[str], yaml_settings: str, output_dir: str):
"""
Plan a relative hydration free energy network, saved as JSON files for
the quickrun command.
Expand Down Expand Up @@ -102,15 +106,6 @@ def plan_rhfe_network(molecules: List[str], output_dir: str):

write("Parsing in Files: ")

from gufe import SolventComponent
from openfe.setup.atom_mapping.lomap_scorers import (
default_lomap_score,
)
from openfe.setup import LomapAtomMapper
from openfe.setup.ligand_network_planning import (
generate_minimal_spanning_network,
)

# INPUT
write("\tGot input: ")

Expand All @@ -120,27 +115,29 @@ def plan_rhfe_network(molecules: List[str], output_dir: str):
+ " ".join([str(sm) for sm in small_molecules])
)

solvent = SolventComponent()
yaml_options = YAML_OPTIONS.get(yaml_settings)
mapper_obj = yaml_options.mapper
mapping_scorer = yaml_options.scorer
ligand_network_planner = yaml_options.ligand_network_planner
solvent = yaml_options.solvent

write("\t\tSolvent: " + str(solvent))
write("")

write("Using Options:")
mapper_obj = LomapAtomMapper(time=20, threed=True, element_change=False, max3d=1)
write("\tMapper: " + str(mapper_obj))

# TODO: write nice parameter
mapping_scorer = default_lomap_score
write("\tMapping Scorer: " + str(mapping_scorer))

# TODO: write nice parameter
ligand_network_planner = generate_minimal_spanning_network
write("\tNetworker: " + str(ligand_network_planner))
write("")

# DO
write("Planning RHFE-Campaign:")
alchemical_network, ligand_network = plan_rhfe_network_main(
mapper=mapper_obj,
mapper=[mapper_obj],
mapping_scorer=mapping_scorer,
ligand_network_planner=ligand_network_planner,
small_molecules=small_molecules,
Expand Down
1 change: 1 addition & 0 deletions openfecli/parameters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
from .output_dir import OUTPUT_DIR
from .protein import PROTEIN
from .molecules import MOL_DIR, COFACTORS
from .plan_network_options import YAML_OPTIONS
179 changes: 179 additions & 0 deletions openfecli/parameters/plan_network_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# This code is part of OpenFE and is licensed under the MIT license.
# For details, see https://github.com/OpenFreeEnergy/openfe
"""Pydantic models for the definition of advanced CLI options
"""
import click
from collections import namedtuple
try:
# todo; once we're fully v2, we can use ConfigDict not nested class
from pydantic.v1 import BaseModel # , ConfigDict
except ImportError:
from pydantic import BaseModel
from plugcli.params import Option
from typing import Any, Optional
import yaml
import warnings


PlanNetworkOptions = namedtuple('PlanNetworkOptions',
['mapper', 'scorer',
'ligand_network_planner', 'solvent'])


class MapperSelection(BaseModel):
# model_config = ConfigDict(extra='allow', str_to_lower=True)
class Config:
extra = 'allow'
anystr_lower = True

method: Optional[str] = None
settings: dict[str, Any] = {}


class NetworkSelection(BaseModel):
# model_config = ConfigDict(extra='allow', str_to_lower=True)
class Config:
extra = 'allow'
anystr_lower = True

method: Optional[str] = None
settings: dict[str, Any] = {}


class CliYaml(BaseModel):
# model_config = ConfigDict(extra='allow')
class Config:
extra = 'allow'

mapper: Optional[MapperSelection] = None
network: Optional[NetworkSelection] = None


def parse_yaml_planner_options(contents: str) -> CliYaml:
"""Parse and minimally validate a user provided yaml
Parameters
----------
contents : str
raw yaml formatted input to parse
Returns
-------
options : CliOptions
will have keys for mapper and network topology choices
Raises
------
ValueError
for any malformed inputs
"""
raw = yaml.safe_load(contents)

if False:
# todo: warnings about extra fields we don't expect?
expected = {'mapper', 'network'}
for field in raw:
if field in expected:
continue
warnings.warn(f"Ignoring unexpected section: '{field}'")

return CliYaml(**raw)


def load_yaml_planner_options(path: Optional[str], context) -> PlanNetworkOptions:
"""Load cli options from yaml file path and resolve these to objects
Parameters
----------
path : str
path to the yaml file
context
unused
Returns
-------
PlanNetworkOptions : namedtuple
a namedtuple with fields 'mapper', 'scorer', 'network_planning_algorithm',
and 'solvent' fields.
these fields each hold appropriate objects ready for use
"""
from gufe import SolventComponent
from openfe.setup.ligand_network_planning import (
generate_radial_network,
generate_minimal_spanning_network,
generate_maximal_network,
generate_minimal_redundant_network,
)
from openfe.setup import (
LomapAtomMapper,
)
from openfe.setup.atom_mapping.lomap_scorers import (
default_lomap_score,
)
from functools import partial

if path is not None:
with open(path, 'r') as f:
raw = f.read()

# convert raw yaml to normalised pydantic model
opt = parse_yaml_planner_options(raw)
else:
opt = None

# convert normalised inputs to objects
if opt and opt.mapper:
mapper_choices = {
'lomap': LomapAtomMapper,
'lomapatommapper': LomapAtomMapper,
}

try:
cls = mapper_choices[opt.mapper.method]
except KeyError:
raise KeyError(f"Bad mapper choice: '{opt.mapper.method}'")
mapper_obj = cls(**opt.mapper.settings)
else:
mapper_obj = LomapAtomMapper(time=20, threed=True, element_change=False,
max3d=1)

# todo: choice of scorer goes here
mapping_scorer = default_lomap_score

if opt and opt.network:
network_choices = {
'generate_radial_network': generate_radial_network,
'radial': generate_radial_network,
'generate_minimal_spanning_network': generate_minimal_spanning_network,
'mst': generate_minimal_spanning_network,
'generate_minimal_redundant_network': generate_minimal_redundant_network,
'generate_maximal_network': generate_maximal_network,
}

try:
func = network_choices[opt.network.method]
except KeyError:
raise KeyError(f"Bad network algorithm choice: '{opt.network.method}'")

ligand_network_planner = partial(func, **opt.network.settings)
else:
ligand_network_planner = generate_minimal_spanning_network

# todo: choice of solvent goes here
solvent = SolventComponent()

return PlanNetworkOptions(
mapper_obj,
mapping_scorer,
ligand_network_planner,
solvent,
)


YAML_OPTIONS = Option(
'-s', "--settings", "yaml_settings",
type=click.Path(exists=True, dir_okay=False),
help="Path to planning settings yaml file.",
getter=load_yaml_planner_options,
)
Loading

0 comments on commit c96cb57

Please sign in to comment.