Skip to content

Commit

Permalink
Release 4.3.0
Browse files Browse the repository at this point in the history
  • Loading branch information
klrn266 committed May 23, 2024
1 parent 1667b7f commit d106e4e
Show file tree
Hide file tree
Showing 62 changed files with 5,355 additions and 2,373 deletions.
29 changes: 28 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# CHANGELOG

## Version 4.3.0 2024-05-22

### Features

- Focussed bonds feature has been created to allow users to choose bonds to freeze and break.
- Functionality has been added to run the broken bonds scorer with MCTS.
- A disconnection-aware Chemformer expansion strategy has been introduced in the plugins.
- Weights for single expansion policies can now be provided as input through the config file.
- The priors returned from the multi-expansion strategy have been rescaled.
- Functionality to mask reaction templates is now supported in the `TemplateBasedExpansionStrategy`.
- The `TemplateBasedDirectExpansionStrategy` has been implemented to directly apply the template in the search process.
- Added support for the C++ version of RDChiral.
- The multi-objective MCTS core algorithm is now implemented within the MCTS search functionality.
- A separate GUI component has been introduced for doing MO tree analysis. Additional functionalities have been added to GUI widgets to set two rewards/objectives to MCTS search. Pareto front is automatically plotted if MO-MCTS is run. Route re-ordering is automatically disabled for MO-MCTS.
- Preprocessing of the tree search can now be done using `aizynthcli`.
- `ReagentConnectAvailabilityQuery` can be used as an internal stock query class that uses the CAG client for querying reagent connect for availability.
- The `StockAvailablityScorer` has been updated such that it takes and additional `other_source_score` parameter.
- A `cutoff_number` parameter can be provided to the multi-expansion strategy to obtain only the top predictions.
- A `BrokenBondsScorer` has been created for scoring nodes and reaction trees based on the breaking of atom bonds.
- A `RouteSimilarityScorer` has been created for scoring based on an LSTM model for computing Tree Edit Distance to a set of reference routes.
- A `DeltaSyntheticComplexityScorer` has been created for scoring nodes based on the delta-synthetic-complexity of the node and its parent 'horizon' steps up in the tree.

### Bug-fixes

- Fixed an issue of sending multiple fingerprints to the GPCR Tensorflow serving model.
- `aizynthcli` has been fixed after updating with multi-objective analysis such that the tool accurately informs the user if a target is solved or not.

## Version 4.0.0 2023-11-30

### Features
Expand Down Expand Up @@ -283,7 +310,7 @@
- Add tools to train filter policy
- Add logic to prevent cycle forming in MCTS by rejecting creation of parent molecule when expanding
- Introduce new `context` subpackage that contains the `config`, `stock`, `policy` and `scoring` modules
- The `Stock`, `ExpansionPolicy`, `FilterPolicy` and `ScorerCollection` classes now has a common interface for selection and loading
- The `Stock`, `ExpansionPolicy`, `FilterPolicy` and `ScorerCollection` classes now has a common interface for selection and loading
- Introduce possibility to remove unsantizable reactions from template library when training
- Catch exceptions from RDChiral more gracefully

Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![License](https://img.shields.io/github/license/MolecularAI/aizynthfinder)](https://github.com/MolecularAI/aizynthfinder/blob/master/LICENSE)
[![Tests](https://github.com/MolecularAI/aizynthfinder/workflows/tests/badge.svg)](https://github.com/MolecularAI/aizynthfinder/actions?workflow=tests)
[![codecov](https://codecov.io/gh/MolecularAI/aizynthfinder/branch/master/graph/badge.svg)](https://codecov.io/gh/MolecularAI/aizynthfinder)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black)
[![version](https://img.shields.io/github/v/release/MolecularAI/aizynthfinder)](https://github.com/MolecularAI/aizynthfinder/releases)
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/MolecularAI/aizynthfinder/blob/master/contrib/notebook.ipynb)

Expand All @@ -28,7 +28,7 @@ The tool has been developed on a Linux platform, but the software has been teste

First time, execute the following command in a console or an Anaconda prompt

conda create "python>=3.8,<3.10" -n aizynth-env
conda create "python>=3.9,<3.11" -n aizynth-env

To install, activate the environment and install the package using pypi

Expand All @@ -43,12 +43,12 @@ for a smaller package, without all the functionality, you can also type

First clone the repository using Git.

Then execute the following commands in the root of the repository
Then execute the following commands in the root of the repository

conda env create -f env-dev.yml
conda activate aizynth-dev
poetry install --all-extras

the `aizynthfinder` package is now installed in editable mode.


Expand All @@ -68,7 +68,7 @@ To use the tool you need
1. A stock file
2. A trained expansion policy network
3. A trained filter policy network (optional)

Such files can be downloaded from [figshare](https://figshare.com/articles/AiZynthFinder_a_fast_robust_and_flexible_open-source_software_for_retrosynthetic_planning/12334577) and [here](https://figshare.com/articles/dataset/A_quick_policy_to_filter_reactions_based_on_feasibility_in_AI-guided_retrosynthetic_planning/13280507) or they can be downloaded automatically using

```
Expand All @@ -91,7 +91,7 @@ Run the tests using:
The full command run on the CI server is available through an `invoke` command

invoke full-tests

### Documentation generation

The documentation is generated by Sphinx from hand-written tutorials and docstrings
Expand Down
102 changes: 89 additions & 13 deletions aizynthfinder/aizynthfinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
)
from aizynthfinder.chem import FixedRetroReaction, Molecule, TreeMolecule
from aizynthfinder.context.config import Configuration
from aizynthfinder.context.scoring import CombinedScorer
from aizynthfinder.context.policy import BondFilter
from aizynthfinder.context.scoring import BrokenBondsScorer, CombinedScorer
from aizynthfinder.reactiontree import ReactionTreeFromExpansion
from aizynthfinder.search.andor_trees import AndOrSearchTreeBase
from aizynthfinder.search.mcts import MctsSearchTree
Expand Down Expand Up @@ -82,6 +83,9 @@ def __init__(
self.search_stats: StrDict = dict()
self.routes = RouteCollection([])
self.analysis: Optional[TreeAnalysis] = None
self._num_objectives = len(
self.config.search.algorithm_config.get("search_rewards", [])
)

@property
def target_smiles(self) -> str:
Expand All @@ -107,7 +111,7 @@ def target_mol(self, mol: Molecule) -> None:
def build_routes(
self,
selection: Optional[RouteSelectionArguments] = None,
scorer: Optional[str] = None,
scorer: Optional[Union[str, List[str]]] = None,
) -> None:
"""
Build reaction routes
Expand All @@ -116,18 +120,10 @@ def build_routes(
to extract results from the tree search.
:param selection: the selection criteria for the routes
:param scorer: a reference to the object used to score the nodes
:param scorer: a reference to the object used to score the nodes, can be a list
:raises ValueError: if the search tree not initialized
"""

scorer = scorer or self.config.post_processing.route_scorer

if not self.tree:
raise ValueError("Search tree not initialized")

_scorer = self.scorers[scorer]

self.analysis = TreeAnalysis(self.tree, scorer=_scorer)
self.analysis = self._setup_analysis(scorer=scorer)
config_selection = RouteSelectionArguments(
nmin=self.config.post_processing.min_routes,
nmax=self.config.post_processing.max_routes,
Expand Down Expand Up @@ -174,9 +170,13 @@ def prepare_tree(self) -> None:
self.stock.exclude(self.target_mol)
self._logger.debug("Excluding the target compound from the stock")

if self.config.search.break_bonds or self.config.search.freeze_bonds:
self._setup_focussed_bonds(self.target_mol)

self._setup_search_tree()
self.analysis = None
self.routes = RouteCollection([])
self.filter_policy.reset_cache()
self.expansion_policy.reset_cache()

def stock_info(self) -> StrDict:
Expand Down Expand Up @@ -249,8 +249,40 @@ def tree_search(self, show_progress: bool = False) -> float:
self.search_stats["time"] = time_past
return time_past

def _setup_focussed_bonds(self, target_mol: Molecule) -> None:
"""
Setup multi-objective scoring function with 'broken bonds'-scorer and
add 'frozen bonds'-filter to filter policy.
:param target_mol: the target molecule.
"""
target_mol = TreeMolecule(smiles=target_mol.smiles, parent=None)

bond_filter_key = "__finder_bond_filter"
if self.config.search.freeze_bonds:
if not target_mol.has_all_focussed_bonds(self.config.search.freeze_bonds):
raise ValueError("Bonds in 'freeze_bond' must exist in target molecule")
bond_filter = BondFilter(bond_filter_key, self.config)
self.filter_policy.load(bond_filter)
self.filter_policy.select(bond_filter_key, append=True)
elif (
self.filter_policy.selection
and bond_filter_key in self.filter_policy.selection
):
self.filter_policy.deselect(bond_filter_key)

search_rewards = self.config.search.algorithm_config.get("search_rewards")
if not search_rewards:
return

if self.config.search.break_bonds and "broken bonds" in search_rewards:
if not target_mol.has_all_focussed_bonds(self.config.search.break_bonds):
raise ValueError("Bonds in 'break_bonds' must exist in target molecule")
self.scorers.load(BrokenBondsScorer(self.config))
self._num_objectives = len(search_rewards)

def _setup_search_tree(self) -> None:
self._logger.debug("Defining tree root: %s" % self.target_smiles)
self._logger.debug(f"Defining tree root: {self.target_smiles}")
if self.config.search.algorithm.lower() == "mcts":
self.tree = MctsSearchTree(
root_smiles=self.target_smiles, config=self.config
Expand All @@ -259,6 +291,50 @@ def _setup_search_tree(self) -> None:
cls = load_dynamic_class(self.config.search.algorithm)
self.tree = cls(root_smiles=self.target_smiles, config=self.config)

def _setup_analysis(
self,
scorer: Optional[Union[str, List[str]]],
) -> TreeAnalysis:
"""Configure TreeAnalysis
:param scorer: a reference to the object used to score the nodes, can be a list
:returns: the configured TreeAnalysis
:raises ValueError: if the search tree not initialized
"""
if not self.tree:
raise ValueError("Search tree not initialized")

if scorer is None:
scorer_names = self.config.post_processing.route_scorers
# If not defined, use the same scorer as the search rewards
if not scorer_names:
search_rewards = self.config.search.algorithm_config.get(
"search_rewards"
)
scorer_names = search_rewards if search_rewards else ["state score"]

elif isinstance(scorer, str):
scorer_names = [scorer]
else:
scorer_names = list(scorer)

if "broken bonds" in scorer_names:
# Add broken bonds scorer if required
self.scorers.load(BrokenBondsScorer(self.config))

scorers = [self.scorers[name] for name in scorer_names]

if self.config.post_processing.scorer_weights:
scorers = [
CombinedScorer(
self.config,
scorer_names,
self.config.post_processing.scorer_weights,
)
]

return TreeAnalysis(self.tree, scorers)


class AiZynthExpander:
"""
Expand Down
19 changes: 12 additions & 7 deletions aizynthfinder/analysis/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""
from __future__ import annotations

import copy
from typing import TYPE_CHECKING

import numpy as np
Expand All @@ -12,12 +13,12 @@
except ImportError:
pass

from aizynthfinder.analysis import TreeAnalysis
from aizynthfinder.analysis.utils import CombinedReactionTrees, RouteSelectionArguments
from aizynthfinder.reactiontree import SUPPORT_DISTANCES, ReactionTree
from aizynthfinder.search.mcts import MctsNode, MctsSearchTree

if TYPE_CHECKING:
from aizynthfinder.analysis import TreeAnalysis
from aizynthfinder.context.scoring import Scorer
from aizynthfinder.utils.type_utils import (
Any,
Expand All @@ -26,6 +27,7 @@
PilImage,
Sequence,
StrDict,
Union,
)


Expand Down Expand Up @@ -64,11 +66,11 @@ def __init__(self, reaction_trees: Sequence[ReactionTree], **kwargs) -> None:
self._update_route_dict(self.route_metadata, "route_metadata")

self.nodes = self._unpack_kwarg_with_default("nodes", None, **kwargs)
self.scores = self._unpack_kwarg_with_default("scores", np.nan, **kwargs)
self.scores = self._unpack_kwarg_with_default("scores", dict, **kwargs)
self.all_scores = self._unpack_kwarg_with_default("all_scores", dict, **kwargs)

self._dicts: Optional[Sequence[StrDict]] = self._unpack_kwarg("dicts", **kwargs)
self._images: Optional[Sequence[PilImage]] = self._unpack_kwarg(
self._images: Optional[Sequence[Optional[PilImage]]] = self._unpack_kwarg(
"images", **kwargs
)
self._jsons: Optional[Sequence[str]] = self._unpack_kwarg("jsons", **kwargs)
Expand All @@ -80,7 +82,9 @@ def __init__(self, reaction_trees: Sequence[ReactionTree], **kwargs) -> None:

@classmethod
def from_analysis(
cls, analysis: TreeAnalysis, selection: RouteSelectionArguments = None
cls,
analysis: TreeAnalysis,
selection: Optional[RouteSelectionArguments] = None,
) -> "RouteCollection":
"""
Create a collection from a tree analysis.
Expand All @@ -90,8 +94,8 @@ def from_analysis(
:return: the created collection
"""
items, scores = analysis.sort(selection)
all_scores = [{repr(analysis.scorer): score} for score in scores]
kwargs = {"scores": scores, "all_scores": all_scores}
all_scores = copy.deepcopy(scores)
kwargs: Dict[str, Any] = {"scores": scores, "all_scores": all_scores}
if isinstance(analysis.search_tree, MctsSearchTree):
kwargs["nodes"] = items
reaction_trees = [
Expand Down Expand Up @@ -120,10 +124,11 @@ def dicts(self) -> Sequence[StrDict]:
return self._dicts

@property
def images(self) -> Sequence[PilImage]:
def images(self) -> Sequence[Optional[PilImage]]:
"""Returns a list of pictoral representation of the routes"""
if self._images is None:
self._images = self.make_images()
assert self._images is not None
return self._images

@property
Expand Down
Loading

0 comments on commit d106e4e

Please sign in to comment.