Skip to content

Commit 0a46177

Browse files
authored
Merge pull request #156 from MolecularAI/release-4.3.0
Release 4.3.0
2 parents 1667b7f + 4043e9d commit 0a46177

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+5354
-2373
lines changed

CHANGELOG.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
# CHANGELOG
22

3+
## Version 4.3.0 2024-05-22
4+
5+
### Features
6+
7+
- Focussed bonds feature has been created to allow users to choose bonds to freeze and break.
8+
- Functionality has been added to run the broken bonds scorer with MCTS.
9+
- A disconnection-aware Chemformer expansion strategy has been introduced in the plugins.
10+
- Weights for single expansion policies can now be provided as input through the config file.
11+
- The priors returned from the multi-expansion strategy have been rescaled.
12+
- Functionality to mask reaction templates is now supported in the `TemplateBasedExpansionStrategy`.
13+
- The `TemplateBasedDirectExpansionStrategy` has been implemented to directly apply the template in the search process.
14+
- Added support for the C++ version of RDChiral.
15+
- The multi-objective MCTS core algorithm is now implemented within the MCTS search functionality.
16+
- 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.
17+
- Preprocessing of the tree search can now be done using `aizynthcli`.
18+
- The `StockAvailablityScorer` has been updated such that it takes and additional `other_source_score` parameter.
19+
- A `cutoff_number` parameter can be provided to the multi-expansion strategy to obtain only the top predictions.
20+
- A `BrokenBondsScorer` has been created for scoring nodes and reaction trees based on the breaking of atom bonds.
21+
- A `RouteSimilarityScorer` has been created for scoring based on an LSTM model for computing Tree Edit Distance to a set of reference routes.
22+
- 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.
23+
24+
### Bug-fixes
25+
26+
- Fixed an issue of sending multiple fingerprints to the GPCR Tensorflow serving model.
27+
- `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.
28+
329
## Version 4.0.0 2023-11-30
430

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

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![License](https://img.shields.io/github/license/MolecularAI/aizynthfinder)](https://github.com/MolecularAI/aizynthfinder/blob/master/LICENSE)
44
[![Tests](https://github.com/MolecularAI/aizynthfinder/workflows/tests/badge.svg)](https://github.com/MolecularAI/aizynthfinder/actions?workflow=tests)
55
[![codecov](https://codecov.io/gh/MolecularAI/aizynthfinder/branch/master/graph/badge.svg)](https://codecov.io/gh/MolecularAI/aizynthfinder)
6-
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black)
6+
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black)
77
[![version](https://img.shields.io/github/v/release/MolecularAI/aizynthfinder)](https://github.com/MolecularAI/aizynthfinder/releases)
88
[![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)
99

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

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

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

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

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

4444
First clone the repository using Git.
4545

46-
Then execute the following commands in the root of the repository
46+
Then execute the following commands in the root of the repository
4747

4848
conda env create -f env-dev.yml
4949
conda activate aizynth-dev
5050
poetry install --all-extras
51-
51+
5252
the `aizynthfinder` package is now installed in editable mode.
5353

5454

@@ -68,7 +68,7 @@ To use the tool you need
6868
1. A stock file
6969
2. A trained expansion policy network
7070
3. A trained filter policy network (optional)
71-
71+
7272
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
7373

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

9393
invoke full-tests
94-
94+
9595
### Documentation generation
9696

9797
The documentation is generated by Sphinx from hand-written tutorials and docstrings

aizynthfinder/aizynthfinder.py

Lines changed: 89 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
)
1616
from aizynthfinder.chem import FixedRetroReaction, Molecule, TreeMolecule
1717
from aizynthfinder.context.config import Configuration
18-
from aizynthfinder.context.scoring import CombinedScorer
18+
from aizynthfinder.context.policy import BondFilter
19+
from aizynthfinder.context.scoring import BrokenBondsScorer, CombinedScorer
1920
from aizynthfinder.reactiontree import ReactionTreeFromExpansion
2021
from aizynthfinder.search.andor_trees import AndOrSearchTreeBase
2122
from aizynthfinder.search.mcts import MctsSearchTree
@@ -82,6 +83,9 @@ def __init__(
8283
self.search_stats: StrDict = dict()
8384
self.routes = RouteCollection([])
8485
self.analysis: Optional[TreeAnalysis] = None
86+
self._num_objectives = len(
87+
self.config.search.algorithm_config.get("search_rewards", [])
88+
)
8589

8690
@property
8791
def target_smiles(self) -> str:
@@ -107,7 +111,7 @@ def target_mol(self, mol: Molecule) -> None:
107111
def build_routes(
108112
self,
109113
selection: Optional[RouteSelectionArguments] = None,
110-
scorer: Optional[str] = None,
114+
scorer: Optional[Union[str, List[str]]] = None,
111115
) -> None:
112116
"""
113117
Build reaction routes
@@ -116,18 +120,10 @@ def build_routes(
116120
to extract results from the tree search.
117121
118122
:param selection: the selection criteria for the routes
119-
:param scorer: a reference to the object used to score the nodes
123+
:param scorer: a reference to the object used to score the nodes, can be a list
120124
:raises ValueError: if the search tree not initialized
121125
"""
122-
123-
scorer = scorer or self.config.post_processing.route_scorer
124-
125-
if not self.tree:
126-
raise ValueError("Search tree not initialized")
127-
128-
_scorer = self.scorers[scorer]
129-
130-
self.analysis = TreeAnalysis(self.tree, scorer=_scorer)
126+
self.analysis = self._setup_analysis(scorer=scorer)
131127
config_selection = RouteSelectionArguments(
132128
nmin=self.config.post_processing.min_routes,
133129
nmax=self.config.post_processing.max_routes,
@@ -174,9 +170,13 @@ def prepare_tree(self) -> None:
174170
self.stock.exclude(self.target_mol)
175171
self._logger.debug("Excluding the target compound from the stock")
176172

173+
if self.config.search.break_bonds or self.config.search.freeze_bonds:
174+
self._setup_focussed_bonds(self.target_mol)
175+
177176
self._setup_search_tree()
178177
self.analysis = None
179178
self.routes = RouteCollection([])
179+
self.filter_policy.reset_cache()
180180
self.expansion_policy.reset_cache()
181181

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

252+
def _setup_focussed_bonds(self, target_mol: Molecule) -> None:
253+
"""
254+
Setup multi-objective scoring function with 'broken bonds'-scorer and
255+
add 'frozen bonds'-filter to filter policy.
256+
257+
:param target_mol: the target molecule.
258+
"""
259+
target_mol = TreeMolecule(smiles=target_mol.smiles, parent=None)
260+
261+
bond_filter_key = "__finder_bond_filter"
262+
if self.config.search.freeze_bonds:
263+
if not target_mol.has_all_focussed_bonds(self.config.search.freeze_bonds):
264+
raise ValueError("Bonds in 'freeze_bond' must exist in target molecule")
265+
bond_filter = BondFilter(bond_filter_key, self.config)
266+
self.filter_policy.load(bond_filter)
267+
self.filter_policy.select(bond_filter_key, append=True)
268+
elif (
269+
self.filter_policy.selection
270+
and bond_filter_key in self.filter_policy.selection
271+
):
272+
self.filter_policy.deselect(bond_filter_key)
273+
274+
search_rewards = self.config.search.algorithm_config.get("search_rewards")
275+
if not search_rewards:
276+
return
277+
278+
if self.config.search.break_bonds and "broken bonds" in search_rewards:
279+
if not target_mol.has_all_focussed_bonds(self.config.search.break_bonds):
280+
raise ValueError("Bonds in 'break_bonds' must exist in target molecule")
281+
self.scorers.load(BrokenBondsScorer(self.config))
282+
self._num_objectives = len(search_rewards)
283+
252284
def _setup_search_tree(self) -> None:
253-
self._logger.debug("Defining tree root: %s" % self.target_smiles)
285+
self._logger.debug(f"Defining tree root: {self.target_smiles}")
254286
if self.config.search.algorithm.lower() == "mcts":
255287
self.tree = MctsSearchTree(
256288
root_smiles=self.target_smiles, config=self.config
@@ -259,6 +291,50 @@ def _setup_search_tree(self) -> None:
259291
cls = load_dynamic_class(self.config.search.algorithm)
260292
self.tree = cls(root_smiles=self.target_smiles, config=self.config)
261293

294+
def _setup_analysis(
295+
self,
296+
scorer: Optional[Union[str, List[str]]],
297+
) -> TreeAnalysis:
298+
"""Configure TreeAnalysis
299+
300+
:param scorer: a reference to the object used to score the nodes, can be a list
301+
:returns: the configured TreeAnalysis
302+
:raises ValueError: if the search tree not initialized
303+
"""
304+
if not self.tree:
305+
raise ValueError("Search tree not initialized")
306+
307+
if scorer is None:
308+
scorer_names = self.config.post_processing.route_scorers
309+
# If not defined, use the same scorer as the search rewards
310+
if not scorer_names:
311+
search_rewards = self.config.search.algorithm_config.get(
312+
"search_rewards"
313+
)
314+
scorer_names = search_rewards if search_rewards else ["state score"]
315+
316+
elif isinstance(scorer, str):
317+
scorer_names = [scorer]
318+
else:
319+
scorer_names = list(scorer)
320+
321+
if "broken bonds" in scorer_names:
322+
# Add broken bonds scorer if required
323+
self.scorers.load(BrokenBondsScorer(self.config))
324+
325+
scorers = [self.scorers[name] for name in scorer_names]
326+
327+
if self.config.post_processing.scorer_weights:
328+
scorers = [
329+
CombinedScorer(
330+
self.config,
331+
scorer_names,
332+
self.config.post_processing.scorer_weights,
333+
)
334+
]
335+
336+
return TreeAnalysis(self.tree, scorers)
337+
262338

263339
class AiZynthExpander:
264340
"""

aizynthfinder/analysis/routes.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""
33
from __future__ import annotations
44

5+
import copy
56
from typing import TYPE_CHECKING
67

78
import numpy as np
@@ -12,12 +13,12 @@
1213
except ImportError:
1314
pass
1415

15-
from aizynthfinder.analysis import TreeAnalysis
1616
from aizynthfinder.analysis.utils import CombinedReactionTrees, RouteSelectionArguments
1717
from aizynthfinder.reactiontree import SUPPORT_DISTANCES, ReactionTree
1818
from aizynthfinder.search.mcts import MctsNode, MctsSearchTree
1919

2020
if TYPE_CHECKING:
21+
from aizynthfinder.analysis import TreeAnalysis
2122
from aizynthfinder.context.scoring import Scorer
2223
from aizynthfinder.utils.type_utils import (
2324
Any,
@@ -26,6 +27,7 @@
2627
PilImage,
2728
Sequence,
2829
StrDict,
30+
Union,
2931
)
3032

3133

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

6668
self.nodes = self._unpack_kwarg_with_default("nodes", None, **kwargs)
67-
self.scores = self._unpack_kwarg_with_default("scores", np.nan, **kwargs)
69+
self.scores = self._unpack_kwarg_with_default("scores", dict, **kwargs)
6870
self.all_scores = self._unpack_kwarg_with_default("all_scores", dict, **kwargs)
6971

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

8183
@classmethod
8284
def from_analysis(
83-
cls, analysis: TreeAnalysis, selection: RouteSelectionArguments = None
85+
cls,
86+
analysis: TreeAnalysis,
87+
selection: Optional[RouteSelectionArguments] = None,
8488
) -> "RouteCollection":
8589
"""
8690
Create a collection from a tree analysis.
@@ -90,8 +94,8 @@ def from_analysis(
9094
:return: the created collection
9195
"""
9296
items, scores = analysis.sort(selection)
93-
all_scores = [{repr(analysis.scorer): score} for score in scores]
94-
kwargs = {"scores": scores, "all_scores": all_scores}
97+
all_scores = copy.deepcopy(scores)
98+
kwargs: Dict[str, Any] = {"scores": scores, "all_scores": all_scores}
9599
if isinstance(analysis.search_tree, MctsSearchTree):
96100
kwargs["nodes"] = items
97101
reaction_trees = [
@@ -120,10 +124,11 @@ def dicts(self) -> Sequence[StrDict]:
120124
return self._dicts
121125

122126
@property
123-
def images(self) -> Sequence[PilImage]:
127+
def images(self) -> Sequence[Optional[PilImage]]:
124128
"""Returns a list of pictoral representation of the routes"""
125129
if self._images is None:
126130
self._images = self.make_images()
131+
assert self._images is not None
127132
return self._images
128133

129134
@property

0 commit comments

Comments
 (0)