helderlopes commited on
Commit
25f9bfc
·
0 Parent(s):

Initial commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +3 -0
  2. .gitignore +15 -0
  3. LICENSE.OpenMDW-1.1 +49 -0
  4. README.md +351 -0
  5. compose-cuda.yaml +71 -0
  6. compose.yaml +64 -0
  7. img/containers.png +3 -0
  8. img/forward_reactions.png +0 -0
  9. img/jupyter.png +0 -0
  10. img/notebook.png +3 -0
  11. img/openwebui.png +3 -0
  12. img/openwebui_connections.png +3 -0
  13. img/openwebui_integrations.png +0 -0
  14. img/openwebui_prompt.png +3 -0
  15. img/openwebui_tools.png +0 -0
  16. img/predict_product.svg +10 -0
  17. img/predict_retrosynthesis.svg +9 -0
  18. img/predict_retrosynthesis_tree.svg +15 -0
  19. img/retro_reactions.png +0 -0
  20. img/retro_tree_graph.png +3 -0
  21. img/retro_tree_text.png +3 -0
  22. jupyter/Dockerfile +31 -0
  23. jupyter/notebook.ipynb +1567 -0
  24. jupyter/requirements.txt +7 -0
  25. mcp/Dockerfile +11 -0
  26. mcp/requirements.txt +4 -0
  27. mcp/server.py +198 -0
  28. mcp/start.sh +12 -0
  29. model-training/README.md +186 -0
  30. model-training/pistachio/README.md +133 -0
  31. model-training/pistachio/step1_download.sh +5 -0
  32. model-training/pistachio/step2_get_reactions.py +107 -0
  33. model-training/pistachio/step3_clean_reactions.py +10 -0
  34. model-training/pistachio/step4_standardize_reactions_config.yaml +31 -0
  35. model-training/pistachio/step5_remove_arrow_annotations.sh +19 -0
  36. model-training/pistachio/step6_preprocess_reactions_config.yaml +35 -0
  37. model-training/pistachio/step7_convert_to_jsonl.py +46 -0
  38. model-training/pistachio/step8_generate_vocab.py +83 -0
  39. model-training/requirements.txt +7 -0
  40. model-training/tools/analyze_explicit_hydrogens.py +122 -0
  41. model-training/tools/analyze_tokens.py +106 -0
  42. model-training/tools/check_mol_from_smiles.py +10 -0
  43. model-training/tools/check_tanimoto.py +46 -0
  44. model-training/tools/run_fit.py +50 -0
  45. model-training/tools/run_lr_finder.py +98 -0
  46. model-training/tools/run_predict.py +48 -0
  47. model-training/tools/run_test.py +48 -0
  48. model-training/training/cli_main.py +20 -0
  49. model-training/training/predict.yaml +16 -0
  50. model-training/training/test.yaml +35 -0
.gitattributes ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
2
+ *.png filter=lfs diff=lfs merge=lfs -text
3
+ *.pickle filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment
2
+ env
3
+ .venv
4
+
5
+ # Jupyter
6
+ .ipynb_checkpoints
7
+
8
+ # PyCharm
9
+ .idea
10
+
11
+ # Python bytecode
12
+ __pycache__/
13
+ *.pyc
14
+ *.pyo
15
+ *.pyd
LICENSE.OpenMDW-1.1 ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ OpenMDW License Agreement, version 1.1 (OpenMDW-1.1)
2
+
3
+ By exercising rights granted to you under this agreement, you accept and agree
4
+ to its terms.
5
+
6
+ As used in this agreement, "Model Materials" means the materials provided to
7
+ you under this agreement, consisting of: (1) one or more machine learning
8
+ models (including architecture and parameters); and (2) all related artifacts
9
+ (including associated data, documentation and software) that are provided to
10
+ you hereunder.
11
+
12
+ Subject to your compliance with this agreement, permission is hereby granted,
13
+ free of charge, to deal in the Model Materials without restriction, including
14
+ under all copyright, patent, database, and trade secret rights included or
15
+ embodied therein.
16
+
17
+ If you distribute any portion of the Model Materials, you shall retain in your
18
+ distribution (1) a copy of this agreement, and (2) all copyright notices and
19
+ other notices of origin included in the Model Materials that are applicable to
20
+ your distribution.
21
+
22
+ If you file, maintain, or voluntarily participate in a lawsuit against any
23
+ person or entity asserting that the Model Materials directly or indirectly
24
+ infringe any patent or copyright, then all rights and grants made to you
25
+ hereunder are terminated, unless that lawsuit was in response to a
26
+ corresponding lawsuit first brought against you.
27
+
28
+ This agreement does not impose any restrictions or obligations with respect to
29
+ any use, modification, or sharing of any outputs generated by using the Model
30
+ Materials.
31
+
32
+ THE MODEL MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
33
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34
+ FITNESS FOR A PARTICULAR PURPOSE, TITLE, NONINFRINGEMENT, ACCURACY, OR THE
35
+ ABSENCE OF LATENT OR OTHER DEFECTS OR ERRORS, WHETHER OR NOT DISCOVERABLE, ALL
36
+ TO THE GREATEST EXTENT PERMISSIBLE UNDER APPLICABLE LAW.
37
+
38
+ YOU ARE SOLELY RESPONSIBLE FOR (1) CLEARING RIGHTS OF OTHER PERSONS THAT MAY
39
+ APPLY TO THE MODEL MATERIALS OR ANY USE THEREOF, INCLUDING WITHOUT LIMITATION
40
+ ANY PERSON'S COPYRIGHTS OR OTHER RIGHTS INCLUDED OR EMBODIED IN THE MODEL
41
+ MATERIALS; (2) OBTAINING ANY NECESSARY CONSENTS, PERMISSIONS OR OTHER RIGHTS
42
+ REQUIRED FOR ANY USE OF THE MODEL MATERIALS; OR (3) PERFORMING ANY DUE
43
+ DILIGENCE OR UNDERTAKING ANY OTHER INVESTIGATIONS INTO THE MODEL MATERIALS OR
44
+ ANYTHING INCORPORATED OR EMBODIED THEREIN.
45
+
46
+ IN NO EVENT SHALL THE PROVIDERS OF THE MODEL MATERIALS BE LIABLE FOR ANY CLAIM,
47
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
48
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE MODEL MATERIALS, THE
49
+ USE THEREOF OR OTHER DEALINGS THEREIN.
README.md ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - en
4
+ tags:
5
+ - chemistry
6
+ - reaction-prediction
7
+ - retrosynthesis
8
+ license: openmdw-1.1
9
+ ---
10
+
11
+ # RXN-Sandbox
12
+
13
+ A lightweight, local instance of RXN for running forward reaction and retrosynthesis predictions.
14
+
15
+ > [!NOTE]
16
+ > This repository is cross-listed on **[GitHub](https://github.com/rxn4chemistry/rxn-sandbox)** and **[Hugging Face](https://huggingface.co/rxn4chemistry/rxn-sandbox)**.
17
+
18
+ ## Table of Contents
19
+ - [Overview](#overview)
20
+ - [Prerequisites](#prerequisites)
21
+ - [Quick Start](#quick-start)
22
+ - [Usage](#usage)
23
+ - [Jupyter Notebook](#jupyter-notebook)
24
+ - [MCP Integration with OpenWebUI](#mcp-integration-with-openwebui)
25
+ - [Python Scripts](#python-scripts)
26
+ - [Container Management](#container-management)
27
+
28
+ ## Overview
29
+
30
+ This repository enables offline chemical reaction prediction using transformer models. The available tasks are RXN's most used prediction functionality: forward reaction, single-step retrosynthesis, and retrosynthesis tree generation. These tasks are performed locally via Jupyter notebook, via dedicated Python scripts, or via LLM (using OpenWeb UI and MCP). The transformer models were trained using 2025Q2 Pistachio data.
31
+
32
+ | Forward Reaction | Retrosynthesis (Single Step) | Retrosynthesis (Tree) |
33
+ |:---:|:---:|:---:|
34
+ | ![Product Prediction](img/predict_product.svg "Product Prediction")<br>Predict products from reactants | ![Retrosynthesis Prediction](img/predict_retrosynthesis.svg "Retrosynthesis Prediction")<br>Predict reactants for one step | ![Retrosynthesis Tree Prediction](img/predict_retrosynthesis_tree.svg "Retrosynthesis Tree Prediction")<br>Generate multi-step routes |
35
+
36
+
37
+ ## Prerequisites
38
+
39
+ **Software**:
40
+
41
+ - [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install) OR
42
+ [Podman](https://podman.io/getting-started/installation) and [Podman Compose](https://podman-desktop.io/docs/compose/setting-up-compose)
43
+ - [Git LFS](https://git-lfs.com/)
44
+
45
+ > [!NOTE]
46
+ > If using Podman, replace `docker` commands with `podman` throughout this guide.
47
+ >
48
+ > This repository uses Git LFS for `.ckpt` model files. Install Git LFS, then run `git lfs install` once on your machine before cloning or pulling the repository.
49
+
50
+ **Hardware**:
51
+
52
+ - **8GB RAM** (16GB recommended for retrosynthesis tree predictions)
53
+ - **10GB free disk space** for container images and models
54
+ - **Supported platforms:** macOS, Linux, Windows
55
+ - **Supported architectures:** Intel/AMD (x86_64) and ARM64 (Apple Silicon)
56
+ - **NVIDIA GPU** (optional) — required only when using `compose-cuda.yaml`
57
+
58
+ ## Quick Start
59
+
60
+ Two Compose files are provided:
61
+
62
+ | File | Description |
63
+ |------|-------------|
64
+ | [`compose.yaml`](compose.yaml) | Default — CPU-only inference |
65
+ | [`compose-cuda.yaml`](compose-cuda.yaml) | GPU-accelerated inference via NVIDIA CUDA (requires an NVIDIA GPU and the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html)) |
66
+
67
+ Clone this repository, and from the root directory run the following commands. Replace `compose.yaml` with `compose-cuda.yaml` in each command to enable GPU acceleration.
68
+
69
+ #### 1. Build the Docker Images
70
+
71
+ ```bash
72
+ docker compose -f compose.yaml build
73
+ ```
74
+
75
+ #### 2. Start the Services
76
+
77
+ ```bash
78
+ docker compose -f compose.yaml up -d
79
+ ```
80
+
81
+ #### 3. Verify Running Containers
82
+
83
+ You should see all containers running:
84
+
85
+ ![Containers](img/containers.png "Containers")
86
+
87
+ #### 4. Access the Interface(s)
88
+
89
+ - **Jupyter Notebook**: http://localhost:8888/
90
+ - **OpenWeb UI**: http://localhost:3000/
91
+
92
+
93
+
94
+ ## Usage
95
+
96
+ <details id="jupyter-notebook">
97
+
98
+ <summary>Jupyter Notebook (click to expand)</summary>
99
+
100
+ ### Jupyter Notebook
101
+
102
+ Access Jupyter at http://localhost:8888/ to use the interactive notebook environment.
103
+
104
+ ![Jupyter](img/jupyter.png "Jupyter")
105
+
106
+ Use the provided [notebook.ipynb](http://localhost:8888/notebooks/notebook.ipynb) to explore examples and interact with the models.
107
+
108
+ ![Notebook](img/notebook.png "Notebook")
109
+
110
+ ### Initial Setup
111
+
112
+ Run the **Celery setup and helper functions** section first to:
113
+ - Import required libraries
114
+ - Configure the Celery application
115
+ - Define helper functions for visualizing results
116
+
117
+ ### Product Prediction
118
+
119
+ Two examples are provided for product prediction (batch and single reaction). Customize the reactants list:
120
+
121
+ ```python
122
+ # Set up a list of reactants to make predictions
123
+ reactants_list = ["CCI.O=Cc1ccc([N+](=O)[O-])c(O)c1"]
124
+ ```
125
+
126
+ Configure prediction parameters:
127
+
128
+ ```python
129
+ # Setup task kwargs
130
+ kwargs = {
131
+ "topn": 3, # Number of results per reactant
132
+ "num_beams": 5, # Number of beams used for prediction. Must be >= topn
133
+ }
134
+ ```
135
+ After running the prediction, the results will be displayed in a table:
136
+
137
+ ![Product Prediction Results](img/forward_reactions.png "Product Prediction Results")
138
+
139
+ ### Retrosynthesis Prediction
140
+
141
+ Retrosynthesis predictions process one product at a time. Set the target product:
142
+
143
+ ```python
144
+ # Choose product for retrosynthesis prediction
145
+ product = "C=CC(=C)C[Si](C)(C)C"
146
+ ```
147
+ Configure retrosynthesis-specific parameters:
148
+
149
+ ```python
150
+ # Setup task kwargs
151
+ kwargs = {
152
+ "topn": 10, # Number of results per reactant
153
+ "num_beams": 10, # Number of beams used for prediction. Must be >= topn
154
+ "fap": 0.6, # Forward likelihood acceptance probability (not length averaged)
155
+ "fld": 0.2, # Forward likelihood delta required between the top2 forward prediction results
156
+ }
157
+ ```
158
+
159
+ Results are displayed in a similar table format:
160
+
161
+ ![Retrosynthesis Prediction Results](img/retro_reactions.png "Retrosynthesis Prediction Results")
162
+
163
+ ### Retrosynthesis Tree Prediction
164
+
165
+ Start by selecting a target product SMILES:
166
+
167
+ ```python
168
+ # Choose product for retrosynthesis tree prediction
169
+ product = "C1C(C[Si](C)(C)C)=CCC2C(=O)OC(=O)C12"
170
+ ```
171
+
172
+ Configure prediction and tree-specific parameters:
173
+
174
+ ```python
175
+ # Setup task kwargs
176
+ kwargs = {
177
+ "topn": 15, # Number of results per reactant
178
+ "num_beams": 15, # Number of beams used for prediction. Must be >= topn
179
+ "fap": 0.6, # Forward likelihood acceptance probability (not length averaged)
180
+ "fld": 0.2, # Forward likelihood delta required between the top2 forward prediction results
181
+ "max_depth": 4, # Max depth of the retrosynthesis tree
182
+ "beam_width": 6, # Max amount of nodes being expanded in each step
183
+ }
184
+
185
+ ```
186
+
187
+ > **⚠️ Performance Note**: Retrosynthesis tree predictions are computationally intensive and may take significant time to complete.
188
+
189
+ ### Result Visualization
190
+
191
+ Two visualization options are available:
192
+
193
+ **1. Text Representation** - Complete textual view of predicted routes and steps:
194
+
195
+ ![Retrosynthesis Tree Text Representation](img/retro_tree_text.png "Retrosynthesis Tree Text Representation")
196
+
197
+ **2. Graph Representation** - Visual tree structure with molecule expansion paths. Use the selector to switch between different prediction results:
198
+
199
+ ![Retrosynthesis Tree Graph Representation](img/retro_tree_graph.png "Retrosynthesis Tree Graph Representation")
200
+
201
+ </details>
202
+
203
+ <details id="mcp-integration-with-openwebui">
204
+ <summary>LLM (click to expand)</summary>
205
+
206
+ ### MCP Integration with OpenWebUI
207
+
208
+ Access OpenWeb UI at http://localhost:3000/ to interact with RXN models using natural language through AI assistants.
209
+
210
+ ![OpenWeb UI](img/openwebui.png "OpenWeb UI")
211
+
212
+ #### Setup MCP Server Connection
213
+
214
+ 1. Navigate to **Settings → Integrations**
215
+ 2. Under **Manage Tool Servers**, click the `+` icon
216
+ 3. Set URL to `http://localhost:8000`
217
+ 4. Click **Verify Connection** and **Save**
218
+
219
+ <img src="img/openwebui_integrations.png" alt="OpenWeb UI New Integration" width="450"/>
220
+
221
+ #### Configure External Models (Optional)
222
+
223
+ To use external AI models:
224
+ 1. Go to **Admin Settings → Connections**
225
+ 2. Add your API key for the desired model provider
226
+
227
+ ![OpenWeb UI Connections](img/openwebui_connections.png "OpenWeb UI Connections")
228
+
229
+ #### Enable RXN Tools
230
+
231
+ 1. Select your preferred AI model
232
+ 2. Click the **Integrations** button below the prompt input
233
+ 3. Select **Tools** and toggle on `rxn-mcp-server`
234
+
235
+ > **Note**: You must re-enable the tool when switching models.
236
+
237
+ ![OpenWeb UI Activate Tool](img/openwebui_tools.png "OpenWeb UI Activate Tool")
238
+
239
+ #### Using Natural Language
240
+
241
+ Interact with RXN functions using conversational prompts:
242
+
243
+ ![OpenWeb UI Chat Prompt](img/openwebui_prompt.png "OpenWeb UI Chat Prompt")
244
+
245
+ </details>
246
+
247
+ <details id="python-scripts">
248
+
249
+ <summary>Python Scripts (click to expand)</summary>
250
+
251
+ ### Running via Scripts
252
+
253
+ If you prefer a command-line workflow, you can run predictions directly from the provided Python scripts without using Jupyter or OpenWebUI. This method may be useful for
254
+ running analyses on remote machines (which often lack GUIs).
255
+
256
+ #### Open a Shell in the Worker Container
257
+
258
+ The scripts are executed inside the `worker` container, where the models and Celery configuration are already available:
259
+
260
+ ```bash
261
+ docker exec -it rxn-worker-1 bash
262
+ ```
263
+
264
+ #### Available Example Scripts
265
+
266
+ - `python scripts/predict_product.py` — run forward reaction prediction examples
267
+ - `python scripts/predict_retrosynthesis.py` — run single-step retrosynthesis examples
268
+ - `python scripts/predict_retrosynthesis_tree.py` — run retrosynthesis tree examples
269
+ - `python scripts/run_notebook_examples.py` — run the same examples shown in the notebook in sequence (this is effectively a combination of the three prior scripts)
270
+
271
+ #### Customize Inputs and Parameters
272
+
273
+ Each script is intended to be edited before execution. The `scripts/` directory is mounted into the worker container, so local changes are immediately available without rebuilding the image. Update the input SMILES and prediction parameters directly in the file:
274
+
275
+ - `reactants_list` for forward reaction prediction
276
+ - `product` for retrosynthesis and retrosynthesis tree prediction
277
+ - `topn`, `num_beams`, `fap`, `fld`, `max_depth`, and `beam_width` as needed
278
+
279
+ Then run the script you want:
280
+
281
+ ```bash
282
+ python scripts/predict_product.py
283
+ ```
284
+
285
+ #### Exit the Container
286
+
287
+ When you are finished, leave the container shell with:
288
+
289
+ ```bash
290
+ exit
291
+ ```
292
+ </details>
293
+
294
+ ## Container Management
295
+
296
+ ### Container Architecture
297
+
298
+ The system consists of six containers:
299
+
300
+ - **redis** - Results backend for Celery tasks
301
+ - **broker** - RabbitMQ message queue for task distribution
302
+ - **worker** - Celery worker running the transformer models
303
+ - **jupyter** - Interactive notebook environment
304
+ - **mcp** - Model Context Protocol server for LLM integration
305
+ - **openwebui** - Web interface for AI assistant interaction
306
+
307
+ ### Useful Commands
308
+
309
+ ```bash
310
+ # Stopping services
311
+ docker compose -f compose.yaml stop
312
+
313
+ # Restarting services
314
+ docker compose -f compose.yaml restart
315
+
316
+ # Viewing logs (all services)
317
+ docker compose -f compose.yaml logs -f
318
+
319
+ # Viewing logs (specific service)
320
+ docker compose -f compose.yaml logs -f worker
321
+
322
+ # Removing everything (containers, networks, and volumes)
323
+ docker compose -f compose.yaml down -v
324
+ ```
325
+
326
+ > [!TIP]
327
+ > Replace `-f compose.yaml` with `-f compose-cuda.yaml` in any of the commands above to manage the GPU-accelerated stack instead.
328
+
329
+ ### Performance Notes
330
+
331
+ - **Forward Predictions**: Fast (seconds)
332
+ - **Single-Step Retrosynthesis**: Moderate (seconds to minutes)
333
+ - **Tree Retrosynthesis**: Slow (minutes to hours depending on depth/width)
334
+
335
+ **Optimization Tips**:
336
+
337
+ - Start with smaller `topn` and `num_beams` values
338
+ - Limit `max_depth` to 3-4 for tree predictions
339
+ - Use `beam_width` of 5-10 for reasonable performance
340
+ - Allocate 16GB RAM for complex tree predictions
341
+
342
+ **Benchmarking**:
343
+
344
+ GPU acceleration provides significant speedup, especially for complex retrosynthesis tree predictions. These results are from the Python scripts in the `scripts/` folder. Analysis
345
+ times can vary substantially depending on the query molecule(s) and parameters.
346
+
347
+ | Script | T4 GPU (AWS g4dn.xlarge) | M1 Mac CPU | GPU Speedup |
348
+ |--------|---------------------|------------|-------------|
349
+ | `predict_product.py` | **1.7 s** | 3.4 s | 2.0x |
350
+ | `predict_retrosynthesis.py` | **2.5 s** | 8.5 s | 3.4x |
351
+ | `predict_retrosynthesis_tree.py` | **87 s (1.5 min)** | 763 s (12.7 min) | 8.8x |
compose-cuda.yaml ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: rxn
2
+
3
+ services:
4
+
5
+ redis:
6
+ image: public.ecr.aws/docker/library/redis
7
+ command: redis-server --requirepass ubuntu
8
+ environment:
9
+ - REDIS_PASSWORD=ubuntu
10
+
11
+ broker:
12
+ image: public.ecr.aws/docker/library/rabbitmq:3.13-management
13
+ environment:
14
+ RABBITMQ_DEFAULT_PASS: ubuntu
15
+ RABBITMQ_DEFAULT_USER: ubuntu
16
+
17
+ worker:
18
+ build:
19
+ context: ./worker
20
+ volumes:
21
+ - ./models:/app/models
22
+ - ./vocab:/app/vocab
23
+ - ./scripts:/app/scripts
24
+ environment:
25
+ CELERY_QUEUE: product_prediction,retro_prediction
26
+ CELERY_BROKER_URL: amqp://ubuntu:ubuntu@broker:5672//
27
+ CELERY_RESULT_BACKEND: redis://:ubuntu@redis:6379/0
28
+ depends_on: [redis, broker]
29
+ deploy:
30
+ resources:
31
+ reservations:
32
+ devices:
33
+ - driver: nvidia
34
+ count: all
35
+ capabilities: [gpu]
36
+
37
+ jupyter:
38
+ build:
39
+ context: ./jupyter
40
+ working_dir: /rxn
41
+ volumes:
42
+ - ./jupyter:/rxn
43
+ environment:
44
+ CELERY_QUEUE: product_prediction,retro_prediction
45
+ CELERY_BROKER_URL: amqp://ubuntu:ubuntu@broker:5672//
46
+ CELERY_RESULT_BACKEND: redis://:ubuntu@redis:6379/0
47
+ ports:
48
+ - "8888:8888"
49
+ depends_on: [redis, broker, worker]
50
+
51
+ mcp:
52
+ build:
53
+ context: ./mcp
54
+ ports:
55
+ - "8000:8000"
56
+ environment:
57
+ CELERY_BROKER_URL: amqp://ubuntu:ubuntu@broker:5672//
58
+ CELERY_RESULT_BACKEND: redis://:ubuntu@redis:6379/0
59
+ depends_on: [redis, broker, worker]
60
+
61
+ openwebui:
62
+ image: ghcr.io/open-webui/open-webui
63
+ ports:
64
+ - "3000:8080"
65
+ environment:
66
+ WEBUI_AUTH: "False"
67
+ volumes:
68
+ - openwebui-data:/app/backend/data
69
+
70
+ volumes:
71
+ openwebui-data:
compose.yaml ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: rxn
2
+
3
+ services:
4
+
5
+ redis:
6
+ image: public.ecr.aws/docker/library/redis
7
+ command: redis-server --requirepass ubuntu
8
+ environment:
9
+ - REDIS_PASSWORD=ubuntu
10
+
11
+ broker:
12
+ image: public.ecr.aws/docker/library/rabbitmq:3.13-management
13
+ environment:
14
+ RABBITMQ_DEFAULT_PASS: ubuntu
15
+ RABBITMQ_DEFAULT_USER: ubuntu
16
+
17
+ worker:
18
+ build:
19
+ context: ./worker
20
+ volumes:
21
+ - ./models:/app/models
22
+ - ./vocab:/app/vocab
23
+ - ./scripts:/app/scripts
24
+ environment:
25
+ CELERY_QUEUE: product_prediction,retro_prediction
26
+ CELERY_BROKER_URL: amqp://ubuntu:ubuntu@broker:5672//
27
+ CELERY_RESULT_BACKEND: redis://:ubuntu@redis:6379/0
28
+ depends_on: [redis, broker]
29
+
30
+ jupyter:
31
+ build:
32
+ context: ./jupyter
33
+ working_dir: /rxn
34
+ volumes:
35
+ - ./jupyter:/rxn
36
+ environment:
37
+ CELERY_QUEUE: product_prediction,retro_prediction
38
+ CELERY_BROKER_URL: amqp://ubuntu:ubuntu@broker:5672//
39
+ CELERY_RESULT_BACKEND: redis://:ubuntu@redis:6379/0
40
+ ports:
41
+ - "8888:8888"
42
+ depends_on: [redis, broker, worker]
43
+
44
+ mcp:
45
+ build:
46
+ context: ./mcp
47
+ ports:
48
+ - "8000:8000"
49
+ environment:
50
+ CELERY_BROKER_URL: amqp://ubuntu:ubuntu@broker:5672//
51
+ CELERY_RESULT_BACKEND: redis://:ubuntu@redis:6379/0
52
+ depends_on: [redis, broker, worker]
53
+
54
+ openwebui:
55
+ image: ghcr.io/open-webui/open-webui
56
+ ports:
57
+ - "3000:8080"
58
+ environment:
59
+ WEBUI_AUTH: "False"
60
+ volumes:
61
+ - openwebui-data:/app/backend/data
62
+
63
+ volumes:
64
+ openwebui-data:
img/containers.png ADDED

Git LFS Details

  • SHA256: 035462e638efa5558d8b6b2fe283cfa9c40a6e5c6a9e93db56426e09516a4970
  • Pointer size: 131 Bytes
  • Size of remote file: 123 kB
img/forward_reactions.png ADDED
img/jupyter.png ADDED
img/notebook.png ADDED

Git LFS Details

  • SHA256: cccecc90b3ac8f103e6347cf656ad7dfe036358d28a75607d82bbff85451138d
  • Pointer size: 131 Bytes
  • Size of remote file: 203 kB
img/openwebui.png ADDED

Git LFS Details

  • SHA256: 041adbe138f5fd9ce266286ae1688352ee54ad2e6913e6df15dddfa77d6bc82b
  • Pointer size: 131 Bytes
  • Size of remote file: 178 kB
img/openwebui_connections.png ADDED

Git LFS Details

  • SHA256: 40231f8d1ca79b14659431a160ca105c99cff3ce778b67dc2fe651d1571306ab
  • Pointer size: 131 Bytes
  • Size of remote file: 267 kB
img/openwebui_integrations.png ADDED
img/openwebui_prompt.png ADDED

Git LFS Details

  • SHA256: a0b4c74b0be255c4bbfa80959cea30e424fe42385f8389bf955d587db7be9008
  • Pointer size: 131 Bytes
  • Size of remote file: 212 kB
img/openwebui_tools.png ADDED
img/predict_product.svg ADDED
img/predict_retrosynthesis.svg ADDED
img/predict_retrosynthesis_tree.svg ADDED
img/retro_reactions.png ADDED
img/retro_tree_graph.png ADDED

Git LFS Details

  • SHA256: 6c13f19ec7b19f09eb51352b905432784937d6273e6e7c75e933a028b170c783
  • Pointer size: 131 Bytes
  • Size of remote file: 209 kB
img/retro_tree_text.png ADDED

Git LFS Details

  • SHA256: 9a3855fed5a8d1ac7ab8f35b54524bf8065ec2bb49004cac2cc4c480260d1d4a
  • Pointer size: 131 Bytes
  • Size of remote file: 166 kB
jupyter/Dockerfile ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM public.ecr.aws/docker/library/python:3.13-slim
2
+
3
+ # Prevents interactive prompts during apt install
4
+ ENV DEBIAN_FRONTEND=noninteractive
5
+
6
+ # Install system dependencies required by RDKit and matplotlib
7
+ RUN apt-get update && apt-get install -y \
8
+ build-essential \
9
+ libxrender1 \
10
+ libxext6 \
11
+ libsm6 \
12
+ libglib2.0-0 \
13
+ libgl1 \
14
+ ca-certificates \
15
+ && rm -rf /var/lib/apt/lists/*
16
+
17
+ WORKDIR /rxn
18
+
19
+ # Copy and install Python dependencies
20
+ COPY requirements.txt .
21
+
22
+ RUN pip install --no-cache-dir -r requirements.txt
23
+
24
+ CMD ["jupyter", "notebook", \
25
+ "--ip=0.0.0.0", \
26
+ "--port=8888", \
27
+ "--no-browser", \
28
+ "--ServerApp.allow_root=True", \
29
+ "--IdentityProvider.token=", \
30
+ "--ServerApp.password=", \
31
+ "--ServerApp.default_url=/notebook.ipynb"]
jupyter/notebook.ipynb ADDED
@@ -0,0 +1,1567 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "6964ba26-6c99-4150-b2d2-0e2b55072993",
6
+ "metadata": {},
7
+ "source": [
8
+ "# RXN Sandbox"
9
+ ]
10
+ },
11
+ {
12
+ "cell_type": "markdown",
13
+ "id": "290b4d5f-2315-469f-afcd-9bffde994f1b",
14
+ "metadata": {},
15
+ "source": [
16
+ "### Celery setup and helper functions"
17
+ ]
18
+ },
19
+ {
20
+ "cell_type": "code",
21
+ "execution_count": 1,
22
+ "id": "831d0e08-3b3d-4348-9de4-5befa7a081a6",
23
+ "metadata": {},
24
+ "outputs": [],
25
+ "source": [
26
+ "# All necessary imports\n",
27
+ "import time\n",
28
+ "import base64\n",
29
+ "import pandas as pd\n",
30
+ "import ipywidgets as widgets\n",
31
+ "from io import BytesIO\n",
32
+ "from rdkit import Chem\n",
33
+ "from rdkit.Chem import Draw\n",
34
+ "from celery import Celery\n",
35
+ "from celery.result import AsyncResult\n",
36
+ "from typing import Optional, Dict, Any\n",
37
+ "from IPython.display import display, HTML, clear_output, Markdown\n",
38
+ "\n",
39
+ "\n",
40
+ "# Initialize Celery\n",
41
+ "celery_app = Celery()\n",
42
+ "#print(\"Broker:\", celery_app.conf.broker_url)\n",
43
+ "#print(\"Backend:\", celery_app.conf.result_backend)\n",
44
+ "\n",
45
+ "\n",
46
+ "# Wait for the celery result\n",
47
+ "def wait_for_result(app: Celery, task_id: str, timeout: float = 120.0, poll: float = 0.5) -> Dict[str, Any]:\n",
48
+ " \"\"\"\n",
49
+ " Poll for a result with a timeout. If task updates state with meta (e.g., PROGRESS),\n",
50
+ " we surface that along the way.\n",
51
+ " \"\"\"\n",
52
+ " res = AsyncResult(task_id, app=app)\n",
53
+ " t0 = time.time()\n",
54
+ " last_state = None\n",
55
+ "\n",
56
+ " while True:\n",
57
+ " state = res.state\n",
58
+ " if state != last_state:\n",
59
+ " print(f\"State: {state} | Info: {res.info}\")\n",
60
+ " last_state = state\n",
61
+ "\n",
62
+ " if res.ready():\n",
63
+ " # could be SUCCESS or FAILURE; .get() will raise on FAILURE\n",
64
+ " return res.get(propagate=False) # returns exception object if failed\n",
65
+ "\n",
66
+ " if time.time() - t0 > timeout:\n",
67
+ " raise TimeoutError(f\"Task {task_id} did not finish in {timeout} seconds.\")\n",
68
+ " time.sleep(poll)\n",
69
+ "\n",
70
+ "\n",
71
+ "# Build results dataframe\n",
72
+ "def build_results_dataframe(records):\n",
73
+ " \"\"\"\n",
74
+ " Convert Celery result(s) into a DataFrame.\n",
75
+ " Only keeps: smiles, predicted_smiles, confidence\n",
76
+ " \"\"\"\n",
77
+ " if records is None:\n",
78
+ " return pd.DataFrame()\n",
79
+ " if isinstance(records, dict):\n",
80
+ " records = [records]\n",
81
+ " if not isinstance(records, list):\n",
82
+ " return pd.DataFrame()\n",
83
+ "\n",
84
+ " df = pd.DataFrame(records)\n",
85
+ " \n",
86
+ " if {\"reactants\", \"product\"}.issubset(df.columns):\n",
87
+ " df.insert(\n",
88
+ " 0, # put as first column (or change to another position)\n",
89
+ " \"Predicted Reaction\",\n",
90
+ " df[\"reactants\"].fillna(\"\").astype(str) + \" → \" + df[\"product\"].fillna(\"\").astype(str)\n",
91
+ " )\n",
92
+ " # Optionally remove the original columns:\n",
93
+ " df = df.drop(columns=[\"reactants\", \"product\"])\n",
94
+ "\n",
95
+ " cols = []\n",
96
+ " for c in [\"Predicted Reaction\", \"confidence\"]:\n",
97
+ " if c in df.columns:\n",
98
+ " cols.append(c)\n",
99
+ "\n",
100
+ " return df[cols]\n",
101
+ "\n",
102
+ "\n",
103
+ "# Show results table\n",
104
+ "def style_results_table(df, caption=\"Reaction Predictions\", align=\"left\"):\n",
105
+ " \"\"\"\n",
106
+ " Style the table:\n",
107
+ " - Bigger, bold caption\n",
108
+ " - Confidence mapped red→yellow→green (0→1), fixed range vmin=0, vmax=1\n",
109
+ " - Removes index\n",
110
+ " \"\"\"\n",
111
+ "\n",
112
+ " clear_output(wait=True)\n",
113
+ " \n",
114
+ " if df.empty:\n",
115
+ " display(HTML(\"<p><em>No results to display.</em></p>\"))\n",
116
+ " return\n",
117
+ "\n",
118
+ " df = df.copy()\n",
119
+ "\n",
120
+ " # Build the Styler\n",
121
+ " styler = df.style\n",
122
+ "\n",
123
+ " # Confidence formatting + red→yellow→green gradient (low→high)\n",
124
+ " # Lock range to [0, 1] so colors are consistent even for partial data.\n",
125
+ " if \"confidence\" in df.columns:\n",
126
+ " try:\n",
127
+ " styler = styler.format({\"confidence\": \"{:.3f}\"}, escape=None)\n",
128
+ " except TypeError:\n",
129
+ " styler = styler.format({\"confidence\": \"{:.3f}\"}, escape=False)\n",
130
+ " styler = styler.background_gradient(\n",
131
+ " subset=[\"confidence\"],\n",
132
+ " cmap=\"RdYlGn\", # red (low) → yellow → green (high)\n",
133
+ " vmin=0.0,\n",
134
+ " vmax=1.0,\n",
135
+ " )\n",
136
+ " styler = styler.set_properties(\n",
137
+ " subset=[\"confidence\"], **{\"font-weight\": \"500\",})\n",
138
+ "\n",
139
+ " # SMILES styling: bold + monospace + wrapping\n",
140
+ " if \"Predicted Reaction\" in df.columns:\n",
141
+ " styler = styler.set_properties(\n",
142
+ " subset=[\"Predicted Reaction\"], \n",
143
+ " **{\n",
144
+ " \"background-color\": \"#fafafa\", \n",
145
+ " \"text-align\": align,\n",
146
+ " \"font-weight\": \"600\",\n",
147
+ " \"font-family\": \"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace\",\n",
148
+ " })\n",
149
+ "\n",
150
+ " # Table styles (caption, headers, borders)\n",
151
+ " styler = styler.set_table_styles(\n",
152
+ " [\n",
153
+ " {\n",
154
+ " \"selector\": \"caption\",\n",
155
+ " \"props\": [\n",
156
+ " (\"caption-side\", \"top\"),\n",
157
+ " (\"font-weight\", \"700\"),\n",
158
+ " (\"font-size\", \"1.2rem\"),\n",
159
+ " (\"margin-bottom\", \"10px\"),\n",
160
+ " ],\n",
161
+ " },\n",
162
+ " {\n",
163
+ " \"selector\": \"th.col_heading\",\n",
164
+ " \"props\": [\n",
165
+ " (\"font-weight\", \"700\"),\n",
166
+ " (\"padding\", \"8px\"),\n",
167
+ " (\"text-align\", \"center\"),\n",
168
+ " ],\n",
169
+ " },\n",
170
+ " {\n",
171
+ " \"selector\": \"td\",\n",
172
+ " \"props\": [\n",
173
+ " (\"padding\", \"8px\"),\n",
174
+ " (\"vertical-align\", \"center\"),\n",
175
+ " ],\n",
176
+ " },\n",
177
+ " {\n",
178
+ " \"selector\": \"table\",\n",
179
+ " \"props\": [\n",
180
+ " (\"border-collapse\", \"separate\"),\n",
181
+ " (\"border-spacing\", \"0px\"),\n",
182
+ " (\"width\", \"100%\"),\n",
183
+ " ],\n",
184
+ " },\n",
185
+ " ]\n",
186
+ " )\n",
187
+ "\n",
188
+ " # Render with inline HTML (formatters already handled escaping logic)\n",
189
+ " try:\n",
190
+ " styler = styler.hide(axis=\"index\")\n",
191
+ " except Exception:\n",
192
+ " styler = styler.hide_index()\n",
193
+ "\n",
194
+ " # Use modern API first; fall back for older pandas\n",
195
+ " try:\n",
196
+ " html = styler.set_caption(caption).to_html() # escape behavior controlled in format()\n",
197
+ " except TypeError:\n",
198
+ " html = styler.set_caption(caption).to_html(escape=False)\n",
199
+ "\n",
200
+ " display(HTML(html))\n",
201
+ "\n",
202
+ "\n",
203
+ "# Display a retrosynthesis tree textually\n",
204
+ "def display_route(route, idx):\n",
205
+ " display(Markdown(\n",
206
+ " f\"## Route {idx + 1}\\n\"\n",
207
+ " f\"- **Score:** `{route['score']:.3f}`\\n\"\n",
208
+ " f\"- **Steps:** `{route['steps']}`\"\n",
209
+ " ))\n",
210
+ "\n",
211
+ " open_nodes = route.get(\"open_nodes\", [])\n",
212
+ " if open_nodes:\n",
213
+ " display(Markdown(f\"**Remaining molecules:** `{', '.join(open_nodes)}`\"))\n",
214
+ "\n",
215
+ " for i, arc in enumerate(route.get(\"arcs\", []), start=1):\n",
216
+ " product = arc[\"product\"]\n",
217
+ " reactants = \".\".join(arc[\"reactants\"])\n",
218
+ "\n",
219
+ " display(Markdown(\n",
220
+ " f\"#### Step {i}\\n\"\n",
221
+ " f\"**Product:** `{product}` \\n\"\n",
222
+ " f\"**Reactants:** `{reactants}` \\n\"\n",
223
+ " f\"- Forward likelihood: `{arc['forward_likelihood']:.3f}` \\n\"\n",
224
+ " f\"- Arc score: `{arc['arc_score']:.3f}` \\n\"\n",
225
+ " f\"- Retro confidence: `{arc.get('confidence', 'n/a')}`\"\n",
226
+ " ))\n",
227
+ "\n",
228
+ "\n",
229
+ "\n",
230
+ "\n",
231
+ "# Convert a molecule to PNG for faster representation\n",
232
+ "def mol_png_base64(smiles, size=(250, 200)):\n",
233
+ " mol = Chem.MolFromSmiles(smiles)\n",
234
+ " if mol is None:\n",
235
+ " return \"\"\n",
236
+ "\n",
237
+ " img = Draw.MolToImage(mol, size=size)\n",
238
+ " buf = BytesIO()\n",
239
+ " img.save(buf, format=\"PNG\")\n",
240
+ " return base64.b64encode(buf.getvalue()).decode(\"utf-8\")\n",
241
+ "\n",
242
+ "\n",
243
+ "# Draw a row of reactants that make a retrosynthesis tree step\n",
244
+ "def draw_reactant_row_png(reactants, expanded_smiles=None):\n",
245
+ " imgs = [mol_png_base64(smi) for smi in reactants]\n",
246
+ "\n",
247
+ " html = \"\"\"\n",
248
+ " <div style=\"\n",
249
+ " display: flex;\n",
250
+ " flex-direction: row;\n",
251
+ " justify-content: flex-start;\n",
252
+ " align-items: flex-start;\n",
253
+ " gap: 20px;\n",
254
+ " overflow-x: auto;\n",
255
+ " padding: 12px 20px;\n",
256
+ " border: 1px solid #ddd;\n",
257
+ " margin-bottom: 16px;\n",
258
+ " box-sizing: border-box;\n",
259
+ " \">\n",
260
+ " \"\"\"\n",
261
+ "\n",
262
+ " for smi, img in zip(reactants, imgs):\n",
263
+ " is_expanded = (smi == expanded_smiles)\n",
264
+ "\n",
265
+ " border = \"2px solid #1f77b4\" if is_expanded else \"1px solid #ccc\"\n",
266
+ " label = \"<div style='font-weight:700; color:#1f77b4;'>expanded ↓</div>\" if is_expanded else \"\"\n",
267
+ "\n",
268
+ " html += f\"\"\"\n",
269
+ " <div style=\"\n",
270
+ " display: flex;\n",
271
+ " flex-direction: column;\n",
272
+ " align-items: center;\n",
273
+ " flex: 0 0 auto;\n",
274
+ " min-width: 260px;\n",
275
+ " padding: 6px;\n",
276
+ " border: {border};\n",
277
+ " border-radius: 6px;\n",
278
+ " \">\n",
279
+ " <img src=\"data:image/png;base64,{img}\" style=\"display:block;\"/>\n",
280
+ " <div style=\"\n",
281
+ " margin-top: 6px;\n",
282
+ " font-size: 14px;\n",
283
+ " font-weight: 700;\n",
284
+ " text-align: center;\n",
285
+ " word-break: break-all;\n",
286
+ " font-family: monospace;\n",
287
+ " \">\n",
288
+ " {smi}\n",
289
+ " </div>\n",
290
+ " {label}\n",
291
+ " </div>\n",
292
+ " \"\"\"\n",
293
+ "\n",
294
+ " html += \"</div>\"\n",
295
+ " display(HTML(html))\n",
296
+ "\n",
297
+ "\n",
298
+ "# Draw the full retrosynthesis tree\n",
299
+ "def draw_route_png(route):\n",
300
+ " display(Markdown(\"## Target molecule\"))\n",
301
+ " draw_reactant_row_png([route[\"arcs\"][0][\"product\"]])\n",
302
+ "\n",
303
+ " for i, arc in enumerate(route[\"arcs\"]):\n",
304
+ " display(Markdown(f\"## Step {i + 1} reactants\"))\n",
305
+ "\n",
306
+ " # Determine which reactant is further decomposed\n",
307
+ " expanded = None\n",
308
+ " if i + 1 < len(route[\"arcs\"]):\n",
309
+ " expanded = route[\"arcs\"][i + 1][\"product\"]\n",
310
+ "\n",
311
+ " draw_reactant_row_png(\n",
312
+ " arc[\"reactants\"],\n",
313
+ " expanded_smiles=expanded\n",
314
+ " )\n",
315
+ "\n",
316
+ "\n",
317
+ "# Builds a selector to display the full retrosynthesis tree for the selected route\n",
318
+ "def tree_route_selector(results):\n",
319
+ " options = [\n",
320
+ " (f\"Route {i+1} | score={r['score']:.3f}\", i)\n",
321
+ " for i, r in enumerate(results)\n",
322
+ " ]\n",
323
+ " \n",
324
+ " def show_route(idx):\n",
325
+ " draw_route_png(results[idx])\n",
326
+ " \n",
327
+ " dropdown = widgets.Dropdown(\n",
328
+ " options=options,\n",
329
+ " description=\"Route\",\n",
330
+ " )\n",
331
+ " \n",
332
+ " ui = widgets.interactive(show_route, idx=dropdown)\n",
333
+ " display(ui)"
334
+ ]
335
+ },
336
+ {
337
+ "cell_type": "markdown",
338
+ "id": "66ebc007-7a61-4f2f-badb-ede0efcfbc47",
339
+ "metadata": {},
340
+ "source": [
341
+ "### Product prediction for a batch of reactions"
342
+ ]
343
+ },
344
+ {
345
+ "cell_type": "code",
346
+ "execution_count": 5,
347
+ "id": "9708b32c-41d5-4796-a4a7-16e7a740a979",
348
+ "metadata": {},
349
+ "outputs": [
350
+ {
351
+ "name": "stdout",
352
+ "output_type": "stream",
353
+ "text": [
354
+ "Task sent. Assigned task_id: 31bccfad-38b8-4cc5-965f-08552ed376f6\n",
355
+ "State: PENDING | Info: None\n",
356
+ "State: STARTED | Info: None\n",
357
+ "State: SUCCESS | Info: {'result': [{'reactants': 'CCI.O=Cc1ccc([N+](=O)[O-])c(O)c1', 'product': 'CCOc1cc(C=O)ccc1[N+](=O)[O-]', 'confidence': 0.9987357632971271, 'smiles': 'CCI.O=Cc1ccc([N+](=O)[O-])c(O)c1>>CCOc1cc(C=O)ccc1[N+](=O)[O-]'}, {'reactants': 'CCOc1cc(O)c(C=O)cc1OCC.OCCCBr', 'product': 'CCOc1cc(C=O)c(OCCCO)cc1OCC', 'confidence': 0.992653825419336, 'smiles': 'CCOc1cc(O)c(C=O)cc1OCC.OCCCBr>>CCOc1cc(C=O)c(OCCCO)cc1OCC'}, {'reactants': 'C=CCc1cc(OCc2ccccc2)ccc1O.CCBr', 'product': 'C=CCc1cc(OCc2ccccc2)ccc1OCC', 'confidence': 0.9966198460612675, 'smiles': 'C=CCc1cc(OCc2ccccc2)ccc1O.CCBr>>C=CCc1cc(OCc2ccccc2)ccc1OCC'}], 'time': 3.18107008934021}\n"
358
+ ]
359
+ }
360
+ ],
361
+ "source": [
362
+ "# Set up a list of reactants to make predictions\n",
363
+ "reactants_list = [\"CCI.O=Cc1ccc([N+](=O)[O-])c(O)c1\", \"CCOc1cc(O)c(C=O)cc1OCC.OCCCBr\", \"C=CCc1cc(OCc2ccccc2)ccc1O.CCBr\"]\n",
364
+ "\n",
365
+ "# Setup task kwargs\n",
366
+ "kwargs = {\n",
367
+ " \"topn\": 1, # Number of results per reactant\n",
368
+ " \"num_beams\": 3, # Number of beams used for prediction. Must be >= topn\n",
369
+ " \"device\": None, # Device used for predicting, either \"cuda\" or \"cpu\", None defaults to cuda if available\n",
370
+ " \"ckpt_forward\": \"Pistachio2025Q2-Forward\", # Default forward model\n",
371
+ " \"vocab\": \"Pistachio2025Q2\", # Vocab for default forward model\n",
372
+ " # \"ckpt_forward_path\": \"models/forward/Pistachio2025Q2-Forward.ckpt\", # Can be used instead of ckpt_forward\n",
373
+ " # \"vocab_path\": \"vocab/Pistachio2025Q2.txt\", # Can be used instead of vocab\n",
374
+ "}\n",
375
+ "\n",
376
+ "# Send the product_prediction task with the reaction list and kwargs\n",
377
+ "task = celery_app.send_task(\n",
378
+ " \"tasks.product_prediction\",\n",
379
+ " [reactants_list],\n",
380
+ " kwargs=kwargs,\n",
381
+ " queue=\"product_prediction\",\n",
382
+ ")\n",
383
+ "print(\"Task sent. Assigned task_id: {}\".format(task.id))\n",
384
+ "\n",
385
+ "# Use the task id to get the result. Increase timeout if needed.\n",
386
+ "response = wait_for_result(celery_app, task.id, timeout=180)"
387
+ ]
388
+ },
389
+ {
390
+ "cell_type": "code",
391
+ "execution_count": 6,
392
+ "id": "df6007a1-1e6d-4b71-9991-8d6a21febdf0",
393
+ "metadata": {
394
+ "scrolled": true
395
+ },
396
+ "outputs": [
397
+ {
398
+ "data": {
399
+ "text/html": [
400
+ "<style type=\"text/css\">\n",
401
+ "#T_cf7d5 caption {\n",
402
+ " caption-side: top;\n",
403
+ " font-weight: 700;\n",
404
+ " font-size: 1.2rem;\n",
405
+ " margin-bottom: 10px;\n",
406
+ "}\n",
407
+ "#T_cf7d5 th.col_heading {\n",
408
+ " font-weight: 700;\n",
409
+ " padding: 8px;\n",
410
+ " text-align: center;\n",
411
+ "}\n",
412
+ "#T_cf7d5 td {\n",
413
+ " padding: 8px;\n",
414
+ " vertical-align: center;\n",
415
+ "}\n",
416
+ "#T_cf7d5 table {\n",
417
+ " border-collapse: separate;\n",
418
+ " border-spacing: 0px;\n",
419
+ " width: 100%;\n",
420
+ "}\n",
421
+ "#T_cf7d5_row0_col0, #T_cf7d5_row1_col0, #T_cf7d5_row2_col0 {\n",
422
+ " background-color: #fafafa;\n",
423
+ " text-align: center;\n",
424
+ " font-weight: 600;\n",
425
+ " font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;\n",
426
+ "}\n",
427
+ "#T_cf7d5_row0_col1, #T_cf7d5_row2_col1 {\n",
428
+ " background-color: #006837;\n",
429
+ " color: #f1f1f1;\n",
430
+ " font-weight: 500;\n",
431
+ "}\n",
432
+ "#T_cf7d5_row1_col1 {\n",
433
+ " background-color: #016a38;\n",
434
+ " color: #f1f1f1;\n",
435
+ " font-weight: 500;\n",
436
+ "}\n",
437
+ "</style>\n",
438
+ "<table id=\"T_cf7d5\">\n",
439
+ " <caption>Product Prediction Results</caption>\n",
440
+ " <thead>\n",
441
+ " <tr>\n",
442
+ " <th id=\"T_cf7d5_level0_col0\" class=\"col_heading level0 col0\" >Predicted Reaction</th>\n",
443
+ " <th id=\"T_cf7d5_level0_col1\" class=\"col_heading level0 col1\" >confidence</th>\n",
444
+ " </tr>\n",
445
+ " </thead>\n",
446
+ " <tbody>\n",
447
+ " <tr>\n",
448
+ " <td id=\"T_cf7d5_row0_col0\" class=\"data row0 col0\" >CCI.O=Cc1ccc([N+](=O)[O-])c(O)c1 → CCOc1cc(C=O)ccc1[N+](=O)[O-]</td>\n",
449
+ " <td id=\"T_cf7d5_row0_col1\" class=\"data row0 col1\" >0.999</td>\n",
450
+ " </tr>\n",
451
+ " <tr>\n",
452
+ " <td id=\"T_cf7d5_row1_col0\" class=\"data row1 col0\" >CCOc1cc(O)c(C=O)cc1OCC.OCCCBr → CCOc1cc(C=O)c(OCCCO)cc1OCC</td>\n",
453
+ " <td id=\"T_cf7d5_row1_col1\" class=\"data row1 col1\" >0.993</td>\n",
454
+ " </tr>\n",
455
+ " <tr>\n",
456
+ " <td id=\"T_cf7d5_row2_col0\" class=\"data row2 col0\" >C=CCc1cc(OCc2ccccc2)ccc1O.CCBr → C=CCc1cc(OCc2ccccc2)ccc1OCC</td>\n",
457
+ " <td id=\"T_cf7d5_row2_col1\" class=\"data row2 col1\" >0.997</td>\n",
458
+ " </tr>\n",
459
+ " </tbody>\n",
460
+ "</table>\n"
461
+ ],
462
+ "text/plain": [
463
+ "<IPython.core.display.HTML object>"
464
+ ]
465
+ },
466
+ "metadata": {},
467
+ "output_type": "display_data"
468
+ }
469
+ ],
470
+ "source": [
471
+ "# Build a dataframe with the result in the response\n",
472
+ "df = build_results_dataframe(response[\"result\"])\n",
473
+ "\n",
474
+ "# Create and show the data in a table\n",
475
+ "style_results_table(df, caption=\"Product Prediction Results\", align=\"center\")"
476
+ ]
477
+ },
478
+ {
479
+ "cell_type": "markdown",
480
+ "id": "71f84c47-07d2-405f-b824-86d95dc9ea2a",
481
+ "metadata": {},
482
+ "source": [
483
+ "### Product prediction for a single reaction - top 3 results"
484
+ ]
485
+ },
486
+ {
487
+ "cell_type": "code",
488
+ "execution_count": 7,
489
+ "id": "8c8fb6b9-2a2b-42b6-9fb8-e5356377a7c4",
490
+ "metadata": {},
491
+ "outputs": [
492
+ {
493
+ "name": "stdout",
494
+ "output_type": "stream",
495
+ "text": [
496
+ "Task sent. Assigned task_id: 10bdf9f8-e49f-4c5f-8f11-5d03f3f75d83\n",
497
+ "State: PENDING | Info: None\n",
498
+ "State: STARTED | Info: None\n",
499
+ "State: SUCCESS | Info: {'result': [{'reactants': 'CCI.O=Cc1ccc([N+](=O)[O-])c(O)c1', 'product': 'CCOc1cc(C=O)ccc1[N+](=O)[O-]', 'confidence': 0.9987357632971271, 'smiles': 'CCI.O=Cc1ccc([N+](=O)[O-])c(O)c1>>CCOc1cc(C=O)ccc1[N+](=O)[O-]'}, {'reactants': 'CCI.O=Cc1ccc([N+](=O)[O-])c(O)c1', 'product': 'CCOc1cc(C=O)ccc1[N+](=O)[O-].CCOc1cc(C=O)ccc1[N+](=O)[O-]', 'confidence': 0.8466943868002893, 'smiles': 'CCI.O=Cc1ccc([N+](=O)[O-])c(O)c1>>CCOc1cc(C=O)ccc1[N+](=O)[O-].CCOc1cc(C=O)ccc1[N+](=O)[O-]'}, {'reactants': 'CCI.O=Cc1ccc([N+](=O)[O-])c(O)c1', 'product': 'CCOc1cc(C=O)ccc1[N+](=O)[O-].CCOc1cc(CO)ccc1[N+](=O)[O-]', 'confidence': 0.7820610951050019, 'smiles': 'CCI.O=Cc1ccc([N+](=O)[O-])c(O)c1>>CCOc1cc(C=O)ccc1[N+](=O)[O-].CCOc1cc(CO)ccc1[N+](=O)[O-]'}], 'time': 3.020906448364258}\n"
500
+ ]
501
+ }
502
+ ],
503
+ "source": [
504
+ "# Set up a list of reactants to make predictions\n",
505
+ "reactants_list = [\"CCI.O=Cc1ccc([N+](=O)[O-])c(O)c1\"]\n",
506
+ "\n",
507
+ "# Setup task kwargs\n",
508
+ "kwargs = {\n",
509
+ " \"topn\": 3, # Number of results per reactant\n",
510
+ " \"num_beams\": 5, # Number of beams used for prediction. Must be >= topn\n",
511
+ " \"device\": None, # Device used for predicting, either \"cuda\" or \"cpu\", None defaults to cuda if available\n",
512
+ " \"ckpt_forward\": \"Pistachio2025Q2-Forward\", # Default forward model\n",
513
+ " \"vocab\": \"Pistachio2025Q2\", # Vocab for default forward model\n",
514
+ " # \"ckpt_forward_path\": \"models/forward/Pistachio2025Q2-Forward.ckpt\", # Can be used instead of ckpt_forward\n",
515
+ " # \"vocab_path\": \"vocab/Pistachio2025Q2.txt\", # Can be used instead of vocab\n",
516
+ "}\n",
517
+ "\n",
518
+ "# Send the product_prediction task with the reaction list and kwargs\n",
519
+ "task = celery_app.send_task(\n",
520
+ " \"tasks.product_prediction\",\n",
521
+ " [reactants_list],\n",
522
+ " kwargs=kwargs,\n",
523
+ " queue=\"product_prediction\",\n",
524
+ ")\n",
525
+ "print(\"Task sent. Assigned task_id: {}\".format(task.id))\n",
526
+ "\n",
527
+ "# Use the task id to get the result. Increase timeout if needed.\n",
528
+ "response = wait_for_result(celery_app, task.id, timeout=180)"
529
+ ]
530
+ },
531
+ {
532
+ "cell_type": "code",
533
+ "execution_count": 8,
534
+ "id": "2872dd07-110a-4cdb-a905-588e31bdae49",
535
+ "metadata": {},
536
+ "outputs": [
537
+ {
538
+ "data": {
539
+ "text/html": [
540
+ "<style type=\"text/css\">\n",
541
+ "#T_dbf15 caption {\n",
542
+ " caption-side: top;\n",
543
+ " font-weight: 700;\n",
544
+ " font-size: 1.2rem;\n",
545
+ " margin-bottom: 10px;\n",
546
+ "}\n",
547
+ "#T_dbf15 th.col_heading {\n",
548
+ " font-weight: 700;\n",
549
+ " padding: 8px;\n",
550
+ " text-align: center;\n",
551
+ "}\n",
552
+ "#T_dbf15 td {\n",
553
+ " padding: 8px;\n",
554
+ " vertical-align: center;\n",
555
+ "}\n",
556
+ "#T_dbf15 table {\n",
557
+ " border-collapse: separate;\n",
558
+ " border-spacing: 0px;\n",
559
+ " width: 100%;\n",
560
+ "}\n",
561
+ "#T_dbf15_row0_col0, #T_dbf15_row1_col0, #T_dbf15_row2_col0 {\n",
562
+ " background-color: #fafafa;\n",
563
+ " text-align: left;\n",
564
+ " font-weight: 600;\n",
565
+ " font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;\n",
566
+ "}\n",
567
+ "#T_dbf15_row0_col1 {\n",
568
+ " background-color: #006837;\n",
569
+ " color: #f1f1f1;\n",
570
+ " font-weight: 500;\n",
571
+ "}\n",
572
+ "#T_dbf15_row1_col1 {\n",
573
+ " background-color: #42ac5a;\n",
574
+ " color: #f1f1f1;\n",
575
+ " font-weight: 500;\n",
576
+ "}\n",
577
+ "#T_dbf15_row2_col1 {\n",
578
+ " background-color: #70c164;\n",
579
+ " color: #000000;\n",
580
+ " font-weight: 500;\n",
581
+ "}\n",
582
+ "</style>\n",
583
+ "<table id=\"T_dbf15\">\n",
584
+ " <caption>Product Prediction Results</caption>\n",
585
+ " <thead>\n",
586
+ " <tr>\n",
587
+ " <th id=\"T_dbf15_level0_col0\" class=\"col_heading level0 col0\" >Predicted Reaction</th>\n",
588
+ " <th id=\"T_dbf15_level0_col1\" class=\"col_heading level0 col1\" >confidence</th>\n",
589
+ " </tr>\n",
590
+ " </thead>\n",
591
+ " <tbody>\n",
592
+ " <tr>\n",
593
+ " <td id=\"T_dbf15_row0_col0\" class=\"data row0 col0\" >CCI.O=Cc1ccc([N+](=O)[O-])c(O)c1 → CCOc1cc(C=O)ccc1[N+](=O)[O-]</td>\n",
594
+ " <td id=\"T_dbf15_row0_col1\" class=\"data row0 col1\" >0.999</td>\n",
595
+ " </tr>\n",
596
+ " <tr>\n",
597
+ " <td id=\"T_dbf15_row1_col0\" class=\"data row1 col0\" >CCI.O=Cc1ccc([N+](=O)[O-])c(O)c1 → CCOc1cc(C=O)ccc1[N+](=O)[O-].CCOc1cc(C=O)ccc1[N+](=O)[O-]</td>\n",
598
+ " <td id=\"T_dbf15_row1_col1\" class=\"data row1 col1\" >0.847</td>\n",
599
+ " </tr>\n",
600
+ " <tr>\n",
601
+ " <td id=\"T_dbf15_row2_col0\" class=\"data row2 col0\" >CCI.O=Cc1ccc([N+](=O)[O-])c(O)c1 → CCOc1cc(C=O)ccc1[N+](=O)[O-].CCOc1cc(CO)ccc1[N+](=O)[O-]</td>\n",
602
+ " <td id=\"T_dbf15_row2_col1\" class=\"data row2 col1\" >0.782</td>\n",
603
+ " </tr>\n",
604
+ " </tbody>\n",
605
+ "</table>\n"
606
+ ],
607
+ "text/plain": [
608
+ "<IPython.core.display.HTML object>"
609
+ ]
610
+ },
611
+ "metadata": {},
612
+ "output_type": "display_data"
613
+ }
614
+ ],
615
+ "source": [
616
+ "# Build a dataframe with the result in the response\n",
617
+ "df = build_results_dataframe(response[\"result\"])\n",
618
+ "\n",
619
+ "# Create and show the data in a table\n",
620
+ "style_results_table(df, caption=\"Product Prediction Results\")"
621
+ ]
622
+ },
623
+ {
624
+ "cell_type": "markdown",
625
+ "id": "ee465c64-f56c-4295-8f29-5adf23b494c1",
626
+ "metadata": {},
627
+ "source": [
628
+ "### Retrosynthesis prediction"
629
+ ]
630
+ },
631
+ {
632
+ "cell_type": "code",
633
+ "execution_count": 9,
634
+ "id": "c46f70ed-2b19-43cb-b5d9-ae846cdc99c7",
635
+ "metadata": {},
636
+ "outputs": [
637
+ {
638
+ "name": "stdout",
639
+ "output_type": "stream",
640
+ "text": [
641
+ "Task sent. Assigned task_id: 2ac795d1-67f2-42ab-b6e8-db9d0f04e37d\n",
642
+ "State: PENDING | Info: None\n",
643
+ "State: STARTED | Info: None\n",
644
+ "State: SUCCESS | Info: {'result': [{'reactants': 'C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl]', 'product': 'C=CC(=C)C[Si](C)(C)C', 'confidence': 0.8633497322540727, 'smiles': 'C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl]>>C=CC(=C)C[Si](C)(C)C'}, {'reactants': 'C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl].N#N', 'product': 'C=CC(=C)C[Si](C)(C)C', 'confidence': 0.853555662500899, 'smiles': 'C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl].N#N>>C=CC(=C)C[Si](C)(C)C'}, {'reactants': 'C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl]', 'product': 'C=CC(=C)C[Si](C)(C)C', 'confidence': 0.8437746350333076, 'smiles': 'C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl]>>C=CC(=C)C[Si](C)(C)C'}, {'reactants': 'C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl].O.[Cl-].[NH4+]', 'product': 'C=CC(=C)C[Si](C)(C)C', 'confidence': 0.8341045215463807, 'smiles': 'C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl].O.[Cl-].[NH4+]>>C=CC(=C)C[Si](C)(C)C'}, {'reactants': 'C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl].N#N', 'product': 'C=CC(=C)C[Si](C)(C)C', 'confidence': 0.833862536714611, 'smiles': 'C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl].N#N>>C=CC(=C)C[Si](C)(C)C'}, {'reactants': 'C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl].O.[Cl-].[NH4+]', 'product': 'C=CC(=C)C[Si](C)(C)C', 'confidence': 0.8328274628653165, 'smiles': 'C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl].O.[Cl-].[NH4+]>>C=CC(=C)C[Si](C)(C)C'}, {'reactants': 'C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl].Cl.O', 'product': 'C=CC(=C)C[Si](C)(C)C', 'confidence': 0.8303334989016697, 'smiles': 'C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl].Cl.O>>C=CC(=C)C[Si](C)(C)C'}, {'reactants': 'C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl].O.[Cl-].[Mg].[NH4+]', 'product': 'C=CC(=C)C[Si](C)(C)C', 'confidence': 0.8301065237221663, 'smiles': 'C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl].O.[Cl-].[Mg].[NH4+]>>C=CC(=C)C[Si](C)(C)C'}], 'time': 14.295113801956177}\n"
645
+ ]
646
+ }
647
+ ],
648
+ "source": [
649
+ "# Choose product for retrosynthesis prediction\n",
650
+ "product = \"C=CC(=C)C[Si](C)(C)C\"\n",
651
+ "\n",
652
+ "# Setup task kwargs\n",
653
+ "kwargs = {\n",
654
+ " \"topn\": 15, # Number of results per reactant\n",
655
+ " \"num_beams\": 15, # Number of beams used for prediction. Must be >= topn\n",
656
+ " \"fap\": 0.6, # Forward likelihood acceptance probability (not length averaged)\n",
657
+ " \"fld\": 0.2, # Forward likelihood delta required between the top2 forward prediction results\n",
658
+ " \"device\": None, # Device used for predicting, either \"cuda\" or \"cpu\", None defaults to cuda if available\n",
659
+ " \"ckpt_forward\": \"Pistachio2025Q2-Forward\", # Default forward model\n",
660
+ " \"ckpt_retro\": \"Pistachio2025Q2-Retro\", # Default retrosynthesis model\n",
661
+ " \"vocab\": \"Pistachio2025Q2\", # Vocab for default forward and retrosynthesis models\n",
662
+ " # \"ckpt_forward_path\": \"models/forward/Pistachio2025Q2-Forward.ckpt\", # Can be used instead of ckpt_forward\n",
663
+ " # \"ckpt_retro_path\": \"models/retrosynthesis/Pistachio2025Q2-Retro.ckpt\", # Can be used instead of ckpt_retro\n",
664
+ " # \"vocab_path\": \"vocab/Pistachio2025Q2.txt\", # Can be used instead of vocab\n",
665
+ "}\n",
666
+ "\n",
667
+ "# Send the retro_prediction task with the product and kwargs\n",
668
+ "task = celery_app.send_task(\n",
669
+ " \"tasks.retro_prediction\",\n",
670
+ " [product],\n",
671
+ " kwargs=kwargs,\n",
672
+ " queue=\"retro_prediction\",\n",
673
+ ")\n",
674
+ "print(\"Task sent. Assigned task_id: {}\".format(task.id))\n",
675
+ "\n",
676
+ "# Use the task id to get the result. Increase timeout if needed.\n",
677
+ "response = wait_for_result(celery_app, task.id, timeout=300)"
678
+ ]
679
+ },
680
+ {
681
+ "cell_type": "code",
682
+ "execution_count": 10,
683
+ "id": "f7601ac3-2a27-43eb-956a-209c8dc3d4cc",
684
+ "metadata": {},
685
+ "outputs": [
686
+ {
687
+ "data": {
688
+ "text/html": [
689
+ "<style type=\"text/css\">\n",
690
+ "#T_90827 caption {\n",
691
+ " caption-side: top;\n",
692
+ " font-weight: 700;\n",
693
+ " font-size: 1.2rem;\n",
694
+ " margin-bottom: 10px;\n",
695
+ "}\n",
696
+ "#T_90827 th.col_heading {\n",
697
+ " font-weight: 700;\n",
698
+ " padding: 8px;\n",
699
+ " text-align: center;\n",
700
+ "}\n",
701
+ "#T_90827 td {\n",
702
+ " padding: 8px;\n",
703
+ " vertical-align: center;\n",
704
+ "}\n",
705
+ "#T_90827 table {\n",
706
+ " border-collapse: separate;\n",
707
+ " border-spacing: 0px;\n",
708
+ " width: 100%;\n",
709
+ "}\n",
710
+ "#T_90827_row0_col0, #T_90827_row1_col0, #T_90827_row2_col0, #T_90827_row3_col0, #T_90827_row4_col0, #T_90827_row5_col0, #T_90827_row6_col0, #T_90827_row7_col0 {\n",
711
+ " background-color: #fafafa;\n",
712
+ " text-align: right;\n",
713
+ " font-weight: 600;\n",
714
+ " font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;\n",
715
+ "}\n",
716
+ "#T_90827_row0_col1 {\n",
717
+ " background-color: #33a456;\n",
718
+ " color: #f1f1f1;\n",
719
+ " font-weight: 500;\n",
720
+ "}\n",
721
+ "#T_90827_row1_col1 {\n",
722
+ " background-color: #3ca959;\n",
723
+ " color: #f1f1f1;\n",
724
+ " font-weight: 500;\n",
725
+ "}\n",
726
+ "#T_90827_row2_col1 {\n",
727
+ " background-color: #42ac5a;\n",
728
+ " color: #f1f1f1;\n",
729
+ " font-weight: 500;\n",
730
+ "}\n",
731
+ "#T_90827_row3_col1, #T_90827_row4_col1, #T_90827_row5_col1 {\n",
732
+ " background-color: #4bb05c;\n",
733
+ " color: #f1f1f1;\n",
734
+ " font-weight: 500;\n",
735
+ "}\n",
736
+ "#T_90827_row6_col1, #T_90827_row7_col1 {\n",
737
+ " background-color: #4eb15d;\n",
738
+ " color: #f1f1f1;\n",
739
+ " font-weight: 500;\n",
740
+ "}\n",
741
+ "</style>\n",
742
+ "<table id=\"T_90827\">\n",
743
+ " <caption>Retrosynthesis Prediction Results</caption>\n",
744
+ " <thead>\n",
745
+ " <tr>\n",
746
+ " <th id=\"T_90827_level0_col0\" class=\"col_heading level0 col0\" >Predicted Reaction</th>\n",
747
+ " <th id=\"T_90827_level0_col1\" class=\"col_heading level0 col1\" >confidence</th>\n",
748
+ " </tr>\n",
749
+ " </thead>\n",
750
+ " <tbody>\n",
751
+ " <tr>\n",
752
+ " <td id=\"T_90827_row0_col0\" class=\"data row0 col0\" >C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl] → C=CC(=C)C[Si](C)(C)C</td>\n",
753
+ " <td id=\"T_90827_row0_col1\" class=\"data row0 col1\" >0.863</td>\n",
754
+ " </tr>\n",
755
+ " <tr>\n",
756
+ " <td id=\"T_90827_row1_col0\" class=\"data row1 col0\" >C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl].N#N → C=CC(=C)C[Si](C)(C)C</td>\n",
757
+ " <td id=\"T_90827_row1_col1\" class=\"data row1 col1\" >0.854</td>\n",
758
+ " </tr>\n",
759
+ " <tr>\n",
760
+ " <td id=\"T_90827_row2_col0\" class=\"data row2 col0\" >C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl] → C=CC(=C)C[Si](C)(C)C</td>\n",
761
+ " <td id=\"T_90827_row2_col1\" class=\"data row2 col1\" >0.844</td>\n",
762
+ " </tr>\n",
763
+ " <tr>\n",
764
+ " <td id=\"T_90827_row3_col0\" class=\"data row3 col0\" >C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl].O.[Cl-].[NH4+] → C=CC(=C)C[Si](C)(C)C</td>\n",
765
+ " <td id=\"T_90827_row3_col1\" class=\"data row3 col1\" >0.834</td>\n",
766
+ " </tr>\n",
767
+ " <tr>\n",
768
+ " <td id=\"T_90827_row4_col0\" class=\"data row4 col0\" >C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl].N#N → C=CC(=C)C[Si](C)(C)C</td>\n",
769
+ " <td id=\"T_90827_row4_col1\" class=\"data row4 col1\" >0.834</td>\n",
770
+ " </tr>\n",
771
+ " <tr>\n",
772
+ " <td id=\"T_90827_row5_col0\" class=\"data row5 col0\" >C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl].O.[Cl-].[NH4+] → C=CC(=C)C[Si](C)(C)C</td>\n",
773
+ " <td id=\"T_90827_row5_col1\" class=\"data row5 col1\" >0.833</td>\n",
774
+ " </tr>\n",
775
+ " <tr>\n",
776
+ " <td id=\"T_90827_row6_col0\" class=\"data row6 col0\" >C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl].Cl.O → C=CC(=C)C[Si](C)(C)C</td>\n",
777
+ " <td id=\"T_90827_row6_col1\" class=\"data row6 col1\" >0.830</td>\n",
778
+ " </tr>\n",
779
+ " <tr>\n",
780
+ " <td id=\"T_90827_row7_col0\" class=\"data row7 col0\" >C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl].O.[Cl-].[Mg].[NH4+] → C=CC(=C)C[Si](C)(C)C</td>\n",
781
+ " <td id=\"T_90827_row7_col1\" class=\"data row7 col1\" >0.830</td>\n",
782
+ " </tr>\n",
783
+ " </tbody>\n",
784
+ "</table>\n"
785
+ ],
786
+ "text/plain": [
787
+ "<IPython.core.display.HTML object>"
788
+ ]
789
+ },
790
+ "metadata": {},
791
+ "output_type": "display_data"
792
+ }
793
+ ],
794
+ "source": [
795
+ "# Build a dataframe with the result in the response\n",
796
+ "df = build_results_dataframe(response[\"result\"])\n",
797
+ "\n",
798
+ "# Create and show the data in a table\n",
799
+ "style_results_table(df, caption=\"Retrosynthesis Prediction Results\", align=\"right\")"
800
+ ]
801
+ },
802
+ {
803
+ "cell_type": "markdown",
804
+ "id": "98ba77e3-fcf3-488a-b51c-54ce2dcfe7e0",
805
+ "metadata": {},
806
+ "source": [
807
+ "### Retro tree prediction"
808
+ ]
809
+ },
810
+ {
811
+ "cell_type": "code",
812
+ "execution_count": 11,
813
+ "id": "4cdba192-ec0c-494d-b356-b48cbdeb5cb8",
814
+ "metadata": {},
815
+ "outputs": [
816
+ {
817
+ "name": "stdout",
818
+ "output_type": "stream",
819
+ "text": [
820
+ "Task sent. Assigned task_id: 26ea63e0-2527-4baf-9905-673f5d98442e\n",
821
+ "State: PENDING | Info: None\n",
822
+ "State: STARTED | Info: None\n",
823
+ "State: SUCCESS | Info: {'result': [{'open_nodes': ['ClCCl', '[Cl][Ru]([Cl])(=[CH]c1ccccc1)([P](C1CCCCC1)(C1CCCCC1)C1CCCCC1)[P](C1CCCCC1)(C1CCCCC1)C1CCCCC1', 'C=CCC1C(=O)OC(=O)C1CC(=C)C[Si](C)(C)C'], 'expanded': ['C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'], 'score': 0.7056475777714836, 'steps': 1, 'arcs': [{'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'reactants': ['C=CCC1C(=O)OC(=O)C1CC(=C)C[Si](C)(C)C', 'ClCCl', '[Cl][Ru]([Cl])(=[CH]c1ccccc1)([P](C1CCCCC1)(C1CCCCC1)C1CCCCC1)[P](C1CCCCC1)(C1CCCCC1)C1CCCCC1'], 'forward_likelihood': 0.8842267990112305, 'arc_score': 0.7056475777704836, 'confidence': 0.9439050974158504, 'metadata': {'reactants': 'C=CCC1C(=O)OC(=O)C1CC(=C)C[Si](C)(C)C.ClCCl.[Cl][Ru]([Cl])(=[CH]c1ccccc1)([P](C1CCCCC1)(C1CCCCC1)C1CCCCC1)[P](C1CCCCC1)(C1CCCCC1)C1CCCCC1', 'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'confidence': 0.9439050974158504, 'smiles': 'C=CCC1C(=O)OC(=O)C1CC(=C)C[Si](C)(C)C.ClCCl.[Cl][Ru]([Cl])(=[CH]c1ccccc1)([P](C1CCCCC1)(C1CCCCC1)C1CCCCC1)[P](C1CCCCC1)(C1CCCCC1)C1CCCCC1>>C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'}}]}, {'open_nodes': ['ClCCl', 'Cc1cc(C)c(N2CCN(c3c(C)cc(C)cc3C)[C]2=[Ru]([Cl])([Cl])(=[CH]c2ccccc2)[P](C2CCCCC2)(C2CCCCC2)C2CCCCC2)c(C)c1', 'C=CCC1C(=O)OC(=O)C1CC(=C)C[Si](C)(C)C'], 'expanded': ['C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'], 'score': 0.37149402762328515, 'steps': 1, 'arcs': [{'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'reactants': ['C=CCC1C(=O)OC(=O)C1CC(=C)C[Si](C)(C)C', 'Cc1cc(C)c(N2CCN(c3c(C)cc(C)cc3C)[C]2=[Ru]([Cl])([Cl])(=[CH]c2ccccc2)[P](C2CCCCC2)(C2CCCCC2)C2CCCCC2)c(C)c1', 'ClCCl'], 'forward_likelihood': 0.8087736368179321, 'arc_score': 0.3714940276222852, 'confidence': 0.9503329457084253, 'metadata': {'reactants': 'C=CCC1C(=O)OC(=O)C1CC(=C)C[Si](C)(C)C.Cc1cc(C)c(N2CCN(c3c(C)cc(C)cc3C)[C]2=[Ru]([Cl])([Cl])(=[CH]c2ccccc2)[P](C2CCCCC2)(C2CCCCC2)C2CCCCC2)c(C)c1.ClCCl', 'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'confidence': 0.9503329457084253, 'smiles': 'C=CCC1C(=O)OC(=O)C1CC(=C)C[Si](C)(C)C.Cc1cc(C)c(N2CCN(c3c(C)cc(C)cc3C)[C]2=[Ru]([Cl])([Cl])(=[CH]c2ccccc2)[P](C2CCCCC2)(C2CCCCC2)C2CCCCC2)c(C)c1.ClCCl>>C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'}}]}, {'open_nodes': ['ClCCl', 'C=CCC1C(=O)OC(=O)C1CC(=C[Si](C)(C)C)C[Si](C)(C)C', 'Cc1cc(C)c(N2CCN(c3c(C)cc(C)cc3C)[C]2=[Ru]([Cl])([Cl])(=[CH]c2ccccc2)[P](C2CCCCC2)(C2CCCCC2)C2CCCCC2)c(C)c1'], 'expanded': ['C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'], 'score': 0.33907517755787414, 'steps': 1, 'arcs': [{'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'reactants': ['C=CCC1C(=O)OC(=O)C1CC(=C[Si](C)(C)C)C[Si](C)(C)C', 'Cc1cc(C)c(N2CCN(c3c(C)cc(C)cc3C)[C]2=[Ru]([Cl])([Cl])(=[CH]c2ccccc2)[P](C2CCCCC2)(C2CCCCC2)C2CCCCC2)c(C)c1', 'ClCCl'], 'forward_likelihood': 0.8071097731590271, 'arc_score': 0.3390751775568742, 'confidence': 0.9455194125006082, 'metadata': {'reactants': 'C=CCC1C(=O)OC(=O)C1CC(=C[Si](C)(C)C)C[Si](C)(C)C.Cc1cc(C)c(N2CCN(c3c(C)cc(C)cc3C)[C]2=[Ru]([Cl])([Cl])(=[CH]c2ccccc2)[P](C2CCCCC2)(C2CCCCC2)C2CCCCC2)c(C)c1.ClCCl', 'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'confidence': 0.9455194125006082, 'smiles': 'C=CCC1C(=O)OC(=O)C1CC(=C[Si](C)(C)C)C[Si](C)(C)C.Cc1cc(C)c(N2CCN(c3c(C)cc(C)cc3C)[C]2=[Ru]([Cl])([Cl])(=[CH]c2ccccc2)[P](C2CCCCC2)(C2CCCCC2)C2CCCCC2)c(C)c1.ClCCl>>C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'}}]}, {'open_nodes': ['C1CCOC1', 'O=C1C=CC(=O)O1', 'C=CC(=C)O', 'ClP(Cl)(Cl)(Cl)Cl', 'Cc1ccccc1', 'C[Si](C)(C)[CH2][Mg][Cl]'], 'expanded': ['C=CC(=C)C[Si](C)(C)C', 'C=CC(=C)Cl', 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'], 'score': 0.24857583642032657, 'steps': 3, 'arcs': [{'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'reactants': ['C=CC(=C)C[Si](C)(C)C', 'Cc1ccccc1', 'O=C1C=CC(=O)O1'], 'forward_likelihood': 0.39315515756607056, 'arc_score': 0.43673660563264405, 'confidence': 0.9271861522313083, 'metadata': {'reactants': 'C=CC(=C)C[Si](C)(C)C.Cc1ccccc1.O=C1C=CC(=O)O1', 'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'confidence': 0.9271861522313083, 'smiles': 'C=CC(=C)C[Si](C)(C)C.Cc1ccccc1.O=C1C=CC(=O)O1>>C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'}}, {'product': 'C=CC(=C)C[Si](C)(C)C', 'reactants': ['C1CCOC1', 'C=CC(=C)Cl', 'C[Si](C)(C)[CH2][Mg][Cl]'], 'forward_likelihood': 0.9396551847457886, 'arc_score': 1.0504772488213368, 'confidence': 0.8633497322540727, 'metadata': {'reactants': 'C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl]', 'product': 'C=CC(=C)C[Si](C)(C)C', 'confidence': 0.8633497322540727, 'smiles': 'C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl]>>C=CC(=C)C[Si](C)(C)C'}}, {'product': 'C=CC(=C)Cl', 'reactants': ['C=CC(=C)O', 'ClP(Cl)(Cl)(Cl)Cl'], 'forward_likelihood': 0.5561506152153015, 'arc_score': 0.541817045936755, 'confidence': 0.8283158501723433, 'metadata': {'reactants': 'C=CC(=C)O.ClP(Cl)(Cl)(Cl)Cl', 'product': 'C=CC(=C)Cl', 'confidence': 0.8283158501723433, 'smiles': 'C=CC(=C)O.ClP(Cl)(Cl)(Cl)Cl>>C=CC(=C)Cl'}}]}, {'open_nodes': ['O=C1C=CC(=O)O1', '[NH4+]', 'C=CC(=C)O', 'ClP(Cl)(Cl)(Cl)Cl', '[Cl-]', 'Cc1ccccc1', 'C[Si](C)(C)[CH2][Mg][Cl]', 'C1CCOC1', 'O'], 'expanded': ['C=CC(=C)C[Si](C)(C)C', 'C=CC(=C)Cl', 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'], 'score': 0.2424893468373566, 'steps': 3, 'arcs': [{'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'reactants': ['C=CC(=C)C[Si](C)(C)C', 'Cc1ccccc1', 'O=C1C=CC(=O)O1'], 'forward_likelihood': 0.39315515756607056, 'arc_score': 0.43673660563264405, 'confidence': 0.9271861522313083, 'metadata': {'reactants': 'C=CC(=C)C[Si](C)(C)C.Cc1ccccc1.O=C1C=CC(=O)O1', 'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'confidence': 0.9271861522313083, 'smiles': 'C=CC(=C)C[Si](C)(C)C.Cc1ccccc1.O=C1C=CC(=O)O1>>C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'}}, {'product': 'C=CC(=C)C[Si](C)(C)C', 'reactants': ['C1CCOC1', 'C=CC(=C)Cl', 'C[Si](C)(C)[CH2][Mg][Cl]', 'O', '[Cl-]', '[NH4+]'], 'forward_likelihood': 0.9166916608810425, 'arc_score': 1.02475584756135, 'confidence': 0.8341045215463807, 'metadata': {'reactants': 'C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl].O.[Cl-].[NH4+]', 'product': 'C=CC(=C)C[Si](C)(C)C', 'confidence': 0.8341045215463807, 'smiles': 'C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl].O.[Cl-].[NH4+]>>C=CC(=C)C[Si](C)(C)C'}}, {'product': 'C=CC(=C)Cl', 'reactants': ['C=CC(=C)O', 'ClP(Cl)(Cl)(Cl)Cl'], 'forward_likelihood': 0.5561506152153015, 'arc_score': 0.541817045936755, 'confidence': 0.8283158501723433, 'metadata': {'reactants': 'C=CC(=C)O.ClP(Cl)(Cl)(Cl)Cl', 'product': 'C=CC(=C)Cl', 'confidence': 0.8283158501723433, 'smiles': 'C=CC(=C)O.ClP(Cl)(Cl)(Cl)Cl>>C=CC(=C)Cl'}}]}, {'open_nodes': ['O=C1C=CC(=O)O1', 'C=CC(=C)O', 'CCOCC', 'ClP(Cl)(Cl)(Cl)Cl', 'Cc1ccccc1', 'C[Si](C)(C)[CH2][Mg][Cl]', 'C1CCOC1', 'Cl', 'O'], 'expanded': ['C=CC(=C)C[Si](C)(C)C', 'C=CC(=C)Cl', 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'], 'score': 0.23326155626917652, 'steps': 3, 'arcs': [{'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'reactants': ['C=CC(=C)C[Si](C)(C)C', 'Cc1ccccc1', 'O=C1C=CC(=O)O1'], 'forward_likelihood': 0.39315515756607056, 'arc_score': 0.43673660563264405, 'confidence': 0.9271861522313083, 'metadata': {'reactants': 'C=CC(=C)C[Si](C)(C)C.Cc1ccccc1.O=C1C=CC(=O)O1', 'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'confidence': 0.9271861522313083, 'smiles': 'C=CC(=C)C[Si](C)(C)C.Cc1ccccc1.O=C1C=CC(=O)O1>>C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'}}, {'product': 'C=CC(=C)C[Si](C)(C)C', 'reactants': ['C1CCOC1', 'C=CC(=C)Cl', 'CCOCC', 'C[Si](C)(C)[CH2][Mg][Cl]', 'Cl', 'O'], 'forward_likelihood': 0.8817673325538635, 'arc_score': 0.9857593618676268, 'confidence': 0.8303334989016697, 'metadata': {'reactants': 'C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl].Cl.O', 'product': 'C=CC(=C)C[Si](C)(C)C', 'confidence': 0.8303334989016697, 'smiles': 'C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl].Cl.O>>C=CC(=C)C[Si](C)(C)C'}}, {'product': 'C=CC(=C)Cl', 'reactants': ['C=CC(=C)O', 'ClP(Cl)(Cl)(Cl)Cl'], 'forward_likelihood': 0.5561506152153015, 'arc_score': 0.541817045936755, 'confidence': 0.8283158501723433, 'metadata': {'reactants': 'C=CC(=C)O.ClP(Cl)(Cl)(Cl)Cl', 'product': 'C=CC(=C)Cl', 'confidence': 0.8283158501723433, 'smiles': 'C=CC(=C)O.ClP(Cl)(Cl)(Cl)Cl>>C=CC(=C)Cl'}}]}, {'open_nodes': ['O=C1C=CC(=O)O1', '[NH4+]', 'C=CC(=C)O', 'CCOCC', '[Cl-]', 'ClP(Cl)(Cl)(Cl)Cl', 'Cc1ccccc1', 'C[Si](C)(C)[CH2][Mg][Cl]', 'C1CCOC1', 'O', '[Mg]'], 'expanded': ['C=CC(=C)C[Si](C)(C)C', 'C=CC(=C)Cl', 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'], 'score': 0.21788071070873768, 'steps': 3, 'arcs': [{'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'reactants': ['C=CC(=C)C[Si](C)(C)C', 'Cc1ccccc1', 'O=C1C=CC(=O)O1'], 'forward_likelihood': 0.39315515756607056, 'arc_score': 0.43673660563264405, 'confidence': 0.9271861522313083, 'metadata': {'reactants': 'C=CC(=C)C[Si](C)(C)C.Cc1ccccc1.O=C1C=CC(=O)O1', 'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'confidence': 0.9271861522313083, 'smiles': 'C=CC(=C)C[Si](C)(C)C.Cc1ccccc1.O=C1C=CC(=O)O1>>C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'}}, {'product': 'C=CC(=C)C[Si](C)(C)C', 'reactants': ['C1CCOC1', 'C=CC(=C)Cl', 'CCOCC', 'C[Si](C)(C)[CH2][Mg][Cl]', 'O', '[Cl-]', '[Mg]', '[NH4+]'], 'forward_likelihood': 0.8617748022079468, 'arc_score': 0.9207601706285793, 'confidence': 0.8301065237221663, 'metadata': {'reactants': 'C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl].O.[Cl-].[Mg].[NH4+]', 'product': 'C=CC(=C)C[Si](C)(C)C', 'confidence': 0.8301065237221663, 'smiles': 'C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl].O.[Cl-].[Mg].[NH4+]>>C=CC(=C)C[Si](C)(C)C'}}, {'product': 'C=CC(=C)Cl', 'reactants': ['C=CC(=C)O', 'ClP(Cl)(Cl)(Cl)Cl'], 'forward_likelihood': 0.5561506152153015, 'arc_score': 0.541817045936755, 'confidence': 0.8283158501723433, 'metadata': {'reactants': 'C=CC(=C)O.ClP(Cl)(Cl)(Cl)Cl', 'product': 'C=CC(=C)Cl', 'confidence': 0.8283158501723433, 'smiles': 'C=CC(=C)O.ClP(Cl)(Cl)(Cl)Cl>>C=CC(=C)Cl'}}]}, {'open_nodes': ['C1CCOC1', 'O=C1C=CC(=O)O1', 'C=CC(=C)O', 'O=S(Cl)Cl', 'Cc1ccccc1', 'C[Si](C)(C)[CH2][Mg][Cl]'], 'expanded': ['C=CC(=C)C[Si](C)(C)C', 'C=CC(=C)Cl', 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'], 'score': 0.18048843471208253, 'steps': 3, 'arcs': [{'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'reactants': ['C=CC(=C)C[Si](C)(C)C', 'Cc1ccccc1', 'O=C1C=CC(=O)O1'], 'forward_likelihood': 0.39315515756607056, 'arc_score': 0.43673660563264405, 'confidence': 0.9271861522313083, 'metadata': {'reactants': 'C=CC(=C)C[Si](C)(C)C.Cc1ccccc1.O=C1C=CC(=O)O1', 'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'confidence': 0.9271861522313083, 'smiles': 'C=CC(=C)C[Si](C)(C)C.Cc1ccccc1.O=C1C=CC(=O)O1>>C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'}}, {'product': 'C=CC(=C)C[Si](C)(C)C', 'reactants': ['C1CCOC1', 'C=CC(=C)Cl', 'C[Si](C)(C)[CH2][Mg][Cl]'], 'forward_likelihood': 0.9396551847457886, 'arc_score': 1.0504772488213368, 'confidence': 0.8633497322540727, 'metadata': {'reactants': 'C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl]', 'product': 'C=CC(=C)C[Si](C)(C)C', 'confidence': 0.8633497322540727, 'smiles': 'C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl]>>C=CC(=C)C[Si](C)(C)C'}}, {'product': 'C=CC(=C)Cl', 'reactants': ['C=CC(=C)O', 'O=S(Cl)Cl'], 'forward_likelihood': 0.403808057308197, 'arc_score': 0.39340795118967853, 'confidence': 0.8269444351225532, 'metadata': {'reactants': 'C=CC(=C)O.O=S(Cl)Cl', 'product': 'C=CC(=C)Cl', 'confidence': 0.8269444351225532, 'smiles': 'C=CC(=C)O.O=S(Cl)Cl>>C=CC(=C)Cl'}}]}, {'open_nodes': ['O=C1OC(=O)C2CC=CCC12', 'Cc1cc(C)c(N2CCN(c3c(C)cc(C)cc3C)[C]2=[Ru]([Cl])([Cl])=[CH]c2ccccc2)c(C)c1', 'ClCCl', 'C=CC(=C)C[Si](C)(C)C', 'C1CCC(P(C2CCCCC2)C2CCCCC2)CC1', 'N#N'], 'expanded': ['C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'], 'score': 0.1796396903569043, 'steps': 1, 'arcs': [{'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'reactants': ['C1CCC(P(C2CCCCC2)C2CCCCC2)CC1', 'C=CC(=C)C[Si](C)(C)C', 'Cc1cc(C)c(N2CCN(c3c(C)cc(C)cc3C)[C]2=[Ru]([Cl])([Cl])=[CH]c2ccccc2)c(C)c1', 'ClCCl', 'N#N', 'O=C1OC(=O)C2CC=CCC12'], 'forward_likelihood': 0.395216703414917, 'arc_score': 0.1796396903559043, 'confidence': 0.9290369156214755, 'metadata': {'reactants': 'C1CCC(P(C2CCCCC2)C2CCCCC2)CC1.C=CC(=C)C[Si](C)(C)C.Cc1cc(C)c(N2CCN(c3c(C)cc(C)cc3C)[C]2=[Ru]([Cl])([Cl])=[CH]c2ccccc2)c(C)c1.ClCCl.N#N.O=C1OC(=O)C2CC=CCC12', 'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'confidence': 0.9290369156214755, 'smiles': 'C1CCC(P(C2CCCCC2)C2CCCCC2)CC1.C=CC(=C)C[Si](C)(C)C.Cc1cc(C)c(N2CCN(c3c(C)cc(C)cc3C)[C]2=[Ru]([Cl])([Cl])=[CH]c2ccccc2)c(C)c1.ClCCl.N#N.O=C1OC(=O)C2CC=CCC12>>C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'}}]}, {'open_nodes': ['O=C1C=CC(=O)O1', '[NH4+]', 'C=CC(=C)O', '[Cl-]', 'O=S(Cl)Cl', 'Cc1ccccc1', 'C[Si](C)(C)[CH2][Mg][Cl]', 'C1CCOC1', 'O'], 'expanded': ['C=CC(=C)C[Si](C)(C)C', 'C=CC(=C)Cl', 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'], 'score': 0.17606909535254775, 'steps': 3, 'arcs': [{'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'reactants': ['C=CC(=C)C[Si](C)(C)C', 'Cc1ccccc1', 'O=C1C=CC(=O)O1'], 'forward_likelihood': 0.39315515756607056, 'arc_score': 0.43673660563264405, 'confidence': 0.9271861522313083, 'metadata': {'reactants': 'C=CC(=C)C[Si](C)(C)C.Cc1ccccc1.O=C1C=CC(=O)O1', 'product': 'C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1', 'confidence': 0.9271861522313083, 'smiles': 'C=CC(=C)C[Si](C)(C)C.Cc1ccccc1.O=C1C=CC(=O)O1>>C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1'}}, {'product': 'C=CC(=C)C[Si](C)(C)C', 'reactants': ['C1CCOC1', 'C=CC(=C)Cl', 'C[Si](C)(C)[CH2][Mg][Cl]', 'O', '[Cl-]', '[NH4+]'], 'forward_likelihood': 0.9166916608810425, 'arc_score': 1.02475584756135, 'confidence': 0.8341045215463807, 'metadata': {'reactants': 'C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl].O.[Cl-].[NH4+]', 'product': 'C=CC(=C)C[Si](C)(C)C', 'confidence': 0.8341045215463807, 'smiles': 'C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl].O.[Cl-].[NH4+]>>C=CC(=C)C[Si](C)(C)C'}}, {'product': 'C=CC(=C)Cl', 'reactants': ['C=CC(=C)O', 'O=S(Cl)Cl'], 'forward_likelihood': 0.403808057308197, 'arc_score': 0.39340795118967853, 'confidence': 0.8269444351225532, 'metadata': {'reactants': 'C=CC(=C)O.O=S(Cl)Cl', 'product': 'C=CC(=C)Cl', 'confidence': 0.8269444351225532, 'smiles': 'C=CC(=C)O.O=S(Cl)Cl>>C=CC(=C)Cl'}}]}], 'time': 1306.3053419589996}\n"
824
+ ]
825
+ }
826
+ ],
827
+ "source": [
828
+ "# Choose product for retrosynthesis tree prediction\n",
829
+ "product = \"C1C(C[Si](C)(C)C)=CCC2C(=O)OC(=O)C12\"\n",
830
+ "#product = \"Cc1cc2scnc2cc1N\"\n",
831
+ "#product = \"CC(C)(C(=O)O)C1C=CC=C(C2CC2)C1=O\"\n",
832
+ "#product = \"Nc1ccc2scnc2c1Br\"\n",
833
+ "\n",
834
+ "# Setup task kwargs\n",
835
+ "kwargs = {\n",
836
+ " \"topn\": 15, # Number of results per reactant\n",
837
+ " \"num_beams\": 15, # Number of beams used for prediction. Must be >= topn\n",
838
+ " \"fap\": 0.6, # Forward likelihood acceptance probability (not length averaged)\n",
839
+ " \"fld\": 0.2, # Forward likelihood delta required between the top2 forward prediction results\n",
840
+ " \"max_depth\": 4, # Max depth of the retrosynthesis tree\n",
841
+ " \"beam_width\": 6, # Max amount of nodes being expanded in each step\n",
842
+ " \"device\": None, # Device used for predicting, either \"cuda\" or \"cpu\", None defaults to cuda if available\n",
843
+ " \"ckpt_forward\": \"Pistachio2025Q2-Forward\", # Default forward model\n",
844
+ " \"ckpt_retro\": \"Pistachio2025Q2-Retro\", # Default retrosynthesis model\n",
845
+ " \"vocab\": \"Pistachio2025Q2\", # Vocab for default forward and retrosynthesis models\n",
846
+ " # \"ckpt_forward_path\": \"models/forward/Pistachio2025Q2-Forward.ckpt\", # Can be used instead of ckpt_forward\n",
847
+ " # \"ckpt_retro_path\": \"models/retrosynthesis/Pistachio2025Q2-Retro.ckpt\", # Can be used instead of ckpt_retro\n",
848
+ " # \"vocab_path\": \"vocab/Pistachio2025Q2.txt\", # Can be used instead of vocab\n",
849
+ "}\n",
850
+ "\n",
851
+ "# Send the retro_prediction_tree task with the product and kwargs\n",
852
+ "task = celery_app.send_task(\n",
853
+ " \"tasks.retro_prediction_tree\",\n",
854
+ " [product],\n",
855
+ " kwargs=kwargs,\n",
856
+ " queue=\"retro_prediction\",\n",
857
+ ")\n",
858
+ "print(\"Task sent. Assigned task_id: {}\".format(task.id))\n",
859
+ "\n",
860
+ "# Use the task id to get the result. Increase timeout if needed.\n",
861
+ "response = wait_for_result(celery_app, task.id, timeout=3000)"
862
+ ]
863
+ },
864
+ {
865
+ "cell_type": "code",
866
+ "execution_count": 12,
867
+ "id": "ee06bf27-b7ac-4d07-bff1-2cf99322b80d",
868
+ "metadata": {},
869
+ "outputs": [
870
+ {
871
+ "data": {
872
+ "text/markdown": [
873
+ "## Route 1\n",
874
+ "- **Score:** `0.706`\n",
875
+ "- **Steps:** `1`"
876
+ ],
877
+ "text/plain": [
878
+ "<IPython.core.display.Markdown object>"
879
+ ]
880
+ },
881
+ "metadata": {},
882
+ "output_type": "display_data"
883
+ },
884
+ {
885
+ "data": {
886
+ "text/markdown": [
887
+ "**Remaining molecules:** `ClCCl, [Cl][Ru]([Cl])(=[CH]c1ccccc1)([P](C1CCCCC1)(C1CCCCC1)C1CCCCC1)[P](C1CCCCC1)(C1CCCCC1)C1CCCCC1, C=CCC1C(=O)OC(=O)C1CC(=C)C[Si](C)(C)C`"
888
+ ],
889
+ "text/plain": [
890
+ "<IPython.core.display.Markdown object>"
891
+ ]
892
+ },
893
+ "metadata": {},
894
+ "output_type": "display_data"
895
+ },
896
+ {
897
+ "data": {
898
+ "text/markdown": [
899
+ "#### Step 1\n",
900
+ "**Product:** `C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1` \n",
901
+ "**Reactants:** `C=CCC1C(=O)OC(=O)C1CC(=C)C[Si](C)(C)C.ClCCl.[Cl][Ru]([Cl])(=[CH]c1ccccc1)([P](C1CCCCC1)(C1CCCCC1)C1CCCCC1)[P](C1CCCCC1)(C1CCCCC1)C1CCCCC1` \n",
902
+ "- Forward likelihood: `0.884` \n",
903
+ "- Arc score: `0.706` \n",
904
+ "- Retro confidence: `0.9439050974158504`"
905
+ ],
906
+ "text/plain": [
907
+ "<IPython.core.display.Markdown object>"
908
+ ]
909
+ },
910
+ "metadata": {},
911
+ "output_type": "display_data"
912
+ },
913
+ {
914
+ "data": {
915
+ "text/markdown": [
916
+ "## Route 2\n",
917
+ "- **Score:** `0.371`\n",
918
+ "- **Steps:** `1`"
919
+ ],
920
+ "text/plain": [
921
+ "<IPython.core.display.Markdown object>"
922
+ ]
923
+ },
924
+ "metadata": {},
925
+ "output_type": "display_data"
926
+ },
927
+ {
928
+ "data": {
929
+ "text/markdown": [
930
+ "**Remaining molecules:** `ClCCl, Cc1cc(C)c(N2CCN(c3c(C)cc(C)cc3C)[C]2=[Ru]([Cl])([Cl])(=[CH]c2ccccc2)[P](C2CCCCC2)(C2CCCCC2)C2CCCCC2)c(C)c1, C=CCC1C(=O)OC(=O)C1CC(=C)C[Si](C)(C)C`"
931
+ ],
932
+ "text/plain": [
933
+ "<IPython.core.display.Markdown object>"
934
+ ]
935
+ },
936
+ "metadata": {},
937
+ "output_type": "display_data"
938
+ },
939
+ {
940
+ "data": {
941
+ "text/markdown": [
942
+ "#### Step 1\n",
943
+ "**Product:** `C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1` \n",
944
+ "**Reactants:** `C=CCC1C(=O)OC(=O)C1CC(=C)C[Si](C)(C)C.Cc1cc(C)c(N2CCN(c3c(C)cc(C)cc3C)[C]2=[Ru]([Cl])([Cl])(=[CH]c2ccccc2)[P](C2CCCCC2)(C2CCCCC2)C2CCCCC2)c(C)c1.ClCCl` \n",
945
+ "- Forward likelihood: `0.809` \n",
946
+ "- Arc score: `0.371` \n",
947
+ "- Retro confidence: `0.9503329457084253`"
948
+ ],
949
+ "text/plain": [
950
+ "<IPython.core.display.Markdown object>"
951
+ ]
952
+ },
953
+ "metadata": {},
954
+ "output_type": "display_data"
955
+ },
956
+ {
957
+ "data": {
958
+ "text/markdown": [
959
+ "## Route 3\n",
960
+ "- **Score:** `0.339`\n",
961
+ "- **Steps:** `1`"
962
+ ],
963
+ "text/plain": [
964
+ "<IPython.core.display.Markdown object>"
965
+ ]
966
+ },
967
+ "metadata": {},
968
+ "output_type": "display_data"
969
+ },
970
+ {
971
+ "data": {
972
+ "text/markdown": [
973
+ "**Remaining molecules:** `ClCCl, C=CCC1C(=O)OC(=O)C1CC(=C[Si](C)(C)C)C[Si](C)(C)C, Cc1cc(C)c(N2CCN(c3c(C)cc(C)cc3C)[C]2=[Ru]([Cl])([Cl])(=[CH]c2ccccc2)[P](C2CCCCC2)(C2CCCCC2)C2CCCCC2)c(C)c1`"
974
+ ],
975
+ "text/plain": [
976
+ "<IPython.core.display.Markdown object>"
977
+ ]
978
+ },
979
+ "metadata": {},
980
+ "output_type": "display_data"
981
+ },
982
+ {
983
+ "data": {
984
+ "text/markdown": [
985
+ "#### Step 1\n",
986
+ "**Product:** `C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1` \n",
987
+ "**Reactants:** `C=CCC1C(=O)OC(=O)C1CC(=C[Si](C)(C)C)C[Si](C)(C)C.Cc1cc(C)c(N2CCN(c3c(C)cc(C)cc3C)[C]2=[Ru]([Cl])([Cl])(=[CH]c2ccccc2)[P](C2CCCCC2)(C2CCCCC2)C2CCCCC2)c(C)c1.ClCCl` \n",
988
+ "- Forward likelihood: `0.807` \n",
989
+ "- Arc score: `0.339` \n",
990
+ "- Retro confidence: `0.9455194125006082`"
991
+ ],
992
+ "text/plain": [
993
+ "<IPython.core.display.Markdown object>"
994
+ ]
995
+ },
996
+ "metadata": {},
997
+ "output_type": "display_data"
998
+ },
999
+ {
1000
+ "data": {
1001
+ "text/markdown": [
1002
+ "## Route 4\n",
1003
+ "- **Score:** `0.249`\n",
1004
+ "- **Steps:** `3`"
1005
+ ],
1006
+ "text/plain": [
1007
+ "<IPython.core.display.Markdown object>"
1008
+ ]
1009
+ },
1010
+ "metadata": {},
1011
+ "output_type": "display_data"
1012
+ },
1013
+ {
1014
+ "data": {
1015
+ "text/markdown": [
1016
+ "**Remaining molecules:** `C1CCOC1, O=C1C=CC(=O)O1, C=CC(=C)O, ClP(Cl)(Cl)(Cl)Cl, Cc1ccccc1, C[Si](C)(C)[CH2][Mg][Cl]`"
1017
+ ],
1018
+ "text/plain": [
1019
+ "<IPython.core.display.Markdown object>"
1020
+ ]
1021
+ },
1022
+ "metadata": {},
1023
+ "output_type": "display_data"
1024
+ },
1025
+ {
1026
+ "data": {
1027
+ "text/markdown": [
1028
+ "#### Step 1\n",
1029
+ "**Product:** `C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1` \n",
1030
+ "**Reactants:** `C=CC(=C)C[Si](C)(C)C.Cc1ccccc1.O=C1C=CC(=O)O1` \n",
1031
+ "- Forward likelihood: `0.393` \n",
1032
+ "- Arc score: `0.437` \n",
1033
+ "- Retro confidence: `0.9271861522313083`"
1034
+ ],
1035
+ "text/plain": [
1036
+ "<IPython.core.display.Markdown object>"
1037
+ ]
1038
+ },
1039
+ "metadata": {},
1040
+ "output_type": "display_data"
1041
+ },
1042
+ {
1043
+ "data": {
1044
+ "text/markdown": [
1045
+ "#### Step 2\n",
1046
+ "**Product:** `C=CC(=C)C[Si](C)(C)C` \n",
1047
+ "**Reactants:** `C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl]` \n",
1048
+ "- Forward likelihood: `0.940` \n",
1049
+ "- Arc score: `1.050` \n",
1050
+ "- Retro confidence: `0.8633497322540727`"
1051
+ ],
1052
+ "text/plain": [
1053
+ "<IPython.core.display.Markdown object>"
1054
+ ]
1055
+ },
1056
+ "metadata": {},
1057
+ "output_type": "display_data"
1058
+ },
1059
+ {
1060
+ "data": {
1061
+ "text/markdown": [
1062
+ "#### Step 3\n",
1063
+ "**Product:** `C=CC(=C)Cl` \n",
1064
+ "**Reactants:** `C=CC(=C)O.ClP(Cl)(Cl)(Cl)Cl` \n",
1065
+ "- Forward likelihood: `0.556` \n",
1066
+ "- Arc score: `0.542` \n",
1067
+ "- Retro confidence: `0.8283158501723433`"
1068
+ ],
1069
+ "text/plain": [
1070
+ "<IPython.core.display.Markdown object>"
1071
+ ]
1072
+ },
1073
+ "metadata": {},
1074
+ "output_type": "display_data"
1075
+ },
1076
+ {
1077
+ "data": {
1078
+ "text/markdown": [
1079
+ "## Route 5\n",
1080
+ "- **Score:** `0.242`\n",
1081
+ "- **Steps:** `3`"
1082
+ ],
1083
+ "text/plain": [
1084
+ "<IPython.core.display.Markdown object>"
1085
+ ]
1086
+ },
1087
+ "metadata": {},
1088
+ "output_type": "display_data"
1089
+ },
1090
+ {
1091
+ "data": {
1092
+ "text/markdown": [
1093
+ "**Remaining molecules:** `O=C1C=CC(=O)O1, [NH4+], C=CC(=C)O, ClP(Cl)(Cl)(Cl)Cl, [Cl-], Cc1ccccc1, C[Si](C)(C)[CH2][Mg][Cl], C1CCOC1, O`"
1094
+ ],
1095
+ "text/plain": [
1096
+ "<IPython.core.display.Markdown object>"
1097
+ ]
1098
+ },
1099
+ "metadata": {},
1100
+ "output_type": "display_data"
1101
+ },
1102
+ {
1103
+ "data": {
1104
+ "text/markdown": [
1105
+ "#### Step 1\n",
1106
+ "**Product:** `C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1` \n",
1107
+ "**Reactants:** `C=CC(=C)C[Si](C)(C)C.Cc1ccccc1.O=C1C=CC(=O)O1` \n",
1108
+ "- Forward likelihood: `0.393` \n",
1109
+ "- Arc score: `0.437` \n",
1110
+ "- Retro confidence: `0.9271861522313083`"
1111
+ ],
1112
+ "text/plain": [
1113
+ "<IPython.core.display.Markdown object>"
1114
+ ]
1115
+ },
1116
+ "metadata": {},
1117
+ "output_type": "display_data"
1118
+ },
1119
+ {
1120
+ "data": {
1121
+ "text/markdown": [
1122
+ "#### Step 2\n",
1123
+ "**Product:** `C=CC(=C)C[Si](C)(C)C` \n",
1124
+ "**Reactants:** `C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl].O.[Cl-].[NH4+]` \n",
1125
+ "- Forward likelihood: `0.917` \n",
1126
+ "- Arc score: `1.025` \n",
1127
+ "- Retro confidence: `0.8341045215463807`"
1128
+ ],
1129
+ "text/plain": [
1130
+ "<IPython.core.display.Markdown object>"
1131
+ ]
1132
+ },
1133
+ "metadata": {},
1134
+ "output_type": "display_data"
1135
+ },
1136
+ {
1137
+ "data": {
1138
+ "text/markdown": [
1139
+ "#### Step 3\n",
1140
+ "**Product:** `C=CC(=C)Cl` \n",
1141
+ "**Reactants:** `C=CC(=C)O.ClP(Cl)(Cl)(Cl)Cl` \n",
1142
+ "- Forward likelihood: `0.556` \n",
1143
+ "- Arc score: `0.542` \n",
1144
+ "- Retro confidence: `0.8283158501723433`"
1145
+ ],
1146
+ "text/plain": [
1147
+ "<IPython.core.display.Markdown object>"
1148
+ ]
1149
+ },
1150
+ "metadata": {},
1151
+ "output_type": "display_data"
1152
+ },
1153
+ {
1154
+ "data": {
1155
+ "text/markdown": [
1156
+ "## Route 6\n",
1157
+ "- **Score:** `0.233`\n",
1158
+ "- **Steps:** `3`"
1159
+ ],
1160
+ "text/plain": [
1161
+ "<IPython.core.display.Markdown object>"
1162
+ ]
1163
+ },
1164
+ "metadata": {},
1165
+ "output_type": "display_data"
1166
+ },
1167
+ {
1168
+ "data": {
1169
+ "text/markdown": [
1170
+ "**Remaining molecules:** `O=C1C=CC(=O)O1, C=CC(=C)O, CCOCC, ClP(Cl)(Cl)(Cl)Cl, Cc1ccccc1, C[Si](C)(C)[CH2][Mg][Cl], C1CCOC1, Cl, O`"
1171
+ ],
1172
+ "text/plain": [
1173
+ "<IPython.core.display.Markdown object>"
1174
+ ]
1175
+ },
1176
+ "metadata": {},
1177
+ "output_type": "display_data"
1178
+ },
1179
+ {
1180
+ "data": {
1181
+ "text/markdown": [
1182
+ "#### Step 1\n",
1183
+ "**Product:** `C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1` \n",
1184
+ "**Reactants:** `C=CC(=C)C[Si](C)(C)C.Cc1ccccc1.O=C1C=CC(=O)O1` \n",
1185
+ "- Forward likelihood: `0.393` \n",
1186
+ "- Arc score: `0.437` \n",
1187
+ "- Retro confidence: `0.9271861522313083`"
1188
+ ],
1189
+ "text/plain": [
1190
+ "<IPython.core.display.Markdown object>"
1191
+ ]
1192
+ },
1193
+ "metadata": {},
1194
+ "output_type": "display_data"
1195
+ },
1196
+ {
1197
+ "data": {
1198
+ "text/markdown": [
1199
+ "#### Step 2\n",
1200
+ "**Product:** `C=CC(=C)C[Si](C)(C)C` \n",
1201
+ "**Reactants:** `C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl].Cl.O` \n",
1202
+ "- Forward likelihood: `0.882` \n",
1203
+ "- Arc score: `0.986` \n",
1204
+ "- Retro confidence: `0.8303334989016697`"
1205
+ ],
1206
+ "text/plain": [
1207
+ "<IPython.core.display.Markdown object>"
1208
+ ]
1209
+ },
1210
+ "metadata": {},
1211
+ "output_type": "display_data"
1212
+ },
1213
+ {
1214
+ "data": {
1215
+ "text/markdown": [
1216
+ "#### Step 3\n",
1217
+ "**Product:** `C=CC(=C)Cl` \n",
1218
+ "**Reactants:** `C=CC(=C)O.ClP(Cl)(Cl)(Cl)Cl` \n",
1219
+ "- Forward likelihood: `0.556` \n",
1220
+ "- Arc score: `0.542` \n",
1221
+ "- Retro confidence: `0.8283158501723433`"
1222
+ ],
1223
+ "text/plain": [
1224
+ "<IPython.core.display.Markdown object>"
1225
+ ]
1226
+ },
1227
+ "metadata": {},
1228
+ "output_type": "display_data"
1229
+ },
1230
+ {
1231
+ "data": {
1232
+ "text/markdown": [
1233
+ "## Route 7\n",
1234
+ "- **Score:** `0.218`\n",
1235
+ "- **Steps:** `3`"
1236
+ ],
1237
+ "text/plain": [
1238
+ "<IPython.core.display.Markdown object>"
1239
+ ]
1240
+ },
1241
+ "metadata": {},
1242
+ "output_type": "display_data"
1243
+ },
1244
+ {
1245
+ "data": {
1246
+ "text/markdown": [
1247
+ "**Remaining molecules:** `O=C1C=CC(=O)O1, [NH4+], C=CC(=C)O, CCOCC, [Cl-], ClP(Cl)(Cl)(Cl)Cl, Cc1ccccc1, C[Si](C)(C)[CH2][Mg][Cl], C1CCOC1, O, [Mg]`"
1248
+ ],
1249
+ "text/plain": [
1250
+ "<IPython.core.display.Markdown object>"
1251
+ ]
1252
+ },
1253
+ "metadata": {},
1254
+ "output_type": "display_data"
1255
+ },
1256
+ {
1257
+ "data": {
1258
+ "text/markdown": [
1259
+ "#### Step 1\n",
1260
+ "**Product:** `C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1` \n",
1261
+ "**Reactants:** `C=CC(=C)C[Si](C)(C)C.Cc1ccccc1.O=C1C=CC(=O)O1` \n",
1262
+ "- Forward likelihood: `0.393` \n",
1263
+ "- Arc score: `0.437` \n",
1264
+ "- Retro confidence: `0.9271861522313083`"
1265
+ ],
1266
+ "text/plain": [
1267
+ "<IPython.core.display.Markdown object>"
1268
+ ]
1269
+ },
1270
+ "metadata": {},
1271
+ "output_type": "display_data"
1272
+ },
1273
+ {
1274
+ "data": {
1275
+ "text/markdown": [
1276
+ "#### Step 2\n",
1277
+ "**Product:** `C=CC(=C)C[Si](C)(C)C` \n",
1278
+ "**Reactants:** `C1CCOC1.C=CC(=C)Cl.CCOCC.C[Si](C)(C)[CH2][Mg][Cl].O.[Cl-].[Mg].[NH4+]` \n",
1279
+ "- Forward likelihood: `0.862` \n",
1280
+ "- Arc score: `0.921` \n",
1281
+ "- Retro confidence: `0.8301065237221663`"
1282
+ ],
1283
+ "text/plain": [
1284
+ "<IPython.core.display.Markdown object>"
1285
+ ]
1286
+ },
1287
+ "metadata": {},
1288
+ "output_type": "display_data"
1289
+ },
1290
+ {
1291
+ "data": {
1292
+ "text/markdown": [
1293
+ "#### Step 3\n",
1294
+ "**Product:** `C=CC(=C)Cl` \n",
1295
+ "**Reactants:** `C=CC(=C)O.ClP(Cl)(Cl)(Cl)Cl` \n",
1296
+ "- Forward likelihood: `0.556` \n",
1297
+ "- Arc score: `0.542` \n",
1298
+ "- Retro confidence: `0.8283158501723433`"
1299
+ ],
1300
+ "text/plain": [
1301
+ "<IPython.core.display.Markdown object>"
1302
+ ]
1303
+ },
1304
+ "metadata": {},
1305
+ "output_type": "display_data"
1306
+ },
1307
+ {
1308
+ "data": {
1309
+ "text/markdown": [
1310
+ "## Route 8\n",
1311
+ "- **Score:** `0.180`\n",
1312
+ "- **Steps:** `3`"
1313
+ ],
1314
+ "text/plain": [
1315
+ "<IPython.core.display.Markdown object>"
1316
+ ]
1317
+ },
1318
+ "metadata": {},
1319
+ "output_type": "display_data"
1320
+ },
1321
+ {
1322
+ "data": {
1323
+ "text/markdown": [
1324
+ "**Remaining molecules:** `C1CCOC1, O=C1C=CC(=O)O1, C=CC(=C)O, O=S(Cl)Cl, Cc1ccccc1, C[Si](C)(C)[CH2][Mg][Cl]`"
1325
+ ],
1326
+ "text/plain": [
1327
+ "<IPython.core.display.Markdown object>"
1328
+ ]
1329
+ },
1330
+ "metadata": {},
1331
+ "output_type": "display_data"
1332
+ },
1333
+ {
1334
+ "data": {
1335
+ "text/markdown": [
1336
+ "#### Step 1\n",
1337
+ "**Product:** `C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1` \n",
1338
+ "**Reactants:** `C=CC(=C)C[Si](C)(C)C.Cc1ccccc1.O=C1C=CC(=O)O1` \n",
1339
+ "- Forward likelihood: `0.393` \n",
1340
+ "- Arc score: `0.437` \n",
1341
+ "- Retro confidence: `0.9271861522313083`"
1342
+ ],
1343
+ "text/plain": [
1344
+ "<IPython.core.display.Markdown object>"
1345
+ ]
1346
+ },
1347
+ "metadata": {},
1348
+ "output_type": "display_data"
1349
+ },
1350
+ {
1351
+ "data": {
1352
+ "text/markdown": [
1353
+ "#### Step 2\n",
1354
+ "**Product:** `C=CC(=C)C[Si](C)(C)C` \n",
1355
+ "**Reactants:** `C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl]` \n",
1356
+ "- Forward likelihood: `0.940` \n",
1357
+ "- Arc score: `1.050` \n",
1358
+ "- Retro confidence: `0.8633497322540727`"
1359
+ ],
1360
+ "text/plain": [
1361
+ "<IPython.core.display.Markdown object>"
1362
+ ]
1363
+ },
1364
+ "metadata": {},
1365
+ "output_type": "display_data"
1366
+ },
1367
+ {
1368
+ "data": {
1369
+ "text/markdown": [
1370
+ "#### Step 3\n",
1371
+ "**Product:** `C=CC(=C)Cl` \n",
1372
+ "**Reactants:** `C=CC(=C)O.O=S(Cl)Cl` \n",
1373
+ "- Forward likelihood: `0.404` \n",
1374
+ "- Arc score: `0.393` \n",
1375
+ "- Retro confidence: `0.8269444351225532`"
1376
+ ],
1377
+ "text/plain": [
1378
+ "<IPython.core.display.Markdown object>"
1379
+ ]
1380
+ },
1381
+ "metadata": {},
1382
+ "output_type": "display_data"
1383
+ },
1384
+ {
1385
+ "data": {
1386
+ "text/markdown": [
1387
+ "## Route 9\n",
1388
+ "- **Score:** `0.180`\n",
1389
+ "- **Steps:** `1`"
1390
+ ],
1391
+ "text/plain": [
1392
+ "<IPython.core.display.Markdown object>"
1393
+ ]
1394
+ },
1395
+ "metadata": {},
1396
+ "output_type": "display_data"
1397
+ },
1398
+ {
1399
+ "data": {
1400
+ "text/markdown": [
1401
+ "**Remaining molecules:** `O=C1OC(=O)C2CC=CCC12, Cc1cc(C)c(N2CCN(c3c(C)cc(C)cc3C)[C]2=[Ru]([Cl])([Cl])=[CH]c2ccccc2)c(C)c1, ClCCl, C=CC(=C)C[Si](C)(C)C, C1CCC(P(C2CCCCC2)C2CCCCC2)CC1, N#N`"
1402
+ ],
1403
+ "text/plain": [
1404
+ "<IPython.core.display.Markdown object>"
1405
+ ]
1406
+ },
1407
+ "metadata": {},
1408
+ "output_type": "display_data"
1409
+ },
1410
+ {
1411
+ "data": {
1412
+ "text/markdown": [
1413
+ "#### Step 1\n",
1414
+ "**Product:** `C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1` \n",
1415
+ "**Reactants:** `C1CCC(P(C2CCCCC2)C2CCCCC2)CC1.C=CC(=C)C[Si](C)(C)C.Cc1cc(C)c(N2CCN(c3c(C)cc(C)cc3C)[C]2=[Ru]([Cl])([Cl])=[CH]c2ccccc2)c(C)c1.ClCCl.N#N.O=C1OC(=O)C2CC=CCC12` \n",
1416
+ "- Forward likelihood: `0.395` \n",
1417
+ "- Arc score: `0.180` \n",
1418
+ "- Retro confidence: `0.9290369156214755`"
1419
+ ],
1420
+ "text/plain": [
1421
+ "<IPython.core.display.Markdown object>"
1422
+ ]
1423
+ },
1424
+ "metadata": {},
1425
+ "output_type": "display_data"
1426
+ },
1427
+ {
1428
+ "data": {
1429
+ "text/markdown": [
1430
+ "## Route 10\n",
1431
+ "- **Score:** `0.176`\n",
1432
+ "- **Steps:** `3`"
1433
+ ],
1434
+ "text/plain": [
1435
+ "<IPython.core.display.Markdown object>"
1436
+ ]
1437
+ },
1438
+ "metadata": {},
1439
+ "output_type": "display_data"
1440
+ },
1441
+ {
1442
+ "data": {
1443
+ "text/markdown": [
1444
+ "**Remaining molecules:** `O=C1C=CC(=O)O1, [NH4+], C=CC(=C)O, [Cl-], O=S(Cl)Cl, Cc1ccccc1, C[Si](C)(C)[CH2][Mg][Cl], C1CCOC1, O`"
1445
+ ],
1446
+ "text/plain": [
1447
+ "<IPython.core.display.Markdown object>"
1448
+ ]
1449
+ },
1450
+ "metadata": {},
1451
+ "output_type": "display_data"
1452
+ },
1453
+ {
1454
+ "data": {
1455
+ "text/markdown": [
1456
+ "#### Step 1\n",
1457
+ "**Product:** `C[Si](C)(C)CC1=CCC2C(=O)OC(=O)C2C1` \n",
1458
+ "**Reactants:** `C=CC(=C)C[Si](C)(C)C.Cc1ccccc1.O=C1C=CC(=O)O1` \n",
1459
+ "- Forward likelihood: `0.393` \n",
1460
+ "- Arc score: `0.437` \n",
1461
+ "- Retro confidence: `0.9271861522313083`"
1462
+ ],
1463
+ "text/plain": [
1464
+ "<IPython.core.display.Markdown object>"
1465
+ ]
1466
+ },
1467
+ "metadata": {},
1468
+ "output_type": "display_data"
1469
+ },
1470
+ {
1471
+ "data": {
1472
+ "text/markdown": [
1473
+ "#### Step 2\n",
1474
+ "**Product:** `C=CC(=C)C[Si](C)(C)C` \n",
1475
+ "**Reactants:** `C1CCOC1.C=CC(=C)Cl.C[Si](C)(C)[CH2][Mg][Cl].O.[Cl-].[NH4+]` \n",
1476
+ "- Forward likelihood: `0.917` \n",
1477
+ "- Arc score: `1.025` \n",
1478
+ "- Retro confidence: `0.8341045215463807`"
1479
+ ],
1480
+ "text/plain": [
1481
+ "<IPython.core.display.Markdown object>"
1482
+ ]
1483
+ },
1484
+ "metadata": {},
1485
+ "output_type": "display_data"
1486
+ },
1487
+ {
1488
+ "data": {
1489
+ "text/markdown": [
1490
+ "#### Step 3\n",
1491
+ "**Product:** `C=CC(=C)Cl` \n",
1492
+ "**Reactants:** `C=CC(=C)O.O=S(Cl)Cl` \n",
1493
+ "- Forward likelihood: `0.404` \n",
1494
+ "- Arc score: `0.393` \n",
1495
+ "- Retro confidence: `0.8269444351225532`"
1496
+ ],
1497
+ "text/plain": [
1498
+ "<IPython.core.display.Markdown object>"
1499
+ ]
1500
+ },
1501
+ "metadata": {},
1502
+ "output_type": "display_data"
1503
+ }
1504
+ ],
1505
+ "source": [
1506
+ "# Textual retrosynthesis tree representation\n",
1507
+ "for i, route in enumerate(response[\"result\"]):\n",
1508
+ " display_route(route, i)"
1509
+ ]
1510
+ },
1511
+ {
1512
+ "cell_type": "code",
1513
+ "execution_count": 13,
1514
+ "id": "8491bcb4-b096-4277-9244-8b59b4499fe2",
1515
+ "metadata": {},
1516
+ "outputs": [
1517
+ {
1518
+ "data": {
1519
+ "application/vnd.jupyter.widget-view+json": {
1520
+ "model_id": "37b74ae49d7748b7acbc0e82ad6b285d",
1521
+ "version_major": 2,
1522
+ "version_minor": 0
1523
+ },
1524
+ "text/plain": [
1525
+ "interactive(children=(Dropdown(description='Route', options=(('Route 1 | score=0.706', 0), ('Route 2 | score=0…"
1526
+ ]
1527
+ },
1528
+ "metadata": {},
1529
+ "output_type": "display_data"
1530
+ }
1531
+ ],
1532
+ "source": [
1533
+ "# Graphical retrosynthesis tree representation with route selection\n",
1534
+ "tree_route_selector(response[\"result\"])"
1535
+ ]
1536
+ },
1537
+ {
1538
+ "cell_type": "code",
1539
+ "execution_count": null,
1540
+ "id": "5f7af0df-b7a9-4e93-ac40-27fc33d0a103",
1541
+ "metadata": {},
1542
+ "outputs": [],
1543
+ "source": []
1544
+ }
1545
+ ],
1546
+ "metadata": {
1547
+ "kernelspec": {
1548
+ "display_name": "Python 3 (ipykernel)",
1549
+ "language": "python",
1550
+ "name": "python3"
1551
+ },
1552
+ "language_info": {
1553
+ "codemirror_mode": {
1554
+ "name": "ipython",
1555
+ "version": 3
1556
+ },
1557
+ "file_extension": ".py",
1558
+ "mimetype": "text/x-python",
1559
+ "name": "python",
1560
+ "nbconvert_exporter": "python",
1561
+ "pygments_lexer": "ipython3",
1562
+ "version": "3.13.12"
1563
+ }
1564
+ },
1565
+ "nbformat": 4,
1566
+ "nbformat_minor": 5
1567
+ }
jupyter/requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ celery[redis]
2
+ ipykernel
3
+ ipywidgets
4
+ matplotlib
5
+ notebook
6
+ pandas
7
+ rdkit
mcp/Dockerfile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM public.ecr.aws/docker/library/python:3.13-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY server.py start.sh .
9
+ RUN chmod +x start.sh
10
+
11
+ CMD ["./start.sh"]
mcp/requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ celery
2
+ mcp
3
+ mcpo
4
+ redis
mcp/server.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ from typing import Optional
4
+ from mcp.server.fastmcp import FastMCP
5
+ from celery import Celery
6
+ from celery.exceptions import TimeoutError
7
+
8
+
9
+ CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "amqp://ubuntu:ubuntu@broker:5672//")
10
+ CELERY_RESULT_BACKEND = os.getenv("CELERY_RESULT_BACKEND", "redis://:ubuntu@redis:6379/0")
11
+
12
+ app = Celery("mcp-client", broker=CELERY_BROKER_URL, backend=CELERY_RESULT_BACKEND)
13
+
14
+ mcp = FastMCP("rxn-mcp-server", json_response=False)
15
+
16
+
17
+ async def wait_for_celery_result(task, timeout: int = 300):
18
+ def _get():
19
+ return task.get(timeout=timeout)
20
+
21
+ return await asyncio.to_thread(_get)
22
+
23
+
24
+ @mcp.tool()
25
+ async def product_prediction(
26
+ reactants_list: list[str],
27
+ topn: int = 3,
28
+ num_beams: int = 3,
29
+ device: Optional[str] = None,
30
+ ckpt_forward = "Pistachio2025Q2-Forward",
31
+ vocab = "Pistachio2025Q2",
32
+ ):
33
+ """
34
+ Gets the product prediction for a batch of reactants SMILES.
35
+
36
+ Args:
37
+ reactants_list (list[str]): List of reactants SMILES.
38
+ topn (int): number of predictions per reactant (Defaults to 3).
39
+ num_beams (int): beams used for prediction (num_beams >= topn) (Defaults to 3).
40
+ device (Optional str): device used for predicting, either "cuda" or "cpu", None defaults to cuda if available.
41
+ ckpt_forward (Optional str): name of the forward model without extension. Should exist inside the "models/forward" directory and be a .ckpt file.
42
+ vocab (Optional str): name of the vocab file without extension. Should exist inside the "vocab" directory and be a .txt file.
43
+ """
44
+ try:
45
+ kwargs = {
46
+ "topn": topn,
47
+ "num_beams": num_beams,
48
+ "device": device,
49
+ "ckpt_forward": ckpt_forward,
50
+ "vocab": vocab,
51
+ }
52
+ task = app.send_task(
53
+ "tasks.product_prediction",
54
+ [reactants_list],
55
+ kwargs=kwargs,
56
+ queue="product_prediction",
57
+ )
58
+ result = await wait_for_celery_result(task, timeout=600)
59
+ result["status"] = "success"
60
+ return result
61
+ except TimeoutError:
62
+ return {
63
+ "status": "timeout",
64
+ "message": "Computation exceeded time limit",
65
+ }
66
+ except Exception as e:
67
+ return {
68
+ "status": "error",
69
+ "message": str(e),
70
+ }
71
+
72
+
73
+ @mcp.tool()
74
+ async def retro_prediction(
75
+ product: str,
76
+ topn: int = 15,
77
+ num_beams: int = 15,
78
+ fap: float = 0.6,
79
+ fld: float = 0.2,
80
+ device: Optional[str] = None,
81
+ ckpt_forward = "Pistachio2025Q2-Forward",
82
+ ckpt_retro = "Pistachio2025Q2-Retro",
83
+ vocab = "Pistachio2025Q2",
84
+ ):
85
+ """
86
+ Gets the retrosynthesis prediction for a product SMILES.
87
+
88
+ Args:
89
+ product (str): List of reactants SMILES.
90
+ topn (int): number of retrosynthesis predictions (Defaults to 15).
91
+ num_beams (int): beams used for prediction (num_beams >= topn) (Defaults to 15).
92
+ fap (float): forward acceptance level (Defaults to 0.6).
93
+ fld (float): forward likelihood delta (Defaults to 0.2).
94
+ device (Optional str): device used for predicting, either "cuda" or "cpu", None defaults to cuda if available.
95
+ ckpt_forward (Optional str): name of the forward model without extension. Should exist inside the "models/forward" directory and be a .ckpt file.
96
+ ckpt_retro (Optional str): name of the retrosynthesis model without extension. Should exist inside the "models/retrosynthesis" directory and be a .ckpt file.
97
+ vocab (Optional str): name of the vocab file without extension. Should exist inside the "vocab" directory and be a .txt file.
98
+ """
99
+ try:
100
+ kwargs = {
101
+ "topn": topn,
102
+ "num_beams": num_beams,
103
+ "fap": fap,
104
+ "fld": fld,
105
+ "device": device,
106
+ "ckpt_forward": ckpt_forward,
107
+ "ckpt_retro": ckpt_retro,
108
+ "vocab": vocab,
109
+ }
110
+ task = app.send_task(
111
+ "tasks.retro_prediction",
112
+ [product],
113
+ kwargs=kwargs,
114
+ queue="retro_prediction",
115
+ )
116
+ result = await wait_for_celery_result(task, timeout=600)
117
+ result["status"] = "success"
118
+ return result
119
+ except TimeoutError:
120
+ return {
121
+ "status": "timeout",
122
+ "message": "Computation exceeded time limit",
123
+ }
124
+ except Exception as e:
125
+ return {
126
+ "status": "error",
127
+ "message": str(e),
128
+ }
129
+
130
+
131
+ @mcp.tool()
132
+ async def retro_tree_prediction(
133
+ product: str,
134
+ topn: int = 15,
135
+ num_beams: int = 15,
136
+ fap: float = 0.6,
137
+ fld: float = 0.2,
138
+ max_depth: int = 3,
139
+ beam_width: int = 5,
140
+ device: Optional[str] = None,
141
+ ckpt_forward = "Pistachio2025Q2-Forward",
142
+ ckpt_retro = "Pistachio2025Q2-Retro",
143
+ vocab = "Pistachio2025Q2",
144
+ ):
145
+ """
146
+ Gets the full retrosynthesis tree for a product SMILES.
147
+
148
+ Args:
149
+ product (str): List of reactants SMILES.
150
+ topn (int): number of retrosynthesis predictions (Defaults to 15).
151
+ num_beams (int): beams used for prediction (num_beams >= topn) (Defaults to 15).
152
+ fap (float): forward acceptance level (Defaults to 0.6).
153
+ fld (float): forward likelihood delta (Defaults to 0.2).
154
+ max_depth (int): maximum number of retrosynthesis expansions in the tree (Defaults to 3).
155
+ beam_width (int): number of molecules chosen for expansion in each step of the tree (Defaults to 5).
156
+ device (Optional str): device used for predicting, either "cuda" or "cpu", None defaults to cuda if available.
157
+ ckpt_forward (Optional str): name of the forward model without extension. Should exist inside the "models/forward" directory and be a .ckpt file.
158
+ ckpt_retro (Optional str): name of the retrosynthesis model without extension. Should exist inside the "models/retrosynthesis" directory and be a .ckpt file.
159
+ vocab (Optional str): name of the vocab file without extension. Should exist inside the "vocab" directory and be a .txt file.
160
+ """
161
+ try:
162
+ kwargs = {
163
+ "topn": topn,
164
+ "num_beams": num_beams,
165
+ "fap": fap,
166
+ "fld": fld,
167
+ "max_depth": max_depth,
168
+ "beam_width": beam_width,
169
+ "device": device,
170
+ "ckpt_forward": ckpt_forward,
171
+ "ckpt_retro": ckpt_retro,
172
+ "vocab": vocab,
173
+ }
174
+ task = app.send_task(
175
+ "tasks.retro_prediction_tree",
176
+ [product],
177
+ kwargs=kwargs,
178
+ queue="retro_prediction",
179
+ )
180
+ result = await wait_for_celery_result(task, 1500)
181
+ result["status"] = "success"
182
+ return result
183
+ except TimeoutError:
184
+ return {
185
+ "status": "timeout",
186
+ "message": "Computation exceeded time limit",
187
+ }
188
+ except Exception as e:
189
+ return {
190
+ "status": "error",
191
+ "message": str(e),
192
+ }
193
+
194
+
195
+ if __name__ == "__main__":
196
+ mcp.settings.host = "0.0.0.0"
197
+ mcp.settings.port = 8001
198
+ asyncio.run(mcp.run_streamable_http_async())
mcp/start.sh ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ echo "Starting FastMCP on port 8001"
5
+ python server.py &
6
+
7
+ echo "Starting MCPO on port 8000"
8
+ exec mcpo \
9
+ --host 0.0.0.0 \
10
+ --port 8000 \
11
+ --server-type streamable_http \
12
+ -- http://127.0.0.1:8001/mcp
model-training/README.md ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model Training
2
+
3
+ This directory contains everything needed to retrain the forward prediction and retrosynthesis models.
4
+
5
+ - **`pistachio/`** — Scripts to download and preprocess Pistachio reaction data into the JSONL format required for training. See [`pistachio/README.md`](pistachio/README.md) for the full data pipeline.
6
+ - **`training/`** — The LightningCLI entry point (`cli_main.py`) and YAML configuration files for training, testing, and running predictions.
7
+ - **`tools/`** — Standalone utility scripts for data analysis, model diagnostics, and running training/testing/prediction directly without LightningCLI.
8
+
9
+ The model source code lives in [`worker/transformers_model/`](../worker/transformers_model/). The key files are:
10
+ - [`model.py`](../worker/transformers_model/model.py) — `LitVanillaTransformer`, the Lightning module that wraps the model.
11
+ - [`smiles_datamodule.py`](../worker/transformers_model/smiles_datamodule.py) — `LitSmilesDataset`, the Lightning data module.
12
+ - [`configuration.py`](../worker/transformers_model/configuration.py) — `VanillaTransformerConfig`, the model's default hyperparameter configuration.
13
+
14
+ ---
15
+
16
+ ## Setup
17
+
18
+ ### Copy the model package
19
+
20
+ `cli_main.py` imports `transformers_model` as a local package. Python has no way to resolve this import across the repository boundary to `worker/transformers_model/` without installing it, so the folder must be copied into `model-training/` before running any command:
21
+
22
+ ```bash
23
+ cp -r ../worker/transformers_model model-training/transformers_model
24
+ ```
25
+
26
+ This only needs to be done once, or again whenever the model source in `worker/` changes.
27
+
28
+ ### Install dependencies
29
+
30
+ #### With uv (recommended)
31
+
32
+ ```bash
33
+ # Install uv if not already available
34
+ curl -LsSf https://astral.sh/uv/install.sh | sh
35
+
36
+ # Create and activate a virtual environment
37
+ uv venv --python 3.11
38
+ source .venv/bin/activate
39
+
40
+ # Install dependencies
41
+ uv pip install -r requirements.txt
42
+ ```
43
+
44
+ #### Without uv
45
+
46
+ ```bash
47
+ python3.11 -m venv .venv
48
+ source .venv/bin/activate
49
+ pip install -r requirements.txt
50
+ ```
51
+
52
+ > On Windows replace `source .venv/bin/activate` with `.venv\Scripts\activate`.
53
+
54
+ ---
55
+
56
+ ## Running LightningCLI commands
57
+
58
+ All commands are run from the `model-training/` directory using `cli_main.py` together with one of the provided YAML configuration files.
59
+
60
+ ### Training
61
+
62
+ ```bash
63
+ # Forward model — with uv
64
+ uv run python training/cli_main.py fit --config=training/train.yaml
65
+
66
+ # Forward model — without uv (venv already activated)
67
+ python training/cli_main.py fit --config=training/train.yaml
68
+
69
+ # Retrosynthesis model
70
+ python training/cli_main.py fit --config=training/train_retro.yaml
71
+ ```
72
+
73
+ ### Testing
74
+
75
+ ```bash
76
+ # Forward model
77
+ python training/cli_main.py test --config=training/test.yaml
78
+
79
+ # Retrosynthesis model
80
+ python training/cli_main.py test --config=training/test_retro.yaml
81
+ ```
82
+
83
+ ### Prediction
84
+
85
+ ```bash
86
+ python training/cli_main.py predict --config=training/predict.yaml
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Configuration
92
+
93
+ Default values are defined in [`VanillaTransformerConfig`](../worker/transformers_model/configuration.py). Additional defaults are set directly in [`LitVanillaTransformer`](../worker/transformers_model/model.py) and [`LitSmilesDataset`](../worker/transformers_model/smiles_datamodule.py). Note that some defaults from `VanillaTransformerConfig` are overridden inside `LitVanillaTransformer`, so always verify which value is actually used at runtime.
94
+
95
+ Lightning saves the configuration used for each run to `hparams.yaml` inside the corresponding `lightning_logs/<version>/` folder.
96
+
97
+ ### Passing options inline
98
+
99
+ Any option from a YAML file can also be passed directly on the command line:
100
+
101
+ **Model flags** — forwarded to `LitVanillaTransformer.__init__`:
102
+ ```
103
+ --model.vocab_path=vocab/vocab.txt
104
+ --model.learning_rate=0.0002
105
+ --model.task=forward
106
+ ```
107
+
108
+ **Data flags** — forwarded to `LitSmilesDataset.__init__`:
109
+ ```
110
+ --data.vocab_path=vocab/vocab.txt
111
+ --data.train_path=data/pistachio/data.forward/train.jsonl
112
+ --data.validation_path=data/pistachio/data.forward/validation.jsonl
113
+ --data.num_dataloader_workers=16
114
+ --data.batch_size=256
115
+ ```
116
+
117
+ **Trainer flags** — forwarded to the Lightning `Trainer`:
118
+ ```
119
+ --trainer.max_epochs=10
120
+ --trainer.accelerator=gpu
121
+ --trainer.devices=1
122
+ --trainer.precision=32
123
+ --trainer.enable_progress_bar=false
124
+ ```
125
+
126
+ **Global flags**:
127
+ ```
128
+ --seed_everything=42
129
+ --config=training/train.yaml
130
+ ```
131
+
132
+ ### Example YAML with callbacks
133
+
134
+ More complex options, such as callbacks, are easier to specify via a YAML file:
135
+
136
+ ```yaml
137
+ model:
138
+ vocab_path: vocab/vocab.txt
139
+ learning_rate: 0.0002
140
+ data:
141
+ vocab_path: vocab/vocab.txt
142
+ train_path: data/pistachio/data.forward/train.jsonl
143
+ validation_path: data/pistachio/data.forward/validation.jsonl
144
+ num_dataloader_workers: 16
145
+ batch_size: 256
146
+ trainer:
147
+ max_epochs: 10
148
+ accelerator: gpu
149
+ enable_progress_bar: false
150
+ devices: 1
151
+ precision: 32
152
+ callbacks:
153
+ - class_path: lightning.pytorch.callbacks.ModelCheckpoint
154
+ init_args:
155
+ monitor: val_accuracy
156
+ mode: max
157
+ save_top_k: 3
158
+ filename: "{epoch:02d}-{val_accuracy:.4f}"
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Requirements
164
+
165
+ `requirements.txt` covers all dependencies for both model training and the Pistachio data pipeline:
166
+
167
+ | Package | Used by |
168
+ |---------|---------|
169
+ | `lightning` | LightningCLI, model and data modules |
170
+ | `pandas` | Pistachio data pipeline (steps 7 and 8) |
171
+ | `python-Levenshtein` | Edit-distance metrics during validation and testing |
172
+ | `rdkit` | SMILES validation and Tanimoto similarity during training |
173
+ | `rxn-chem-utils` | SMILES tokenization regex pattern |
174
+ | `torch` | Model training |
175
+ | `transformers` | Tokenizer, scheduler, model base classes |
176
+
177
+
178
+ ---
179
+
180
+ ## Improving training throughput
181
+
182
+ - **`num_dataloader_workers`** — Number of CPU workers for data loading. 16 has shown the best results in practice.
183
+ - **`batch_size`** — Samples per training step. Increasing from the default 32 to 256 (or higher) significantly reduces wall time per epoch.
184
+ - **`accumulate_grad_batches`** — Simulates a larger effective batch size without increasing GPU memory usage.
185
+ - **`precision`** — `32` is the stable default. `16-mixed` can speed up training but has shown instability (NaN loss values) in practice.
186
+ - **`profiler`** — Set `trainer.profiler: simple` to identify time bottlenecks across training steps.
model-training/pistachio/README.md ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Pistachio Data Pipeline
2
+
3
+ This directory contains a sequence of scripts that download and process [Pistachio](https://www.nextmovesoftware.com/pistachio.html) reaction data into the JSONL format expected by the forward and retrosynthesis model training pipelines.
4
+
5
+ ## Prerequisites
6
+
7
+ - **Pistachio license** — Access to the Pistachio release archives requires a license from [NextMove Software](https://www.nextmovesoftware.com/). Obtain your credentials before running step 1.
8
+ - **`rxn-reaction-preprocessing`** — Provides the `rxn-data-pipeline` CLI used in steps 4 and 6. Install it from [rxn4chemistry/rxn-reaction-preprocessing](https://github.com/rxn4chemistry/rxn-reaction-preprocessing/).
9
+ - **Python dependencies** — `pandas` is required for steps 7 and 8.
10
+
11
+ > **Working directory**: unless noted otherwise, all scripts assume they are run from the root of the repository, and data is written under `data/pistachio/`.
12
+
13
+ ---
14
+
15
+ ## Step 1 — Download the Pistachio archive
16
+
17
+ Edit `step1_download.sh` to set the desired release version and replace `<USER>` and `<PASSWORD>` with your NextMove Software credentials, then run:
18
+
19
+ ```bash
20
+ bash model-training/pistachio/step1_download.sh
21
+ ```
22
+
23
+ This downloads `pistachio.tar.gz` for the configured release into the current directory. The `-C -` flag enables resuming an interrupted download.
24
+
25
+ ---
26
+
27
+ ## Step 2 — Extract reaction SMILES
28
+
29
+ ```bash
30
+ python model-training/pistachio/step2_get_reactions.py
31
+ ```
32
+
33
+ Iterates over every record in `pistachio.tar.gz` and writes one reaction SMILES per line to `reactions.txt`.
34
+
35
+ The input and output paths can be overridden if needed:
36
+
37
+ ```bash
38
+ python model-training/pistachio/step2_get_reactions.py \
39
+ --input path/to/pistachio.tar.gz \
40
+ --output path/to/reactions.txt
41
+ ```
42
+
43
+ Both a `.tar.gz` archive and an already-extracted directory of JSON files are accepted as input.
44
+
45
+ ---
46
+
47
+ ## Step 3 — Remove extended SMILES annotations
48
+
49
+ ```bash
50
+ python model-training/pistachio/step3_clean_reactions.py
51
+ ```
52
+
53
+ Some reaction SMILES contain extended SMILES notation blocks such as `|&1:4,24,f:2.4|` or `|f:1.2|` that are not handled correctly by downstream tools. This script strips everything after the first `|` character on each line and writes the result to `reactions_cleaned.txt`.
54
+
55
+ ---
56
+
57
+ ## Step 4 — Standardize reactions (IMPORT + STANDARDIZE)
58
+
59
+ ```bash
60
+ rxn-data-pipeline \
61
+ --config-dir model-training/pistachio \
62
+ --config-name step4_standardize_reactions_config.yaml
63
+ ```
64
+
65
+ Runs the **IMPORT** and **STANDARDIZE** stages of the `rxn-data-pipeline` on `reactions_cleaned.txt`. After completion, the pipeline directory (`data/pistachio/pipeline/`) will contain:
66
+
67
+ | File | Description |
68
+ |------|-------------|
69
+ | `data.imported.csv` | Raw reactions imported into the pipeline CSV format |
70
+ | `data.standardized.csv` | Reactions after SMILES standardization |
71
+
72
+ > The full pipeline will fail at this point because a subset of standardized reactions contain coordinate-bond arrow annotations such as `->[Cu+2]<-`. Step 5 removes these before the pipeline is resumed.
73
+
74
+ ---
75
+
76
+ ## Step 5 — Remove arrow annotations
77
+
78
+ ```bash
79
+ bash model-training/pistachio/step5_remove_arrow_annotations.sh
80
+ ```
81
+
82
+ Filters `data.standardized.csv` to remove any rows whose reaction SMILES contain arrow-bond annotations (`->` or `<-`). The removed rows are saved to `arrow.forward.csv` and `arrow.backward.csv` for inspection. The cleaned output is written to `arrow.removed.csv`, which is the input for the next step.
83
+
84
+ ---
85
+
86
+ ## Step 6 — Preprocess and split reactions (PREPROCESS + SPLIT)
87
+
88
+ ```bash
89
+ rxn-data-pipeline \
90
+ --config-dir model-training/pistachio \
91
+ --config-name step6_preprocess_reactions_config.yaml
92
+ ```
93
+
94
+ Resumes the pipeline from `arrow.removed.csv`, running the **PREPROCESS** and **SPLIT** stages. After completion, the pipeline directory will contain:
95
+
96
+ | File | Description |
97
+ |------|-------------|
98
+ | `data.processed.train.csv` | Training split |
99
+ | `data.processed.test.csv` | Test split |
100
+ | `data.processed.validation.csv` | Validation split |
101
+
102
+ The split ratio is set to 5 % for test and validation (see `step6_preprocess_reactions_config.yaml`).
103
+
104
+ ---
105
+
106
+ ## Step 7 — Convert splits to JSONL
107
+
108
+ ```bash
109
+ python model-training/pistachio/step7_convert_to_jsonl.py
110
+ ```
111
+
112
+ Reads the three CSV splits and writes them as JSONL files in the format expected by the model trainer (`{"source": "...", "target": "..."}`). Two sets of output files are produced — one for each task direction:
113
+
114
+ | Directory | Task | source | target |
115
+ |-----------|------|--------|--------|
116
+ | `data/pistachio/data.forward/` | Forward prediction | reactants | products |
117
+ | `data/pistachio/data.retro/` | Retrosynthesis | products | reactants |
118
+
119
+ Each directory contains `train.jsonl`, `test.jsonl`, and `validation.jsonl`.
120
+
121
+ ---
122
+
123
+ ## Step 8 — Generate model vocabulary
124
+
125
+ The vocabulary can be built with either the forward or retro data, from its training and validation splits, since the data is the same with only the source and target being switched.
126
+
127
+ ```bash
128
+ python model-training/pistachio/step8_generate_vocab.py \
129
+ --data-dir data/pistachio/data.forward \
130
+ --output data/pistachio/vocab.txt
131
+ ```
132
+
133
+ The script tokenizes all SMILES strings in `train.jsonl` and `validation.jsonl`, counts every token (bracketed atoms, two-character elements such as `Cl`/`Br`, `%nn` ring-closure labels, and individual characters), and writes a `vocab.txt` file with reserved BERT tokens prepended and all remaining tokens ordered by frequency.
model-training/pistachio/step1_download.sh ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ BASE_URL=https://www.nextmovesoftware.com/downloads/pistachio/releases/
4
+ RELEASE=2025Q2
5
+ curl -L -C - --user <USER>:<PASSWORD> $BASE_URL/$RELEASE/data/pistachio.tar.gz -o pistachio.tar.gz
model-training/pistachio/step2_get_reactions.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Extract reaction SMILES from a Pistachio archive.
3
+
4
+ This script is self-contained: all logic that was previously provided by the
5
+ rxn_pistachio package has been inlined here. No external dependencies beyond
6
+ the Python standard library are required.
7
+
8
+ Usage:
9
+ python step2_get_reactions.py # defaults below
10
+ python step2_get_reactions.py --input my.tar.gz --output reactions.txt
11
+ """
12
+
13
+ import argparse
14
+ import json
15
+ import logging
16
+ import tarfile
17
+ from pathlib import Path
18
+ from typing import Any, Dict, Iterator, Union
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Pistachio archive navigation (inlined from rxn_pistachio)
22
+ # ---------------------------------------------------------------------------
23
+
24
+ def _is_json(path: Union[str, Path]) -> bool:
25
+ path_str = str(path)
26
+ return path_str.endswith(".json") or path_str.endswith(".JSON")
27
+
28
+
29
+ def _iter_reaction_dicts(archive_path: Path) -> Iterator[Dict[str, Any]]:
30
+ """
31
+ Iterate over every reaction JSON object inside a Pistachio .tar.gz file
32
+ or a directory tree of JSON files.
33
+ """
34
+ if archive_path.is_dir():
35
+ for json_file in (p for p in archive_path.rglob("*") if _is_json(p)):
36
+ print(f'Reading file "{json_file}"')
37
+ with open(json_file, "rt") as f:
38
+ for line in f:
39
+ line = line.strip()
40
+ if line:
41
+ yield json.loads(line)
42
+ else:
43
+ with tarfile.open(archive_path, "r:gz") as tar:
44
+ for member in tar.getmembers():
45
+ if not _is_json(member.name):
46
+ continue
47
+ extracted = tar.extractfile(member)
48
+ if extracted is None:
49
+ continue
50
+ for raw_line in extracted:
51
+ line = raw_line.strip()
52
+ if line:
53
+ yield json.loads(line)
54
+
55
+
56
+ def _reaction_smiles(record: Dict[str, Any]) -> str:
57
+ """
58
+ Return the reaction SMILES string from a Pistachio record dictionary.
59
+ Mirrors Record.reaction_smiles from rxn_pistachio.
60
+ """
61
+ data = record.get("data", {})
62
+ if "reactionSmiles" in data:
63
+ return data["reactionSmiles"]
64
+ return data.get("smiles", "")
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Main
69
+ # ---------------------------------------------------------------------------
70
+
71
+ def extract_reactions(input_path: str, output_path: str) -> None:
72
+ archive = Path(input_path)
73
+ if not archive.exists():
74
+ raise FileNotFoundError(f"Input not found: {archive}")
75
+
76
+ written = 0
77
+ skipped = 0
78
+
79
+ with open(output_path, "w") as out:
80
+ for record in _iter_reaction_dicts(archive):
81
+ smiles = _reaction_smiles(record)
82
+ if smiles:
83
+ out.write(smiles + "\n")
84
+ written += 1
85
+ else:
86
+ skipped += 1
87
+
88
+ print(f"Done. Written: {written}, skipped (no SMILES): {skipped}")
89
+ print(f"Output: {output_path}")
90
+
91
+
92
+ if __name__ == "__main__":
93
+ parser = argparse.ArgumentParser(
94
+ description="Extract reaction SMILES from a Pistachio archive."
95
+ )
96
+ parser.add_argument(
97
+ "--input",
98
+ default="pistachio.tar.gz",
99
+ help="Path to pistachio.tar.gz or extracted directory (default: pistachio.tar.gz)",
100
+ )
101
+ parser.add_argument(
102
+ "--output",
103
+ default="reactions.txt",
104
+ help="Output file path (default: reactions.txt)",
105
+ )
106
+ args = parser.parse_args()
107
+ extract_reactions(args.input, args.output)
model-training/pistachio/step3_clean_reactions.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ input_file = 'reactions.txt'
3
+ output_file = 'reactions_cleaned.txt'
4
+
5
+ with open(input_file, 'r') as infile, open(output_file, 'w') as outfile:
6
+ for line in infile:
7
+ cleaned_line = line.split('|')[0].strip()
8
+ outfile.write(cleaned_line + '\n')
9
+
10
+ print(f"Cleaned SMILES reactions saved to '{output_file}'")
model-training/pistachio/step4_standardize_reactions_config.yaml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ data:
2
+ path: data/pistachio/reactions_cleaned.txt
3
+ name: data
4
+ proc_dir: data/pistachio/pipeline
5
+ common:
6
+ sequence:
7
+ - IMPORT
8
+ - STANDARDIZE
9
+ fragment_bond: TILDE
10
+ reaction_column_name: rxn
11
+ keep_intermediate_columns: false
12
+ rxn_import:
13
+ input_file: data/pistachio/reactions_cleaned.txt
14
+ output_csv: data/pistachio/pipeline/data.imported.csv
15
+ data_format: TXT
16
+ input_csv_column_name: rxn
17
+ reaction_column_name: rxn
18
+ fragment_bond: TILDE
19
+ remove_atom_mapping: true
20
+ column_for_light: null
21
+ column_for_heat: null
22
+ keep_original_rxn_column: false
23
+ standardize:
24
+ input_file_path: data/pistachio/pipeline/data.imported.csv
25
+ annotation_file_paths: []
26
+ discard_unannotated_metals: false
27
+ output_file_path: data/pistachio/pipeline/data.standardized.csv
28
+ fragment_bond: TILDE
29
+ reaction_column_name: rxn
30
+ remove_stereo_if_not_defined_in_precursors: false
31
+ keep_intermediate_columns: false
model-training/pistachio/step5_remove_arrow_annotations.sh ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ BASE_PATH=data/pistachio/pipeline
4
+ FILEPATH=$BASE_PATH/data.standardized.csv
5
+
6
+ # Extract the header
7
+ head -n 1 $FILEPATH > $BASE_PATH/arrow.backward.csv
8
+ head -n 1 $FILEPATH > $BASE_PATH/arrow.forward.csv
9
+ head -n 1 $FILEPATH > $BASE_PATH/arrow.removed.csv
10
+ head -n 1 $FILEPATH > $BASE_PATH/tmp.csv
11
+
12
+ # Append matching lines (excluding header) to each file
13
+ tail -n +2 $FILEPATH | grep '<' >> $BASE_PATH/arrow.backward.csv
14
+ tail -n +2 $FILEPATH | grep -v '<' >> $BASE_PATH/tmp.csv
15
+ tail -n +2 $BASE_PATH/tmp.csv | grep "\\->" >> $BASE_PATH/arrow.forward.csv
16
+ tail -n +2 $BASE_PATH/tmp.csv | grep -v "\\->" >> $BASE_PATH/arrow.removed.csv
17
+
18
+ # Remove auxiliary csv
19
+ rm $BASE_PATH/tmp.csv
model-training/pistachio/step6_preprocess_reactions_config.yaml ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ data:
2
+ path: data/pistachio/reactions_cleaned.txt
3
+ name: data
4
+ proc_dir: data/pistachio/pipeline
5
+ common:
6
+ sequence:
7
+ - PREPROCESS
8
+ - SPLIT
9
+ fragment_bond: TILDE
10
+ reaction_column_name: rxn
11
+ keep_intermediate_columns: false
12
+ preprocess:
13
+ input_file_path: data/pistachio/pipeline/arrow.removed.csv
14
+ output_file_path: data/pistachio/pipeline/data.processed.csv
15
+ min_reactants: 1
16
+ max_reactants: 10
17
+ max_reactants_tokens: 254
18
+ min_agents: 0
19
+ max_agents: 0
20
+ max_agents_tokens: 0
21
+ min_products: 1
22
+ max_products: 10
23
+ max_products_tokens: 254
24
+ max_absolute_formal_charge: 2
25
+ fragment_bond: TILDE
26
+ reaction_column_name: rxn
27
+ keep_intermediate_columns: false
28
+ split:
29
+ input_file_path: data/pistachio/pipeline/data.processed.csv
30
+ output_directory: data/pistachio/pipeline
31
+ split_ratio: 0.05
32
+ reaction_column_name: rxn
33
+ index_column: products
34
+ hash_seed: 42
35
+ shuffle_seed: 42
model-training/pistachio/step7_convert_to_jsonl.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import os
3
+ import json
4
+
5
+ # Define base paths
6
+ base_path = "data/pistachio"
7
+ data_path = os.path.join(base_path, "pipeline")
8
+
9
+ # Define input CSV files
10
+ csv_files = {
11
+ "train": os.path.join(data_path, "data.processed.train.csv"),
12
+ "test": os.path.join(data_path, "data.processed.test.csv"),
13
+ "validation": os.path.join(data_path, "data.processed.validation.csv")
14
+ }
15
+
16
+ # Define output directories
17
+ forward_dir = os.path.join(base_path, "data.forward")
18
+ retro_dir = os.path.join(base_path, "data.retro")
19
+ os.makedirs(forward_dir, exist_ok=True)
20
+ os.makedirs(retro_dir, exist_ok=True)
21
+
22
+ # Process each dataset
23
+ for split, csv_file in csv_files.items():
24
+ # Read CSV file
25
+ df = pd.read_csv(csv_file, header=0, names=["reactants", "products"], sep=">>", engine="python")
26
+
27
+ # Escape backslashes
28
+ df["reactants"] = df["reactants"].astype(str)
29
+ df["products"] = df["products"].astype(str)
30
+
31
+ # Write forward JSONL
32
+ forward_file = os.path.join(forward_dir, f"{split}.jsonl")
33
+ with open(forward_file, "w") as f_out:
34
+ for _, row in df.iterrows():
35
+ json.dump({"source": row["reactants"], "target": row["products"]}, f_out)
36
+ f_out.write("\n")
37
+
38
+ # Write retro JSONL
39
+ retro_file = os.path.join(retro_dir, f"{split}.jsonl")
40
+ with open(retro_file, "w") as f_out:
41
+ for _, row in df.iterrows():
42
+ json.dump({"source": row["products"], "target": row["reactants"]}, f_out)
43
+ f_out.write("\n")
44
+
45
+ print("All JSONL files created successfully.")
46
+
model-training/pistachio/step8_generate_vocab.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import re
4
+ from collections import Counter
5
+
6
+ # Reserved tokens
7
+ reserved_tokens = [
8
+ "[PAD]", "[unused1]", "[unused2]", "[unused3]", "[unused4]", "[unused5]",
9
+ "[unused6]", "[unused7]", "[unused8]", "[unused9]", "[unused10]", "[UNK]",
10
+ "[CLS]", "[SEP]", "[MASK]"
11
+ ]
12
+
13
+ def count_characters(file_path):
14
+ char_counter = Counter()
15
+ with open(file_path, 'r') as file:
16
+ for line in file:
17
+ data = json.loads(line)
18
+ source = data.get('source', '')
19
+ target = data.get('target', '')
20
+ combined = source + target
21
+
22
+ # Extract and count bracketed tokens
23
+ bracket_tokens = re.findall(r'\[.*?\]', combined)
24
+ char_counter.update(bracket_tokens)
25
+
26
+ # Remove bracketed tokens
27
+ combined = re.sub(r'\[.*?\]', '', combined)
28
+
29
+ # Extract and count composite % tokens (e.g., %10, %11)
30
+ percent_tokens = re.findall(r'%\d{2}', combined)
31
+ char_counter.update(percent_tokens)
32
+
33
+ # Remove composite % tokens
34
+ combined = re.sub(r'%\d{2}', '', combined)
35
+
36
+ # Tokenize remaining string
37
+ tokens = []
38
+ i = 0
39
+ while i < len(combined):
40
+ if combined[i:i+2] in ('Cl', 'Br'):
41
+ tokens.append(combined[i:i+2])
42
+ i += 2
43
+ else:
44
+ tokens.append(combined[i])
45
+ i += 1
46
+
47
+ char_counter.update(tokens)
48
+
49
+ return char_counter
50
+
51
+ def generate_vocab(train_file, val_file, output_file):
52
+ train_counter = count_characters(train_file)
53
+ val_counter = count_characters(val_file)
54
+ total_counter = train_counter + val_counter
55
+ sorted_tokens = [token for token, _ in total_counter.most_common()]
56
+ vocab = reserved_tokens + sorted_tokens
57
+ with open(output_file, 'w') as file:
58
+ for token in vocab:
59
+ file.write(token + '\n')
60
+
61
+
62
+ if __name__ == "__main__":
63
+ import argparse
64
+
65
+ parser = argparse.ArgumentParser(
66
+ description="Generate a vocabulary file from forward or retro JSONL splits."
67
+ )
68
+ parser.add_argument(
69
+ "--data-dir",
70
+ required=True,
71
+ help="Directory containing train.jsonl and validation.jsonl (e.g. data/pistachio/data.forward.256)",
72
+ )
73
+ parser.add_argument(
74
+ "--output",
75
+ default="vocab.txt",
76
+ help="Output vocabulary file path (default: vocab.txt)",
77
+ )
78
+ args = parser.parse_args()
79
+
80
+ train_file = os.path.join(args.data_dir, "train.jsonl")
81
+ val_file = os.path.join(args.data_dir, "validation.jsonl")
82
+ generate_vocab(train_file, val_file, args.output)
83
+ print(f"Vocabulary written to '{args.output}'")
model-training/requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ lightning
2
+ pandas
3
+ python-Levenshtein
4
+ rdkit
5
+ rxn-chem-utils
6
+ torch
7
+ transformers
model-training/tools/analyze_explicit_hydrogens.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script to analyze reactions with explicit hydrogens and check for organometallic elements.
4
+ Usage: python check_explicit_hydrogens.py <input_file>
5
+ """
6
+
7
+ import sys
8
+ import re
9
+ from collections import Counter
10
+
11
+ # Common organometallic and metalloid elements
12
+ METALS = {
13
+ 'Li', 'Na', 'K', 'Mg', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni',
14
+ 'Cu', 'Zn', 'Al', 'Ga', 'Ge', 'Sn', 'Pb', 'B', 'Si', 'P', 'Pd', 'Pt', 'Rh',
15
+ 'Ru', 'Os', 'Ir', 'Au', 'Ag', 'Cd', 'Hg', 'Zr', 'Mo', 'W', 'Re', 'Bi'
16
+ }
17
+
18
+ def has_explicit_hydrogens(smiles):
19
+ """Check if SMILES contains explicit hydrogens like [CH], [CH2], [CH3]"""
20
+ return bool(re.search(r'\[CH\d?\]', smiles))
21
+
22
+ def extract_elements(smiles):
23
+ """Extract all elements in square brackets from SMILES"""
24
+ # Match elements in square brackets, including charges and other notation
25
+ pattern = r'\[([A-Z][a-z]?)'
26
+ elements = re.findall(pattern, smiles)
27
+ return set(elements)
28
+
29
+ def main():
30
+ if len(sys.argv) != 2:
31
+ print("Usage: python check_explicit_hydrogens.py <input_file>")
32
+ sys.exit(1)
33
+
34
+ input_file = sys.argv[1]
35
+
36
+ try:
37
+ with open(input_file, 'r') as f:
38
+ lines = f.readlines()
39
+ except FileNotFoundError:
40
+ print(f"Error: File {input_file} not found")
41
+ sys.exit(1)
42
+
43
+ # Skip header if present
44
+ if lines and lines[0].strip().lower() in ['rxn', 'reaction', 'smiles']:
45
+ lines = lines[1:]
46
+
47
+ total_reactions = len(lines)
48
+ reactions_with_explicit_h = []
49
+ reactions_with_explicit_h_and_metals = []
50
+ reactions_with_explicit_h_no_metals = []
51
+ metal_counter = Counter()
52
+
53
+ print("Analyzing reactions...")
54
+ print("=" * 60)
55
+
56
+ for line in lines:
57
+ line = line.strip()
58
+ if not line:
59
+ continue
60
+
61
+ if has_explicit_hydrogens(line):
62
+ reactions_with_explicit_h.append(line)
63
+ elements = extract_elements(line)
64
+ metals_found = elements & METALS
65
+
66
+ if metals_found:
67
+ reactions_with_explicit_h_and_metals.append(line)
68
+ for metal in metals_found:
69
+ metal_counter[metal] += 1
70
+ else:
71
+ reactions_with_explicit_h_no_metals.append(line)
72
+
73
+ # Print statistics
74
+ print(f"\nTotal reactions: {total_reactions}")
75
+ print(f"Reactions with explicit hydrogens: {len(reactions_with_explicit_h)}")
76
+ print(f" - With organometallic elements: {len(reactions_with_explicit_h_and_metals)}")
77
+ print(f" - Without organometallic elements: {len(reactions_with_explicit_h_no_metals)}")
78
+
79
+ if reactions_with_explicit_h:
80
+ percentage = (len(reactions_with_explicit_h_and_metals) / len(reactions_with_explicit_h)) * 100
81
+ print(f"\nPercentage with metals: {percentage:.2f}%")
82
+
83
+ # Show metal breakdown
84
+ if metal_counter:
85
+ print("\nBreakdown by metal type (for reactions with explicit H):")
86
+ print("=" * 60)
87
+ for metal, count in metal_counter.most_common():
88
+ print(f" {metal}: {count} reactions")
89
+
90
+ # Show examples without metals
91
+ if reactions_with_explicit_h_no_metals:
92
+ print(f"\nFirst 10 examples with explicit H but NO metals:")
93
+ print("=" * 60)
94
+ for i, rxn in enumerate(reactions_with_explicit_h_no_metals[:10], 1):
95
+ # Truncate long reactions for display
96
+ display_rxn = rxn if len(rxn) <= 100 else rxn[:97] + "..."
97
+ print(f"{i}. {display_rxn}")
98
+
99
+ # Additional analysis: check for specific patterns
100
+ print("\n\nAdditional Analysis:")
101
+ print("=" * 60)
102
+
103
+ # Check for radicals (indicated by explicit H on single atoms)
104
+ radical_pattern = r'\[CH?\](?![0-9])' # [C] or [CH] not followed by a digit
105
+ radicals = [rxn for rxn in reactions_with_explicit_h_no_metals if re.search(radical_pattern, rxn)]
106
+ print(f"Potential radicals (no metals): {len(radicals)}")
107
+
108
+ # Check for charged species
109
+ charged_pattern = r'\[CH\d?[+-]\]'
110
+ charged = [rxn for rxn in reactions_with_explicit_h_no_metals if re.search(charged_pattern, rxn)]
111
+ print(f"Charged species with explicit H (no metals): {len(charged)}")
112
+
113
+ if radicals and len(radicals) <= 5:
114
+ print("\nExamples of potential radicals:")
115
+ for rxn in radicals[:5]:
116
+ display_rxn = rxn if len(rxn) <= 100 else rxn[:97] + "..."
117
+ print(f" {display_rxn}")
118
+
119
+ if __name__ == "__main__":
120
+ main()
121
+
122
+ # Made with Bob
model-training/tools/analyze_tokens.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Analyse token lengths across the train/test/validation JSONL splits.
3
+
4
+ Reports the maximum token count for source and target sequences and how
5
+ many samples exceed the configured max_length threshold.
6
+
7
+ Usage:
8
+ python analyze_tokens.py --data-dir data/pistachio/data.forward --vocab vocab/vocab.txt
9
+ python analyze_tokens.py --data-dir data/pistachio/data.forward --vocab vocab/vocab.txt --max-length 512
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import os
15
+ import sys
16
+
17
+ # Allow running from any directory inside model-training/
18
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
19
+
20
+ from transformers_model.smiles_tokenizer import SmilesTokenizer
21
+
22
+ SPLITS = ["train", "test", "validation"]
23
+ BATCH_SIZE = 1000
24
+
25
+
26
+ def analyse(data_dir: str, vocab_path: str, max_length: int) -> None:
27
+ tokenizer = SmilesTokenizer(vocab_path)
28
+
29
+ source_max_tokens = 0
30
+ target_max_tokens = 0
31
+ source_over_limit = 0
32
+ target_over_limit = 0
33
+ total_samples = 0
34
+
35
+ for split in SPLITS:
36
+ file_path = os.path.join(data_dir, f"{split}.jsonl")
37
+ if not os.path.exists(file_path):
38
+ print(f" [skip] {file_path} not found")
39
+ continue
40
+
41
+ print(f" Processing {file_path} ...")
42
+ with open(file_path, "r", encoding="utf-8") as f:
43
+ batch_sources = []
44
+ batch_targets = []
45
+
46
+ def process_batch(sources, targets):
47
+ nonlocal source_max_tokens, target_max_tokens
48
+ nonlocal source_over_limit, target_over_limit
49
+
50
+ source_encodings = tokenizer(sources, add_special_tokens=True, truncation=False)
51
+ target_encodings = tokenizer(targets, add_special_tokens=True, truncation=False)
52
+
53
+ for s_ids, t_ids in zip(source_encodings["input_ids"], target_encodings["input_ids"]):
54
+ s_len = len(s_ids)
55
+ t_len = len(t_ids)
56
+ source_max_tokens = max(source_max_tokens, s_len)
57
+ target_max_tokens = max(target_max_tokens, t_len)
58
+ if s_len > max_length:
59
+ source_over_limit += 1
60
+ if t_len > max_length:
61
+ target_over_limit += 1
62
+
63
+ for line in f:
64
+ data = json.loads(line)
65
+ batch_sources.append(data["source"])
66
+ batch_targets.append(data["target"])
67
+ total_samples += 1
68
+
69
+ if len(batch_sources) == BATCH_SIZE:
70
+ process_batch(batch_sources, batch_targets)
71
+ batch_sources = []
72
+ batch_targets = []
73
+
74
+ if batch_sources:
75
+ process_batch(batch_sources, batch_targets)
76
+
77
+ print()
78
+ print(f"Max length threshold : {max_length}")
79
+ print(f"Max tokens in source : {source_max_tokens}")
80
+ print(f"Max tokens in target : {target_max_tokens}")
81
+ print(f"Source over limit : {source_over_limit} / {total_samples} ({(source_over_limit / total_samples) * 100:.2f}%)" if total_samples else "No samples processed.")
82
+ print(f"Target over limit : {target_over_limit} / {total_samples} ({(target_over_limit / total_samples) * 100:.2f}%)" if total_samples else "")
83
+ print(f"Total samples : {total_samples}")
84
+
85
+
86
+ if __name__ == "__main__":
87
+ parser = argparse.ArgumentParser(description="Analyse token lengths in JSONL splits.")
88
+ parser.add_argument(
89
+ "--data-dir",
90
+ required=True,
91
+ help="Directory containing train.jsonl, test.jsonl, validation.jsonl "
92
+ "(e.g. data/pistachio/data.forward)",
93
+ )
94
+ parser.add_argument(
95
+ "--vocab",
96
+ required=True,
97
+ help="Path to vocab.txt (e.g. data/pistachio/vocab.txt)",
98
+ )
99
+ parser.add_argument(
100
+ "--max-length",
101
+ type=int,
102
+ default=256,
103
+ help="Token length threshold to count over-limit samples (default: 256)",
104
+ )
105
+ args = parser.parse_args()
106
+ analyse(args.data_dir, args.vocab, args.max_length)
model-training/tools/check_mol_from_smiles.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from rdkit import Chem, DataStructs
2
+ from rdkit.Chem.rdFingerprintGenerator import GetMorganGenerator
3
+
4
+
5
+ smiles = "O=S(=O)([O-])c1ccccc1~O=S(=O)([O-])c1ccccc1~O=S(=O)([O-])c1ccccc1~O=S(=O)([O-])c1ccccc1~O=S(=O)([O-])c1ccccc1~[Fe+2]~[Fe+2]~[Fe+2]~[Fe+2]~[Fe+2]~[Fe+2]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~[Fe+3]~"
6
+
7
+ if __name__ == "__main__":
8
+ print(f"Smiles: {smiles}")
9
+ mol1 = Chem.MolFromSmiles(smiles)
10
+ print(f"Molecule: {mol1}")
model-training/tools/check_tanimoto.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from rdkit import Chem, DataStructs
2
+ from rdkit.Chem.rdFingerprintGenerator import GetMorganGenerator
3
+
4
+
5
+ predicted_texts = [
6
+ "",
7
+ ]
8
+
9
+ target_texts = [
10
+ "",
11
+ ]
12
+
13
+
14
+ if __name__ == "__main__":
15
+ similarities = []
16
+ invalid_pairs = 0
17
+ generator = GetMorganGenerator(radius=2, fpSize=2048)
18
+ for pred, tgt in zip(predicted_texts, target_texts):
19
+
20
+ # Get molecule from prediction
21
+ print(f"Predicted smiles (pred): {pred}")
22
+ mol1 = Chem.MolFromSmiles(pred)
23
+ print(f"Predicted molecule (mol1): {mol1}")
24
+
25
+ # Get molecule from ground truth
26
+ print(f"Target smiles (tgt): {tgt}")
27
+ mol2 = Chem.MolFromSmiles(tgt)
28
+ print(f"Target molecule (mol2): {mol2}")
29
+
30
+ # Increment invalid pairs on failure
31
+ if mol1 is None or mol2 is None:
32
+ invalid_pairs += 1
33
+ print(f"Invalid pair! Total: {invalid_pairs}")
34
+ continue
35
+
36
+
37
+ fp1 = generator.GetFingerprint(mol1)
38
+ print(f"Predicted fingerprint (fp1): {fp1}")
39
+ fp2 = generator.GetFingerprint(mol2)
40
+ print(f"Target fingerprint (fp2): {fp2}")
41
+ sim = DataStructs.TanimotoSimilarity(fp1, fp2)
42
+ print(f"Similarity (sim): {sim}")
43
+ similarities.append(sim)
44
+
45
+ avg_tanimoto = sum(similarities) / len(similarities) if len(similarities) > 0 else 0
46
+ print(f"Average Tanimoto (avg_tanimoto): {avg_tanimoto}")
model-training/tools/run_fit.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Train the model directly with Lightning, bypassing LightningCLI.
3
+
4
+ Useful for quick experiments where you want to set options in code rather
5
+ than via a YAML config. For production training use cli_main.py with the
6
+ YAML configs in training/.
7
+
8
+ Usage (run from model-training/):
9
+ python debug/run_fit.py
10
+ """
11
+
12
+ import os
13
+ import sys
14
+
15
+ # Allow running from any directory inside model-training/
16
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
17
+
18
+ import lightning as L
19
+
20
+ from transformers_model.smiles_datamodule import LitSmilesDataset
21
+ from transformers_model.model import LitVanillaTransformer
22
+
23
+
24
+ if __name__ == "__main__":
25
+ L.seed_everything(42)
26
+
27
+ smiles_dataset = LitSmilesDataset(
28
+ vocab_path="vocab/vocab.txt",
29
+ train_path="data/pistachio/data.forward/train.jsonl",
30
+ validation_path="data/pistachio/data.forward/validation.jsonl",
31
+ num_dataloader_workers=16,
32
+ batch_size=256,
33
+ max_length=256,
34
+ )
35
+
36
+ model = LitVanillaTransformer(
37
+ vocab_path="vocab/vocab.txt",
38
+ task="forward",
39
+ max_length=256,
40
+ )
41
+
42
+ trainer = L.Trainer(
43
+ max_epochs=10,
44
+ accelerator="gpu",
45
+ devices=1,
46
+ precision=32,
47
+ )
48
+
49
+ trainer.fit(model, datamodule=smiles_dataset)
50
+ print("Training complete.")
model-training/tools/run_lr_finder.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Runs the Lightning learning rate finder and saves a plot and CSV of the results.
3
+
4
+ Output files are written to the same directory as this script:
5
+ - lr_finder_plot.png
6
+ - lr_finder_results.csv
7
+
8
+ Usage (run from model-training/):
9
+ python debug/run_lr_finder.py
10
+ python debug/run_lr_finder.py --data-dir data/pistachio/data.forward --vocab vocab/vocab.txt
11
+ """
12
+
13
+ import argparse
14
+ import csv
15
+ import os
16
+ import sys
17
+ import torch
18
+
19
+ # Allow running from any directory inside model-training/
20
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
21
+
22
+ import lightning as L
23
+ import matplotlib.pyplot as plt
24
+ from lightning.pytorch.tuner import Tuner
25
+
26
+ from transformers_model.smiles_datamodule import LitSmilesDataset
27
+ from transformers_model.model import LitVanillaTransformer
28
+
29
+ HERE = os.path.dirname(os.path.abspath(__file__))
30
+
31
+
32
+ def run(data_dir: str, vocab_path: str) -> None:
33
+ L.seed_everything(42)
34
+ torch.set_float32_matmul_precision("high")
35
+
36
+ smiles_dataset = LitSmilesDataset(
37
+ vocab_path=vocab_path,
38
+ train_path=os.path.join(data_dir, "train.jsonl"),
39
+ validation_path=os.path.join(data_dir, "validation.jsonl"),
40
+ num_dataloader_workers=16,
41
+ batch_size=1024,
42
+ max_length=256,
43
+ )
44
+
45
+ model = LitVanillaTransformer(
46
+ vocab_path=vocab_path,
47
+ task="forward",
48
+ max_length=256,
49
+ ignore_nan=True,
50
+ )
51
+
52
+ trainer = L.Trainer(
53
+ max_epochs=60,
54
+ accelerator="gpu",
55
+ devices=1,
56
+ precision="16-mixed",
57
+ deterministic=True,
58
+ gradient_clip_val=1.0,
59
+ gradient_clip_algorithm="norm",
60
+ )
61
+
62
+ tuner = Tuner(trainer)
63
+ lr_finder = tuner.lr_find(model, datamodule=smiles_dataset)
64
+
65
+ # Plot
66
+ fig = lr_finder.plot(suggest=True)
67
+ plt.grid(True, which="both", linestyle="--", linewidth=0.5)
68
+ plot_path = os.path.join(HERE, "lr_finder_plot.png")
69
+ fig.savefig(plot_path)
70
+ print(f"Plot saved to {plot_path}")
71
+ plt.show()
72
+
73
+ # Save results as CSV
74
+ csv_path = os.path.join(HERE, "lr_finder_results.csv")
75
+ with open(csv_path, "w", newline="") as f:
76
+ writer = csv.writer(f)
77
+ writer.writerow(["learning_rate", "loss"])
78
+ writer.writerows(zip(lr_finder.results["lr"], lr_finder.results["loss"]))
79
+ print(f"Results saved to {csv_path}")
80
+
81
+ print(f"Suggested learning rate: {lr_finder.suggestion()}")
82
+
83
+
84
+ if __name__ == "__main__":
85
+ parser = argparse.ArgumentParser(description="Run the Lightning LR finder.")
86
+ parser.add_argument(
87
+ "--data-dir",
88
+ default="data/pistachio/data.forward",
89
+ help="Directory containing train.jsonl and validation.jsonl "
90
+ "(default: data/pistachio/data.forward)",
91
+ )
92
+ parser.add_argument(
93
+ "--vocab",
94
+ default="vocab/vocab.txt",
95
+ help="Path to vocab.txt (default: vocab/vocab.txt)",
96
+ )
97
+ args = parser.parse_args()
98
+ run(args.data_dir, args.vocab)
model-training/tools/run_predict.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Run predictions with the model directly via Lightning, bypassing LightningCLI.
3
+
4
+ Useful for quick experiments where you want to set options in code rather
5
+ than via a YAML config. For production inference use cli_main.py with
6
+ training/predict.yaml.
7
+
8
+ Usage (run from model-training/):
9
+ python debug/run_predict.py
10
+ """
11
+
12
+ import os
13
+ import sys
14
+
15
+ # Allow running from any directory inside model-training/
16
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
17
+
18
+ import lightning as L
19
+
20
+ from transformers_model.smiles_datamodule import LitSmilesDataset
21
+ from transformers_model.model import LitVanillaTransformer
22
+
23
+
24
+ if __name__ == "__main__":
25
+ L.seed_everything(42)
26
+
27
+ smiles_dataset = LitSmilesDataset(
28
+ vocab_path="vocab/vocab.txt",
29
+ predict_path="data/input.jsonl",
30
+ num_dataloader_workers=16,
31
+ batch_size=256,
32
+ max_length=256,
33
+ )
34
+
35
+ # Replace with the actual checkpoint path
36
+ model = LitVanillaTransformer.load_from_checkpoint(
37
+ checkpoint_path="lightning_logs/forward/checkpoints/epoch=59-val_accuracy=0.6476.ckpt",
38
+ vocab_path="vocab/vocab.txt",
39
+ )
40
+
41
+ trainer = L.Trainer(
42
+ accelerator="gpu",
43
+ devices=1,
44
+ precision=32,
45
+ )
46
+
47
+ trainer.predict(model, datamodule=smiles_dataset)
48
+ print("Prediction complete.")
model-training/tools/run_test.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test the model directly with Lightning, bypassing LightningCLI.
3
+
4
+ Useful for quick experiments where you want to set options in code rather
5
+ than via a YAML config. For production testing use cli_main.py with the
6
+ YAML configs in training/.
7
+
8
+ Usage (run from model-training/):
9
+ python debug/run_test.py
10
+ """
11
+
12
+ import os
13
+ import sys
14
+
15
+ # Allow running from any directory inside model-training/
16
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
17
+
18
+ import lightning as L
19
+
20
+ from transformers_model.smiles_datamodule import LitSmilesDataset
21
+ from transformers_model.model import LitVanillaTransformer
22
+
23
+
24
+ if __name__ == "__main__":
25
+ L.seed_everything(42)
26
+
27
+ smiles_dataset = LitSmilesDataset(
28
+ vocab_path="vocab/vocab.txt",
29
+ test_path="data/pistachio/data.forward/test.jsonl",
30
+ num_dataloader_workers=16,
31
+ batch_size=256,
32
+ max_length=256,
33
+ )
34
+
35
+ # Replace with the actual checkpoint path
36
+ model = LitVanillaTransformer.load_from_checkpoint(
37
+ checkpoint_path="lightning_logs/forward/checkpoints/epoch=59-val_accuracy=0.6476.ckpt",
38
+ vocab_path="vocab/vocab.txt",
39
+ )
40
+
41
+ trainer = L.Trainer(
42
+ accelerator="gpu",
43
+ devices=1,
44
+ precision=32,
45
+ )
46
+
47
+ trainer.test(model, datamodule=smiles_dataset)
48
+ print("Testing complete.")
model-training/training/cli_main.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ from lightning.pytorch.cli import LightningCLI
4
+ from transformers_model.model import LitVanillaTransformer
5
+ from transformers_model.smiles_datamodule import LitSmilesDataset
6
+
7
+
8
+ def main():
9
+
10
+ # Enable Tensor Core optimization
11
+ torch.set_float32_matmul_precision('high')
12
+
13
+ LightningCLI(
14
+ model_class=LitVanillaTransformer,
15
+ datamodule_class=LitSmilesDataset,
16
+ )
17
+
18
+
19
+ if __name__ == "__main__":
20
+ main()
model-training/training/predict.yaml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ model:
2
+ vocab_path: vocab/vocab.txt
3
+ task: "forward"
4
+ predictions_path: "predictions.jsonl"
5
+ num_beams: 3
6
+ topn: 3
7
+ max_length: 256 # for generating predictions
8
+ data:
9
+ vocab_path: vocab/vocab.txt
10
+ predict_path: data/input.jsonl
11
+ truncation: true
12
+ max_length: 256 # for tokenizing input data
13
+ num_dataloader_workers: 16
14
+ batch_size: 512
15
+ seed_everything: 42
16
+ ckpt_path: lightning_logs/forward/checkpoints/epoch=59-val_accuracy=0.6476.ckpt
model-training/training/test.yaml ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ model:
2
+ vocab_path: vocab/vocab.txt
3
+ task: "forward"
4
+ learning_rate: 0.001584893192461114
5
+ warmup_ratio: 0.1
6
+ # attention_mask: -1e4
7
+ max_length: 256 # for generating predictions
8
+ data:
9
+ vocab_path: vocab/vocab.txt
10
+ test_path: data/pistachio/data.forward/test.jsonl
11
+ truncation: true
12
+ max_length: 256 # for tokenizing input data
13
+ num_dataloader_workers: 16
14
+ batch_size: 512
15
+ trainer:
16
+ accumulate_grad_batches: 2
17
+ max_epochs: 60
18
+ # profiler: simple
19
+ accelerator: gpu
20
+ enable_progress_bar: false
21
+ devices: 1
22
+ precision: 32
23
+ deterministic: true
24
+ # detect_anomaly: true
25
+ gradient_clip_val: 1.0
26
+ gradient_clip_algorithm: "norm"
27
+ callbacks:
28
+ - class_path: lightning.pytorch.callbacks.ModelCheckpoint
29
+ init_args:
30
+ monitor: val_accuracy
31
+ mode: max
32
+ save_top_k: 10
33
+ filename: "{epoch:02d}-{val_accuracy:.4f}"
34
+ seed_everything: 42
35
+ ckpt_path: lightning_logs/forward/checkpoints/epoch=59-val_accuracy=0.6476.ckpt