boltz2-python-client 0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- boltz2_client/__init__.py +153 -0
- boltz2_client/cli.py +1213 -0
- boltz2_client/client.py +986 -0
- boltz2_client/exceptions.py +213 -0
- boltz2_client/models.py +466 -0
- boltz2_client/models_affinity.py +46 -0
- boltz2_client/utils.py +455 -0
- boltz2_client/virtual_screening.py +608 -0
- boltz2_python_client-0.2.dist-info/METADATA +533 -0
- boltz2_python_client-0.2.dist-info/RECORD +14 -0
- boltz2_python_client-0.2.dist-info/WHEEL +5 -0
- boltz2_python_client-0.2.dist-info/entry_points.txt +2 -0
- boltz2_python_client-0.2.dist-info/licenses/LICENSE +21 -0
- boltz2_python_client-0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
# ---------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
|
|
3
|
+
# ---------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Virtual Screening Module for Boltz-2
|
|
7
|
+
|
|
8
|
+
Provides high-level APIs for virtual screening campaigns with
|
|
9
|
+
automatic parallelization, result analysis, and visualization.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import time
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Dict, List, Optional, Union, Any, Tuple, Callable
|
|
18
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
19
|
+
import pandas as pd
|
|
20
|
+
|
|
21
|
+
from .client import Boltz2Client, Boltz2SyncClient
|
|
22
|
+
from .models import Polymer, Ligand, PredictionRequest, PocketConstraint
|
|
23
|
+
from .exceptions import Boltz2ValidationError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CompoundLibrary:
|
|
27
|
+
"""Represents a chemical library for virtual screening."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, compounds: List[Dict[str, Any]]):
|
|
30
|
+
"""
|
|
31
|
+
Initialize compound library.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
compounds: List of compound dictionaries with required fields:
|
|
35
|
+
- name: Compound name
|
|
36
|
+
- smiles: SMILES string OR ccd: CCD code
|
|
37
|
+
- metadata: Optional dict with additional info
|
|
38
|
+
"""
|
|
39
|
+
self.compounds = self._validate_compounds(compounds)
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_csv(cls, csv_path: Union[str, Path],
|
|
43
|
+
name_col: str = "name",
|
|
44
|
+
smiles_col: str = "smiles",
|
|
45
|
+
ccd_col: Optional[str] = None) -> "CompoundLibrary":
|
|
46
|
+
"""Load compound library from CSV file."""
|
|
47
|
+
df = pd.read_csv(csv_path)
|
|
48
|
+
compounds = []
|
|
49
|
+
|
|
50
|
+
for _, row in df.iterrows():
|
|
51
|
+
compound = {"name": row[name_col]}
|
|
52
|
+
|
|
53
|
+
if smiles_col in row and pd.notna(row[smiles_col]):
|
|
54
|
+
compound["smiles"] = row[smiles_col]
|
|
55
|
+
elif ccd_col and ccd_col in row and pd.notna(row[ccd_col]):
|
|
56
|
+
compound["ccd"] = row[ccd_col]
|
|
57
|
+
else:
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
# Add all other columns as metadata
|
|
61
|
+
metadata = {}
|
|
62
|
+
for col in df.columns:
|
|
63
|
+
if col not in [name_col, smiles_col, ccd_col]:
|
|
64
|
+
metadata[col] = row[col]
|
|
65
|
+
|
|
66
|
+
if metadata:
|
|
67
|
+
compound["metadata"] = metadata
|
|
68
|
+
|
|
69
|
+
compounds.append(compound)
|
|
70
|
+
|
|
71
|
+
return cls(compounds)
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_json(cls, json_path: Union[str, Path]) -> "CompoundLibrary":
|
|
75
|
+
"""Load compound library from JSON file."""
|
|
76
|
+
with open(json_path, 'r') as f:
|
|
77
|
+
compounds = json.load(f)
|
|
78
|
+
return cls(compounds)
|
|
79
|
+
|
|
80
|
+
def _validate_compounds(self, compounds: List[Dict]) -> List[Dict]:
|
|
81
|
+
"""Validate compound entries."""
|
|
82
|
+
validated = []
|
|
83
|
+
for i, compound in enumerate(compounds):
|
|
84
|
+
if "name" not in compound:
|
|
85
|
+
raise Boltz2ValidationError(f"Compound {i} missing 'name' field")
|
|
86
|
+
|
|
87
|
+
if "smiles" not in compound and "ccd" not in compound:
|
|
88
|
+
raise Boltz2ValidationError(
|
|
89
|
+
f"Compound '{compound['name']}' must have either 'smiles' or 'ccd'"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
validated.append(compound)
|
|
93
|
+
|
|
94
|
+
return validated
|
|
95
|
+
|
|
96
|
+
def __len__(self) -> int:
|
|
97
|
+
return len(self.compounds)
|
|
98
|
+
|
|
99
|
+
def __iter__(self):
|
|
100
|
+
return iter(self.compounds)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class VirtualScreeningResult:
|
|
104
|
+
"""Container for virtual screening results."""
|
|
105
|
+
|
|
106
|
+
def __init__(self,
|
|
107
|
+
target_name: str,
|
|
108
|
+
target_sequence: str,
|
|
109
|
+
results: List[Dict[str, Any]],
|
|
110
|
+
parameters: Dict[str, Any],
|
|
111
|
+
duration_seconds: float):
|
|
112
|
+
self.target_name = target_name
|
|
113
|
+
self.target_sequence = target_sequence
|
|
114
|
+
self.results = results
|
|
115
|
+
self.parameters = parameters
|
|
116
|
+
self.duration_seconds = duration_seconds
|
|
117
|
+
self.timestamp = datetime.now()
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def successful_results(self) -> List[Dict]:
|
|
121
|
+
"""Get only successful predictions."""
|
|
122
|
+
return [r for r in self.results if "error" not in r]
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def failed_results(self) -> List[Dict]:
|
|
126
|
+
"""Get only failed predictions."""
|
|
127
|
+
return [r for r in self.results if "error" in r]
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def success_rate(self) -> float:
|
|
131
|
+
"""Calculate success rate."""
|
|
132
|
+
if not self.results:
|
|
133
|
+
return 0.0
|
|
134
|
+
return len(self.successful_results) / len(self.results)
|
|
135
|
+
|
|
136
|
+
def to_dataframe(self) -> pd.DataFrame:
|
|
137
|
+
"""Convert results to pandas DataFrame."""
|
|
138
|
+
return pd.DataFrame(self.successful_results)
|
|
139
|
+
|
|
140
|
+
def save_results(self, output_dir: Union[str, Path],
|
|
141
|
+
save_structures: bool = True) -> Dict[str, Path]:
|
|
142
|
+
"""
|
|
143
|
+
Save all results to files.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Dictionary of saved file paths
|
|
147
|
+
"""
|
|
148
|
+
output_dir = Path(output_dir)
|
|
149
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
|
|
151
|
+
saved_files = {}
|
|
152
|
+
|
|
153
|
+
# Save summary CSV
|
|
154
|
+
df = self.to_dataframe()
|
|
155
|
+
if not df.empty:
|
|
156
|
+
csv_path = output_dir / "screening_results.csv"
|
|
157
|
+
df.to_csv(csv_path, index=False)
|
|
158
|
+
saved_files["results_csv"] = csv_path
|
|
159
|
+
|
|
160
|
+
# Save structures
|
|
161
|
+
if save_structures:
|
|
162
|
+
structures_dir = output_dir / "structures"
|
|
163
|
+
structures_dir.mkdir(exist_ok=True)
|
|
164
|
+
|
|
165
|
+
for result in self.successful_results:
|
|
166
|
+
if "structure_cif" in result:
|
|
167
|
+
cif_path = structures_dir / f"{result['compound_name'].replace(' ', '_')}.cif"
|
|
168
|
+
with open(cif_path, 'w') as f:
|
|
169
|
+
f.write(result["structure_cif"])
|
|
170
|
+
|
|
171
|
+
# Save metadata
|
|
172
|
+
metadata = {
|
|
173
|
+
"target_name": self.target_name,
|
|
174
|
+
"target_sequence_length": len(self.target_sequence),
|
|
175
|
+
"compounds_screened": len(self.results),
|
|
176
|
+
"successful_predictions": len(self.successful_results),
|
|
177
|
+
"failed_predictions": len(self.failed_results),
|
|
178
|
+
"success_rate": self.success_rate,
|
|
179
|
+
"duration_seconds": self.duration_seconds,
|
|
180
|
+
"timestamp": self.timestamp.isoformat(),
|
|
181
|
+
"parameters": self.parameters
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
metadata_path = output_dir / "screening_metadata.json"
|
|
185
|
+
with open(metadata_path, 'w') as f:
|
|
186
|
+
json.dump(metadata, f, indent=2)
|
|
187
|
+
saved_files["metadata"] = metadata_path
|
|
188
|
+
|
|
189
|
+
return saved_files
|
|
190
|
+
|
|
191
|
+
def get_top_hits(self, n: int = 10, by: str = "predicted_pic50") -> pd.DataFrame:
|
|
192
|
+
"""Get top N compounds by specified metric."""
|
|
193
|
+
df = self.to_dataframe()
|
|
194
|
+
if df.empty or by not in df.columns:
|
|
195
|
+
return pd.DataFrame()
|
|
196
|
+
|
|
197
|
+
return df.nlargest(n, by)
|
|
198
|
+
|
|
199
|
+
def get_statistics_by_group(self, group_by: str = "compound_type") -> pd.DataFrame:
|
|
200
|
+
"""Get statistics grouped by a metadata field."""
|
|
201
|
+
df = self.to_dataframe()
|
|
202
|
+
if df.empty or group_by not in df.columns:
|
|
203
|
+
return pd.DataFrame()
|
|
204
|
+
|
|
205
|
+
stats = df.groupby(group_by).agg({
|
|
206
|
+
'predicted_pic50': ['mean', 'std', 'count'],
|
|
207
|
+
'predicted_ic50_nm': 'mean',
|
|
208
|
+
'binding_probability': 'mean'
|
|
209
|
+
}).round(3)
|
|
210
|
+
|
|
211
|
+
return stats
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class VirtualScreening:
|
|
215
|
+
"""High-level API for virtual screening campaigns."""
|
|
216
|
+
|
|
217
|
+
def __init__(self,
|
|
218
|
+
client: Optional[Union[Boltz2Client, Boltz2SyncClient]] = None,
|
|
219
|
+
max_workers: int = 4):
|
|
220
|
+
"""
|
|
221
|
+
Initialize virtual screening.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
client: Boltz2 client instance (if None, creates default)
|
|
225
|
+
max_workers: Maximum parallel workers for screening
|
|
226
|
+
"""
|
|
227
|
+
self.client = client or Boltz2SyncClient()
|
|
228
|
+
self.max_workers = max_workers
|
|
229
|
+
self.is_async = isinstance(self.client, Boltz2Client)
|
|
230
|
+
|
|
231
|
+
def screen(self,
|
|
232
|
+
target_sequence: str,
|
|
233
|
+
compound_library: Union[CompoundLibrary, List[Dict], str, Path],
|
|
234
|
+
target_name: str = "Target",
|
|
235
|
+
predict_affinity: bool = True,
|
|
236
|
+
pocket_residues: Optional[List[int]] = None,
|
|
237
|
+
pocket_radius: float = 10.0,
|
|
238
|
+
recycling_steps: int = 2,
|
|
239
|
+
sampling_steps: int = 30,
|
|
240
|
+
diffusion_samples: int = 1,
|
|
241
|
+
sampling_steps_affinity: int = 100,
|
|
242
|
+
diffusion_samples_affinity: int = 3,
|
|
243
|
+
affinity_mw_correction: bool = True,
|
|
244
|
+
batch_size: Optional[int] = None,
|
|
245
|
+
progress_callback: Optional[Callable] = None) -> VirtualScreeningResult:
|
|
246
|
+
"""
|
|
247
|
+
Run virtual screening campaign.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
target_sequence: Protein target sequence
|
|
251
|
+
compound_library: Compounds to screen (CompoundLibrary, list, or path to file)
|
|
252
|
+
target_name: Name of the target protein
|
|
253
|
+
predict_affinity: Enable affinity prediction
|
|
254
|
+
pocket_residues: List of residue indices defining binding pocket
|
|
255
|
+
pocket_radius: Radius for pocket constraint in Angstroms
|
|
256
|
+
recycling_steps: Number of recycling steps
|
|
257
|
+
sampling_steps: Number of sampling steps
|
|
258
|
+
diffusion_samples: Number of diffusion samples
|
|
259
|
+
sampling_steps_affinity: Sampling steps for affinity prediction
|
|
260
|
+
diffusion_samples_affinity: Diffusion samples for affinity
|
|
261
|
+
affinity_mw_correction: Apply molecular weight correction
|
|
262
|
+
batch_size: Process compounds in batches (None = all parallel)
|
|
263
|
+
progress_callback: Function called with (completed, total) after each compound
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
VirtualScreeningResult object with all results
|
|
267
|
+
"""
|
|
268
|
+
# Prepare compound library
|
|
269
|
+
if isinstance(compound_library, (str, Path)):
|
|
270
|
+
path = Path(compound_library)
|
|
271
|
+
if path.suffix == '.csv':
|
|
272
|
+
compound_library = CompoundLibrary.from_csv(path)
|
|
273
|
+
elif path.suffix == '.json':
|
|
274
|
+
compound_library = CompoundLibrary.from_json(path)
|
|
275
|
+
else:
|
|
276
|
+
raise ValueError(f"Unsupported file format: {path.suffix}")
|
|
277
|
+
elif isinstance(compound_library, list):
|
|
278
|
+
compound_library = CompoundLibrary(compound_library)
|
|
279
|
+
|
|
280
|
+
# Store parameters
|
|
281
|
+
parameters = {
|
|
282
|
+
"target_name": target_name,
|
|
283
|
+
"predict_affinity": predict_affinity,
|
|
284
|
+
"pocket_residues": pocket_residues,
|
|
285
|
+
"pocket_radius": pocket_radius,
|
|
286
|
+
"recycling_steps": recycling_steps,
|
|
287
|
+
"sampling_steps": sampling_steps,
|
|
288
|
+
"diffusion_samples": diffusion_samples,
|
|
289
|
+
"sampling_steps_affinity": sampling_steps_affinity if predict_affinity else None,
|
|
290
|
+
"diffusion_samples_affinity": diffusion_samples_affinity if predict_affinity else None,
|
|
291
|
+
"affinity_mw_correction": affinity_mw_correction if predict_affinity else None
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# Run screening
|
|
295
|
+
start_time = time.time()
|
|
296
|
+
|
|
297
|
+
if self.is_async:
|
|
298
|
+
# Use async screening
|
|
299
|
+
results = asyncio.run(self._screen_async(
|
|
300
|
+
target_sequence, compound_library, parameters,
|
|
301
|
+
pocket_residues, pocket_radius, progress_callback
|
|
302
|
+
))
|
|
303
|
+
else:
|
|
304
|
+
# Use sync screening with thread pool
|
|
305
|
+
results = self._screen_sync(
|
|
306
|
+
target_sequence, compound_library, parameters,
|
|
307
|
+
pocket_residues, pocket_radius, progress_callback, batch_size
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
duration = time.time() - start_time
|
|
311
|
+
|
|
312
|
+
return VirtualScreeningResult(
|
|
313
|
+
target_name=target_name,
|
|
314
|
+
target_sequence=target_sequence,
|
|
315
|
+
results=results,
|
|
316
|
+
parameters=parameters,
|
|
317
|
+
duration_seconds=duration
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
def _screen_sync(self, target_sequence: str, compound_library: CompoundLibrary,
|
|
321
|
+
parameters: Dict, pocket_residues: Optional[List[int]],
|
|
322
|
+
pocket_radius: float, progress_callback: Optional[Callable],
|
|
323
|
+
batch_size: Optional[int]) -> List[Dict]:
|
|
324
|
+
"""Synchronous screening implementation."""
|
|
325
|
+
results = []
|
|
326
|
+
total = len(compound_library)
|
|
327
|
+
|
|
328
|
+
# Create protein polymer once
|
|
329
|
+
protein = Polymer(
|
|
330
|
+
id="A",
|
|
331
|
+
molecule_type="protein",
|
|
332
|
+
sequence=target_sequence
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Prepare pocket constraint if specified
|
|
336
|
+
constraints = []
|
|
337
|
+
if pocket_residues:
|
|
338
|
+
pocket_constraint = PocketConstraint(
|
|
339
|
+
chain_id="A",
|
|
340
|
+
residue_idxs=pocket_residues,
|
|
341
|
+
radius=pocket_radius
|
|
342
|
+
)
|
|
343
|
+
constraints.append(pocket_constraint)
|
|
344
|
+
|
|
345
|
+
# Process compounds
|
|
346
|
+
if batch_size and batch_size < total:
|
|
347
|
+
# Process in batches
|
|
348
|
+
for i in range(0, total, batch_size):
|
|
349
|
+
batch = list(compound_library.compounds[i:i+batch_size])
|
|
350
|
+
batch_results = self._process_batch_sync(
|
|
351
|
+
protein, batch, parameters, constraints
|
|
352
|
+
)
|
|
353
|
+
results.extend(batch_results)
|
|
354
|
+
|
|
355
|
+
if progress_callback:
|
|
356
|
+
progress_callback(len(results), total)
|
|
357
|
+
else:
|
|
358
|
+
# Process all in parallel
|
|
359
|
+
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
360
|
+
futures = []
|
|
361
|
+
|
|
362
|
+
for compound in compound_library:
|
|
363
|
+
future = executor.submit(
|
|
364
|
+
self._screen_single_compound_sync,
|
|
365
|
+
protein, compound, parameters, constraints
|
|
366
|
+
)
|
|
367
|
+
futures.append(future)
|
|
368
|
+
|
|
369
|
+
for i, future in enumerate(futures):
|
|
370
|
+
result = future.result()
|
|
371
|
+
results.append(result)
|
|
372
|
+
|
|
373
|
+
if progress_callback:
|
|
374
|
+
progress_callback(i + 1, total)
|
|
375
|
+
|
|
376
|
+
return results
|
|
377
|
+
|
|
378
|
+
async def _screen_async(self, target_sequence: str, compound_library: CompoundLibrary,
|
|
379
|
+
parameters: Dict, pocket_residues: Optional[List[int]],
|
|
380
|
+
pocket_radius: float, progress_callback: Optional[Callable]) -> List[Dict]:
|
|
381
|
+
"""Asynchronous screening implementation."""
|
|
382
|
+
# Create protein polymer
|
|
383
|
+
protein = Polymer(
|
|
384
|
+
id="A",
|
|
385
|
+
molecule_type="protein",
|
|
386
|
+
sequence=target_sequence
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Prepare pocket constraint
|
|
390
|
+
constraints = []
|
|
391
|
+
if pocket_residues:
|
|
392
|
+
pocket_constraint = PocketConstraint(
|
|
393
|
+
chain_id="A",
|
|
394
|
+
residue_idxs=pocket_residues,
|
|
395
|
+
radius=pocket_radius
|
|
396
|
+
)
|
|
397
|
+
constraints.append(pocket_constraint)
|
|
398
|
+
|
|
399
|
+
# Create tasks
|
|
400
|
+
tasks = []
|
|
401
|
+
for compound in compound_library:
|
|
402
|
+
task = self._screen_single_compound_async(
|
|
403
|
+
protein, compound, parameters, constraints
|
|
404
|
+
)
|
|
405
|
+
tasks.append(task)
|
|
406
|
+
|
|
407
|
+
# Run with progress updates
|
|
408
|
+
results = []
|
|
409
|
+
total = len(tasks)
|
|
410
|
+
|
|
411
|
+
for i, task in enumerate(asyncio.as_completed(tasks)):
|
|
412
|
+
result = await task
|
|
413
|
+
results.append(result)
|
|
414
|
+
|
|
415
|
+
if progress_callback:
|
|
416
|
+
progress_callback(i + 1, total)
|
|
417
|
+
|
|
418
|
+
return results
|
|
419
|
+
|
|
420
|
+
def _screen_single_compound_sync(self, protein: Polymer, compound: Dict,
|
|
421
|
+
parameters: Dict, constraints: List) -> Dict:
|
|
422
|
+
"""Screen a single compound synchronously."""
|
|
423
|
+
try:
|
|
424
|
+
# Create ligand
|
|
425
|
+
if "smiles" in compound:
|
|
426
|
+
ligand = Ligand(
|
|
427
|
+
id="LIG",
|
|
428
|
+
smiles=compound["smiles"],
|
|
429
|
+
predict_affinity=parameters["predict_affinity"]
|
|
430
|
+
)
|
|
431
|
+
else:
|
|
432
|
+
ligand = Ligand(
|
|
433
|
+
id="LIG",
|
|
434
|
+
ccd=compound["ccd"],
|
|
435
|
+
predict_affinity=parameters["predict_affinity"]
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
# Create request
|
|
439
|
+
request = PredictionRequest(
|
|
440
|
+
polymers=[protein],
|
|
441
|
+
ligands=[ligand],
|
|
442
|
+
constraints=constraints if constraints else None,
|
|
443
|
+
recycling_steps=parameters["recycling_steps"],
|
|
444
|
+
sampling_steps=parameters["sampling_steps"],
|
|
445
|
+
diffusion_samples=parameters["diffusion_samples"]
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Add affinity parameters if enabled
|
|
449
|
+
if parameters["predict_affinity"]:
|
|
450
|
+
request.sampling_steps_affinity = parameters["sampling_steps_affinity"]
|
|
451
|
+
request.diffusion_samples_affinity = parameters["diffusion_samples_affinity"]
|
|
452
|
+
request.affinity_mw_correction = parameters["affinity_mw_correction"]
|
|
453
|
+
|
|
454
|
+
# Run prediction
|
|
455
|
+
response = self.client.predict(request)
|
|
456
|
+
|
|
457
|
+
# Extract results
|
|
458
|
+
result = {
|
|
459
|
+
"compound_name": compound["name"],
|
|
460
|
+
"compound_smiles": compound.get("smiles", ""),
|
|
461
|
+
"compound_ccd": compound.get("ccd", ""),
|
|
462
|
+
"structure_confidence": response.confidence_scores[0] if response.confidence_scores else None,
|
|
463
|
+
"structure_cif": response.structures[0].structure
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
# Add metadata
|
|
467
|
+
if "metadata" in compound:
|
|
468
|
+
for key, value in compound["metadata"].items():
|
|
469
|
+
result[f"compound_{key}"] = value
|
|
470
|
+
|
|
471
|
+
# Add affinity results if available
|
|
472
|
+
if response.affinities and "LIG" in response.affinities:
|
|
473
|
+
affinity = response.affinities["LIG"]
|
|
474
|
+
result.update({
|
|
475
|
+
"predicted_pic50": affinity.affinity_pic50[0],
|
|
476
|
+
"predicted_ic50_nm": 10 ** (-affinity.affinity_pic50[0]) * 1e9,
|
|
477
|
+
"binding_probability": affinity.affinity_probability_binary[0]
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
return result
|
|
481
|
+
|
|
482
|
+
except Exception as e:
|
|
483
|
+
return {
|
|
484
|
+
"compound_name": compound["name"],
|
|
485
|
+
"error": str(e)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async def _screen_single_compound_async(self, protein: Polymer, compound: Dict,
|
|
489
|
+
parameters: Dict, constraints: List) -> Dict:
|
|
490
|
+
"""Screen a single compound asynchronously."""
|
|
491
|
+
# Similar to sync version but uses await
|
|
492
|
+
try:
|
|
493
|
+
# Create ligand
|
|
494
|
+
if "smiles" in compound:
|
|
495
|
+
ligand = Ligand(
|
|
496
|
+
id="LIG",
|
|
497
|
+
smiles=compound["smiles"],
|
|
498
|
+
predict_affinity=parameters["predict_affinity"]
|
|
499
|
+
)
|
|
500
|
+
else:
|
|
501
|
+
ligand = Ligand(
|
|
502
|
+
id="LIG",
|
|
503
|
+
ccd=compound["ccd"],
|
|
504
|
+
predict_affinity=parameters["predict_affinity"]
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# Create request
|
|
508
|
+
request = PredictionRequest(
|
|
509
|
+
polymers=[protein],
|
|
510
|
+
ligands=[ligand],
|
|
511
|
+
constraints=constraints if constraints else None,
|
|
512
|
+
recycling_steps=parameters["recycling_steps"],
|
|
513
|
+
sampling_steps=parameters["sampling_steps"],
|
|
514
|
+
diffusion_samples=parameters["diffusion_samples"]
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# Add affinity parameters
|
|
518
|
+
if parameters["predict_affinity"]:
|
|
519
|
+
request.sampling_steps_affinity = parameters["sampling_steps_affinity"]
|
|
520
|
+
request.diffusion_samples_affinity = parameters["diffusion_samples_affinity"]
|
|
521
|
+
request.affinity_mw_correction = parameters["affinity_mw_correction"]
|
|
522
|
+
|
|
523
|
+
# Run prediction
|
|
524
|
+
response = await self.client.predict(request)
|
|
525
|
+
|
|
526
|
+
# Extract results (same as sync)
|
|
527
|
+
result = {
|
|
528
|
+
"compound_name": compound["name"],
|
|
529
|
+
"compound_smiles": compound.get("smiles", ""),
|
|
530
|
+
"compound_ccd": compound.get("ccd", ""),
|
|
531
|
+
"structure_confidence": response.confidence_scores[0] if response.confidence_scores else None,
|
|
532
|
+
"structure_cif": response.structures[0].structure
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
# Add metadata
|
|
536
|
+
if "metadata" in compound:
|
|
537
|
+
for key, value in compound["metadata"].items():
|
|
538
|
+
result[f"compound_{key}"] = value
|
|
539
|
+
|
|
540
|
+
# Add affinity results
|
|
541
|
+
if response.affinities and "LIG" in response.affinities:
|
|
542
|
+
affinity = response.affinities["LIG"]
|
|
543
|
+
result.update({
|
|
544
|
+
"predicted_pic50": affinity.affinity_pic50[0],
|
|
545
|
+
"predicted_ic50_nm": 10 ** (-affinity.affinity_pic50[0]) * 1e9,
|
|
546
|
+
"binding_probability": affinity.affinity_probability_binary[0]
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
return result
|
|
550
|
+
|
|
551
|
+
except Exception as e:
|
|
552
|
+
return {
|
|
553
|
+
"compound_name": compound["name"],
|
|
554
|
+
"error": str(e)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
def _process_batch_sync(self, protein: Polymer, batch: List[Dict],
|
|
558
|
+
parameters: Dict, constraints: List) -> List[Dict]:
|
|
559
|
+
"""Process a batch of compounds."""
|
|
560
|
+
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
561
|
+
futures = []
|
|
562
|
+
|
|
563
|
+
for compound in batch:
|
|
564
|
+
future = executor.submit(
|
|
565
|
+
self._screen_single_compound_sync,
|
|
566
|
+
protein, compound, parameters, constraints
|
|
567
|
+
)
|
|
568
|
+
futures.append(future)
|
|
569
|
+
|
|
570
|
+
results = [future.result() for future in futures]
|
|
571
|
+
|
|
572
|
+
return results
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
# Convenience functions
|
|
576
|
+
def quick_screen(target_sequence: str,
|
|
577
|
+
compounds: Union[List[Dict], str, Path],
|
|
578
|
+
target_name: str = "Target",
|
|
579
|
+
output_dir: Optional[Union[str, Path]] = None,
|
|
580
|
+
**kwargs) -> VirtualScreeningResult:
|
|
581
|
+
"""
|
|
582
|
+
Quick virtual screening with minimal setup.
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
target_sequence: Protein sequence
|
|
586
|
+
compounds: List of compounds or path to CSV/JSON file
|
|
587
|
+
target_name: Name of target
|
|
588
|
+
output_dir: Directory to save results (optional)
|
|
589
|
+
**kwargs: Additional parameters passed to VirtualScreening.screen()
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
VirtualScreeningResult
|
|
593
|
+
"""
|
|
594
|
+
screener = VirtualScreening()
|
|
595
|
+
result = screener.screen(
|
|
596
|
+
target_sequence=target_sequence,
|
|
597
|
+
compound_library=compounds,
|
|
598
|
+
target_name=target_name,
|
|
599
|
+
**kwargs
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
if output_dir:
|
|
603
|
+
saved_files = result.save_results(output_dir)
|
|
604
|
+
print(f"Results saved to: {output_dir}")
|
|
605
|
+
for key, path in saved_files.items():
|
|
606
|
+
print(f" - {key}: {path}")
|
|
607
|
+
|
|
608
|
+
return result
|